Moved code to presenter and now using defer
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import { Prisma } from "@trigger.dev/database";
|
||||
import { sqlDatabaseSchema } from "~/db.server";
|
||||
import { BasePresenter } from "./basePresenter.server";
|
||||
import { getUsage, getUsageSeries } from "~/services/platform.v3.server";
|
||||
import { createTimeSeriesData } from "~/utils/graphs";
|
||||
|
||||
type Options = {
|
||||
organizationId: string;
|
||||
};
|
||||
|
||||
export class UsagePresenter extends BasePresenter {
|
||||
public async call({ organizationId }: Options) {
|
||||
//periods
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
thirtyDaysAgo.setHours(0, 0, 0, 0);
|
||||
const endOfToday = new Date();
|
||||
endOfToday.setDate(endOfToday.getDate() + 1);
|
||||
endOfToday.setHours(23, 59, 59, 999);
|
||||
|
||||
const past30Days = getUsageSeries(organizationId, {
|
||||
from: thirtyDaysAgo,
|
||||
to: endOfToday,
|
||||
window: "DAY",
|
||||
}).then((data) => {
|
||||
if (!data) return [];
|
||||
return createTimeSeriesData({
|
||||
startDate: thirtyDaysAgo,
|
||||
endDate: endOfToday,
|
||||
window: "DAY",
|
||||
data:
|
||||
data.data.map((period) => ({
|
||||
date: new Date(period.windowStart),
|
||||
value: period.value,
|
||||
})) ?? [],
|
||||
}).map((period) => ({
|
||||
date: period.date.toISOString(),
|
||||
dollars: (period.value ?? 0) / 100,
|
||||
}));
|
||||
});
|
||||
|
||||
const startOfMonth = new Date();
|
||||
startOfMonth.setDate(1);
|
||||
startOfMonth.setHours(0, 0, 0, 0);
|
||||
|
||||
const now = new Date();
|
||||
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999);
|
||||
|
||||
const usage = getUsage(organizationId, { from: startOfMonth, to: endOfMonth }).then((data) => {
|
||||
const current = (data?.cents ?? 0) / 100;
|
||||
const percentageThroughMonth = new Date().getDate() / endOfMonth.getDate();
|
||||
return {
|
||||
current: current,
|
||||
projected: current / percentageThroughMonth,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
past30Days,
|
||||
usage,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ArrowRightIcon } from "@heroicons/react/24/solid";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { Bar, BarChart, Rectangle, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
|
||||
import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { redirect, typeddefer, typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { UsageBar } from "~/components/billing/v3/UsageBar";
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { Header2, Header3 } from "~/components/primitives/Headers";
|
||||
@@ -14,6 +14,10 @@ import { createTimeSeriesData } from "~/utils/graphs";
|
||||
import { formatCurrency } from "~/utils/numberFormatter";
|
||||
import { OrganizationParamsSchema, organizationPath } from "~/utils/pathBuilder";
|
||||
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
|
||||
import { UsagePresenter } from "~/presenters/v3/UsagePresenter.server";
|
||||
import { Suspense } from "react";
|
||||
import { Await } from "@remix-run/react";
|
||||
import { Spinner } from "~/components/primitives/Spinner";
|
||||
|
||||
export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
await requireUserId(request);
|
||||
@@ -32,52 +36,11 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
throw new Response(null, { status: 404, statusText: "Organization not found" });
|
||||
}
|
||||
|
||||
//periods
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
thirtyDaysAgo.setHours(0, 0, 0, 0);
|
||||
const endOfToday = new Date();
|
||||
endOfToday.setDate(endOfToday.getDate() + 1);
|
||||
endOfToday.setHours(23, 59, 59, 999);
|
||||
const presenter = new UsagePresenter();
|
||||
const { past30Days, usage } = await presenter.call({ organizationId: organization.id });
|
||||
|
||||
const past30Days = await getUsageSeries(organization.id, {
|
||||
from: thirtyDaysAgo,
|
||||
to: endOfToday,
|
||||
window: "DAY",
|
||||
});
|
||||
|
||||
const startOfMonth = new Date();
|
||||
startOfMonth.setDate(1);
|
||||
startOfMonth.setHours(0, 0, 0, 0);
|
||||
|
||||
const now = new Date();
|
||||
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999);
|
||||
|
||||
const usageData = await getUsage(organization.id, { from: startOfMonth, to: endOfMonth });
|
||||
|
||||
const current = (usageData?.cents ?? 0) / 100;
|
||||
const percentageThroughMonth = new Date().getDate() / endOfMonth.getDate();
|
||||
const usage = {
|
||||
current: current,
|
||||
projected: current / percentageThroughMonth,
|
||||
};
|
||||
|
||||
return typedjson({
|
||||
past30Days: past30Days
|
||||
? createTimeSeriesData({
|
||||
startDate: thirtyDaysAgo,
|
||||
endDate: endOfToday,
|
||||
window: "DAY",
|
||||
data:
|
||||
past30Days.data.map((period) => ({
|
||||
date: new Date(period.windowStart),
|
||||
value: period.value,
|
||||
})) ?? [],
|
||||
}).map((period) => ({
|
||||
date: period.date,
|
||||
dollars: (period.value ?? 0) / 100,
|
||||
}))
|
||||
: [],
|
||||
return typeddefer({
|
||||
past30Days,
|
||||
usage,
|
||||
});
|
||||
}
|
||||
@@ -109,79 +72,112 @@ export default function ChoosePlanPage() {
|
||||
<PageTitle title="Usage" />
|
||||
</NavBar>
|
||||
<PageBody scrollable={true}>
|
||||
<Header2 spacing>This month</Header2>
|
||||
<div className="flex w-full flex-col gap-2 rounded-sm border border-grid-dimmed p-4">
|
||||
<div className="flex w-full items-center gap-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Header3 className="">Month-to-date</Header3>
|
||||
<p className="text-3xl font-medium text-text-bright">
|
||||
{formatCurrency(usage.current, false)}
|
||||
</p>
|
||||
</div>
|
||||
<ArrowRightIcon className="h-6 w-6 text-text-dimmed/50" />
|
||||
<div className="flex flex-col gap-2 text-text-dimmed">
|
||||
<Header3 className="text-text-dimmed">Projected</Header3>
|
||||
<p className="text-3xl font-medium">{formatCurrency(usage.projected, false)}</p>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div>
|
||||
<Header2 spacing>This month</Header2>
|
||||
<div className="flex w-full flex-col gap-2 rounded-sm border border-grid-dimmed p-4">
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<Await resolve={usage}>
|
||||
{(usage) => (
|
||||
<>
|
||||
<div className="flex w-full items-center gap-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Header3 className="">Month-to-date</Header3>
|
||||
<p className="text-3xl font-medium text-text-bright">
|
||||
{formatCurrency(usage.current, false)}
|
||||
</p>
|
||||
</div>
|
||||
<ArrowRightIcon className="h-6 w-6 text-text-dimmed/50" />
|
||||
<div className="flex flex-col gap-2 text-text-dimmed">
|
||||
<Header3 className="text-text-dimmed">Projected</Header3>
|
||||
<p className="text-3xl font-medium">
|
||||
{formatCurrency(usage.projected, false)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<UsageBar
|
||||
current={usage.current}
|
||||
projectedUsage={usage.projected}
|
||||
tierLimit={
|
||||
(currentPlan?.v3Subscription?.plan?.limits.includedUsage ?? 0) / 100
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Await>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
<UsageBar
|
||||
current={usage.current}
|
||||
projectedUsage={usage.projected}
|
||||
tierLimit={(currentPlan?.v3Subscription?.plan?.limits.includedUsage ?? 0) / 100}
|
||||
/>
|
||||
</div>
|
||||
<Header2 spacing>Past 30 days</Header2>
|
||||
<div className="rounded-sm border border-grid-dimmed p-4">
|
||||
<Header3>Usage</Header3>
|
||||
<ResponsiveContainer width="100%" height="100%" className="min-h-96">
|
||||
<BarChart
|
||||
data={past30Days}
|
||||
margin={{
|
||||
top: 20,
|
||||
right: 0,
|
||||
left: -10,
|
||||
bottom: 10,
|
||||
}}
|
||||
>
|
||||
<XAxis
|
||||
stroke="#5F6570"
|
||||
fontSize={12}
|
||||
tickLine={false}
|
||||
axisLine={true}
|
||||
dataKey={(item: { date: Date }) => {
|
||||
if (!item.date) return "";
|
||||
const date = new Date(item.date);
|
||||
if (date.getDate() === 1) {
|
||||
return dateFormatter.format(date);
|
||||
}
|
||||
return `${date.getDate()}`;
|
||||
}}
|
||||
className="text-xs"
|
||||
/>
|
||||
<YAxis
|
||||
stroke="#5F6570"
|
||||
fontSize={12}
|
||||
tickLine={false}
|
||||
axisLine={true}
|
||||
allowDecimals={true}
|
||||
tickFormatter={(value) => `$${value.toFixed(2)}`}
|
||||
/>
|
||||
<Tooltip
|
||||
cursor={{ fill: "rgba(255,255,255,0.05)" }}
|
||||
contentStyle={tooltipStyle}
|
||||
labelFormatter={(value, data) => {
|
||||
const dateString = data.at(0)?.payload.date;
|
||||
if (!dateString) {
|
||||
return "";
|
||||
}
|
||||
<div>
|
||||
<Header2 spacing>Past 30 days</Header2>
|
||||
<div className="rounded-sm border border-grid-dimmed p-4">
|
||||
<Header3>Usage</Header3>
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<Await resolve={past30Days}>
|
||||
{(past30Days) => (
|
||||
<ResponsiveContainer width="100%" height="100%" className="min-h-96">
|
||||
<BarChart
|
||||
data={past30Days}
|
||||
margin={{
|
||||
top: 20,
|
||||
right: 0,
|
||||
left: -10,
|
||||
bottom: 10,
|
||||
}}
|
||||
>
|
||||
<XAxis
|
||||
stroke="#5F6570"
|
||||
fontSize={12}
|
||||
tickLine={false}
|
||||
axisLine={true}
|
||||
dataKey={(item: { date: string }) => {
|
||||
if (!item.date) return "";
|
||||
const date = new Date(item.date);
|
||||
if (date.getDate() === 1) {
|
||||
return dateFormatter.format(date);
|
||||
}
|
||||
return `${date.getDate()}`;
|
||||
}}
|
||||
className="text-xs"
|
||||
/>
|
||||
<YAxis
|
||||
stroke="#5F6570"
|
||||
fontSize={12}
|
||||
tickLine={false}
|
||||
axisLine={true}
|
||||
allowDecimals={true}
|
||||
tickFormatter={(value) => `$${value.toFixed(2)}`}
|
||||
/>
|
||||
<Tooltip
|
||||
cursor={{ fill: "rgba(255,255,255,0.05)" }}
|
||||
contentStyle={tooltipStyle}
|
||||
labelFormatter={(value, data) => {
|
||||
const dateString = data.at(0)?.payload.date;
|
||||
if (!dateString) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return dateFormatter.format(new Date(dateString));
|
||||
}}
|
||||
formatter={(value, data) => [`$${value.toLocaleString()}`, ""]}
|
||||
/>
|
||||
<Bar dataKey="dollars" fill="#28BF5C" activeBar={<Rectangle fill="#5ADE87" />} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
return dateFormatter.format(new Date(dateString));
|
||||
}}
|
||||
formatter={(value, data) => [
|
||||
`$${
|
||||
typeof value === "number" ? value.toFixed(2) : value.toLocaleString()
|
||||
}`,
|
||||
"",
|
||||
]}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="dollars"
|
||||
fill="#28BF5C"
|
||||
activeBar={<Rectangle fill="#5ADE87" />}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
</Await>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageBody>
|
||||
</PageContainer>
|
||||
|
||||
Reference in New Issue
Block a user