| 4 | 4 | | import { MICROS_PER_DOLLAR, type UsageSlice } from "@g1t/contracts"; |
| 5 | 5 | | |
| 6 | 6 | | import type { Route } from "./+types/usage"; |
| 7 | | − | import { foldTasks, usageTask } from "../../lib/billing"; |
| 7 | + | import { foldTasks, planStatus, usageTask } from "../../lib/billing"; |
| 8 | 8 | | import { page } from "../../lib/meta"; |
| 9 | 9 | | import { ButtonLink } from "../../components/ui"; |
| 10 | 10 | | import { billing } from "../../lib/services.server"; |
| ⋯ |
| 38 | 38 | | const asked = new URL(request.url).searchParams.get("period"); |
| 39 | 39 | | const period: Period = asked && asked in PERIODS ? (asked as Period) : "month"; |
| 40 | 40 | | const since = start(period); |
| 41 | | − | const [usage, account] = await Promise.all([ |
| 41 | + | const [usage, account, features, entitlements] = await Promise.all([ |
| 42 | 42 | | billing.usage(params.owner, viewer, since.toISOString()), |
| 43 | 43 | | billing.account(params.owner, viewer), |
| 44 | + | billing.features(params.owner, viewer).catch(() => null), |
| 45 | + | billing.entitlements(params.owner).catch(() => null), |
| 44 | 46 | | ]); |
| 45 | | − | return { period, since: since.toISOString(), usage: unwrap(usage), account: unwrap(account) }; |
| 47 | + | const plan = features?.ok ? (features.value.find((state) => state.plan.feature === "plan") ?? null) : null; |
| 48 | + | // A comped workspace is charged nothing, so it has no credit to run down. |
| 49 | + | const comped = planStatus(plan, entitlements).kind === "comped"; |
| 50 | + | return { period, since: since.toISOString(), usage: unwrap(usage), account: unwrap(account), comped }; |
| 46 | 51 | | } |
| 47 | 52 | | |
| 48 | 53 | | function dollars(micros: number, digits = 2): string { |
| ⋯ |
| 180 | 185 | | } |
| 181 | 186 | | |
| 182 | 187 | | export default function UsagePage({ loaderData, params }: Route.ComponentProps) { |
| 183 | | − | const { period, since, usage, account } = loaderData; |
| 188 | + | const { period, since, usage, account, comped } = loaderData; |
| 184 | 189 | | const byTask = foldTasks(usage.byTask); |
| 185 | 190 | | const base = `/${params.owner}`; |
| 186 | 191 | | const days = Math.max(1, Math.ceil((Date.now() - new Date(since).getTime()) / 86_400_000)); |
| ⋯ |
| 232 | 237 | | value={usage.runs ? dollars(total / usage.runs, 3) : "—"} |
| 233 | 238 | | note="Making a change, reviewing, revising…" |
| 234 | 239 | | /> |
| 235 | | − | <div className="rounded-2xl bg-surface p-5 ring-1 ring-line"> |
| 236 | | − | <p className="flex items-center justify-between text-sm text-muted"> |
| 237 | | − | Credit left |
| 238 | | − | {!usage.free && ( |
| 239 | | − | <Link to={`${base}/-/billing`} className="text-xs text-accent hover:underline"> |
| 240 | | − | Add credit |
| 241 | | − | </Link> |
| 242 | | − | )} |
| 243 | | − | </p> |
| 244 | | − | <p className={`mt-2 text-3xl font-semibold tracking-tight tabular-nums ${account.balanceMicros <= 0 ? "text-warn" : ""}`}> |
| 245 | | − | {dollars(account.balanceMicros)} |
| 246 | | − | </p> |
| 247 | | − | <p className="mt-1 text-xs text-faint"> |
| 248 | | − | {usage.free |
| 249 | | − | ? "Not drawn down while g1t is free." |
| 250 | | − | : runway == null |
| 251 | | − | ? "Nothing spent in this period." |
| 252 | | − | : `About ${runway} ${runway === 1 ? "day" : "days"} at this rate.`} |
| 253 | | − | </p> |
| 254 | | − | </div> |
| 240 | + | {comped ? ( |
| 241 | + | // Nothing is charged to a comped workspace: no credit to run down. |
| 242 | + | <div className="rounded-2xl bg-surface p-5 ring-1 ring-line"> |
| 243 | + | <p className="text-sm text-muted">Credit</p> |
| 244 | + | <p className="mt-2 text-3xl font-semibold tracking-tight text-accent">Comped</p> |
| 245 | + | <p className="mt-1 text-xs text-faint">Recorded at what it costs; nothing is charged to this workspace.</p> |
| 246 | + | </div> |
| 247 | + | ) : ( |
| 248 | + | <div className="rounded-2xl bg-surface p-5 ring-1 ring-line"> |
| 249 | + | <p className="flex items-center justify-between text-sm text-muted"> |
| 250 | + | Credit left |
| 251 | + | {!usage.free && ( |
| 252 | + | <Link to={`${base}/-/billing`} className="text-xs text-accent hover:underline"> |
| 253 | + | Add credit |
| 254 | + | </Link> |
| 255 | + | )} |
| 256 | + | </p> |
| 257 | + | <p className={`mt-2 text-3xl font-semibold tracking-tight tabular-nums ${account.balanceMicros <= 0 ? "text-warn" : ""}`}> |
| 258 | + | {dollars(account.balanceMicros)} |
| 259 | + | </p> |
| 260 | + | <p className="mt-1 text-xs text-faint"> |
| 261 | + | {usage.free |
| 262 | + | ? "Not drawn down while g1t is free." |
| 263 | + | : runway == null |
| 264 | + | ? "Nothing spent in this period." |
| 265 | + | : `About ${runway} ${runway === 1 ? "day" : "days"} at this rate.`} |
| 266 | + | </p> |
| 267 | + | </div> |
| 268 | + | )} |
| 255 | 269 | | </div> |
| 256 | 270 | | |
| 257 | 271 | | <section className="rounded-2xl bg-surface p-5 ring-1 ring-line"> |