mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-29 06:33:28 +00:00
fix(ui): show the budget period date range instead of "cycle" on team member spend (#48488)
* fix(ui): show the budget period date range instead of "cycle" on team member spend Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(ui): derive the budget period start in UTC before showing it in local time Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(ui): keep clock times on sub-day budget periods and restore the total spend lower bound wording Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: joshua <joshua@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
549e6d4f46
commit
100529391a
6 changed files with 198 additions and 35 deletions
|
|
@ -48,7 +48,7 @@ describe("MyUserTab", () => {
|
|||
expect(screen.getByText("Never resets")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows the next reset time when the member budget has a reset window", () => {
|
||||
it("shows the budget period and next reset when the member budget has a reset window", () => {
|
||||
vi.mocked(useMyTeamMember).mockReturnValue({
|
||||
data: {
|
||||
user_id: "user-1",
|
||||
|
|
@ -69,6 +69,8 @@ describe("MyUserTab", () => {
|
|||
|
||||
renderWithProviders(<MyUserTab teamId="team-1" />);
|
||||
|
||||
expect(screen.getByText("Resets Jul 15, 2026 12:00")).toBeInTheDocument();
|
||||
expect(screen.getByText("Current Period Spend (USD)")).toBeInTheDocument();
|
||||
expect(screen.getByText("Jun 15 - Jul 15, 2026 (resets Jul 15, 2026 12:00)")).toBeInTheDocument();
|
||||
expect(screen.queryByText(/cycle/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { describeBudgetResetSchedule } from "@/utils/budgetUtils";
|
||||
import { BudgetResetSchedule, describeBudgetResetSchedule } from "@/utils/budgetUtils";
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import { SimpleTooltip } from "@/components/ui/tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
|
@ -30,6 +30,12 @@ const formatRateLimit = (value: number | null | undefined): string => {
|
|||
return formatNumberWithCommas(value, 0);
|
||||
};
|
||||
|
||||
function describePeriodLine(schedule: BudgetResetSchedule): string {
|
||||
if (schedule.period) return `${schedule.period} (resets ${schedule.label})`;
|
||||
if (schedule.resetAt) return `Resets ${schedule.label}`;
|
||||
return schedule.label;
|
||||
}
|
||||
|
||||
export default function MyUserTab({ teamId }: MyUserTabProps) {
|
||||
const { data, isLoading, error } = useMyTeamMember(teamId);
|
||||
|
||||
|
|
@ -93,16 +99,14 @@ export default function MyUserTab({ teamId }: MyUserTabProps) {
|
|||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardContent>
|
||||
{labelWithTooltip("Current Cycle Spend (USD)", budgetReset.tooltip)}
|
||||
{labelWithTooltip("Current Period Spend (USD)", budgetReset.tooltip)}
|
||||
<div className="mt-2">
|
||||
<h3 className="text-2xl font-semibold">${formatNumber(spend, 4)}</h3>
|
||||
<span className="text-muted-foreground">
|
||||
of {maxBudget === null ? "Unlimited" : `$${formatNumber(maxBudget, 4)}`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 text-muted-foreground">
|
||||
{budgetReset.resetAt ? `Resets ${budgetReset.label}` : budgetReset.label}
|
||||
</div>
|
||||
<div className="mt-1 text-muted-foreground">{describePeriodLine(budgetReset)}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
|
@ -121,7 +125,7 @@ export default function MyUserTab({ teamId }: MyUserTabProps) {
|
|||
<CardContent>
|
||||
{labelWithTooltip(
|
||||
"Total Spend (USD)",
|
||||
"Cumulative spend across budget cycles within this team. This is a lower bound: lifetime tracking began 2026-04-21, so cycles completed before then are not included. It is never less than Current Cycle Spend.",
|
||||
"Cumulative spend within this team across budget periods. This is a lower bound: lifetime tracking began 2026-04-21, so periods that reset before then are not included. It is never less than Current Period Spend.",
|
||||
)}
|
||||
<h4 className="mt-2 text-xl font-semibold">${formatNumber(totalSpend, 4)}</h4>
|
||||
</CardContent>
|
||||
|
|
|
|||
|
|
@ -316,6 +316,29 @@ describe("TeamMembersComponent", () => {
|
|||
expect(screen.getByText("Jul 15, 2026 12:00")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows the budget period date range instead of a bare reset date when the duration is known", () => {
|
||||
const teamData = createMockTeamData();
|
||||
teamData.team_memberships[0].litellm_budget_table!.budget_duration = "30d";
|
||||
renderWithProviders(
|
||||
<TeamMembersComponent
|
||||
teamData={teamData}
|
||||
canEditTeam={false}
|
||||
handleMemberDelete={mockHandleMemberDelete}
|
||||
onMemberSpendReset={mockOnMemberSpendReset}
|
||||
onMemberBudgetReset={mockOnMemberBudgetReset}
|
||||
setSelectedEditMember={mockSetSelectedEditMember}
|
||||
setIsEditMemberModalVisible={mockSetIsEditMemberModalVisible}
|
||||
setIsAddMemberModalVisible={mockSetIsAddMemberModalVisible}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("columnheader", { name: /^Budget Period/ })).toBeInTheDocument();
|
||||
expect(screen.getByRole("columnheader", { name: /^Current Period Spend \(USD\)/ })).toBeInTheDocument();
|
||||
expect(screen.getByText("Jun 15 - Jul 15, 2026")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Jul 15, 2026 12:00")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/cycle/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows Never resets for a member whose budget has no reset window", () => {
|
||||
const teamData = createMockTeamData();
|
||||
teamData.team_memberships[0].litellm_budget_table!.budget_reset_at = null;
|
||||
|
|
@ -696,7 +719,7 @@ describe("TeamMembersComponent", () => {
|
|||
/>,
|
||||
);
|
||||
|
||||
it("resets the member's current cycle spend to $0 after confirming, then refreshes the team", async () => {
|
||||
it("resets the member's current period spend to $0 after confirming, then refreshes the team", async () => {
|
||||
const user = userEvent.setup();
|
||||
POST.mockResolvedValue({ data: {} });
|
||||
renderEditableTab();
|
||||
|
|
@ -745,7 +768,7 @@ describe("TeamMembersComponent", () => {
|
|||
expect(POST).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("disables the reset with a reason on members that have no current cycle spend", async () => {
|
||||
it("disables the reset with a reason on members that have no current period spend", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderEditableTab();
|
||||
|
||||
|
|
@ -753,7 +776,7 @@ describe("TeamMembersComponent", () => {
|
|||
"reset-member-spend",
|
||||
);
|
||||
await user.hover(resetButton);
|
||||
expect(await screen.findByText("No current cycle spend to reset")).toBeInTheDocument();
|
||||
expect(await screen.findByText("No current period spend to reset")).toBeInTheDocument();
|
||||
|
||||
await user.click(resetButton);
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ export default function TeamMemberTab({
|
|||
return "0";
|
||||
};
|
||||
|
||||
const getUserCurrentCycleSpend = (userId: string | null): number => {
|
||||
const getUserCurrentPeriodSpend = (userId: string | null): number => {
|
||||
if (!userId) return 0;
|
||||
return findMembership(userId)?.spend ?? 0;
|
||||
};
|
||||
|
|
@ -153,12 +153,16 @@ export default function TeamMemberTab({
|
|||
return findMembership(userId)?.litellm_budget_table?.budget_reset_at ?? null;
|
||||
};
|
||||
|
||||
const renderBudgetReset = (userId: string | null) => {
|
||||
const getUserBudgetSchedule = (userId: string | null) => {
|
||||
const budget = findMembership(userId)?.litellm_budget_table;
|
||||
const schedule = describeBudgetResetSchedule(budget?.budget_duration ?? null, budget?.budget_reset_at ?? null);
|
||||
return describeBudgetResetSchedule(budget?.budget_duration ?? null, budget?.budget_reset_at ?? null);
|
||||
};
|
||||
|
||||
const renderBudgetPeriod = (userId: string | null) => {
|
||||
const schedule = getUserBudgetSchedule(userId);
|
||||
return (
|
||||
<SimpleTooltip content={schedule.tooltip}>
|
||||
<span>{schedule.label}</span>
|
||||
<span>{schedule.period ?? schedule.label}</span>
|
||||
</SimpleTooltip>
|
||||
);
|
||||
};
|
||||
|
|
@ -200,22 +204,22 @@ export default function TeamMemberTab({
|
|||
{
|
||||
title: (
|
||||
<span className="flex items-center gap-1">
|
||||
Current Cycle Spend (USD)
|
||||
<SimpleTooltip content="Spend in this member's current budget cycle, the value checked against their budget. The Budget Reset column shows when this cycle ends, or Never resets. Spend above a rollover cap at reset carries into the next cycle.">
|
||||
<CircleHelp className="size-4" aria-label="Current cycle spend information" />
|
||||
Current Period Spend (USD)
|
||||
<SimpleTooltip content="Spend in this member's current budget period, the value checked against their budget. The Budget Period column shows the dates that period covers, or Never resets. Spend above a rollover cap at reset carries into the next period.">
|
||||
<CircleHelp className="size-4" aria-label="Current period spend information" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
),
|
||||
key: "spend",
|
||||
numeric: true,
|
||||
sortValue: (record: Member) => getUserCurrentCycleSpend(record.user_id),
|
||||
render: (record: Member) => <MoneyCell value={getUserCurrentCycleSpend(record.user_id)} decimals={2} />,
|
||||
sortValue: (record: Member) => getUserCurrentPeriodSpend(record.user_id),
|
||||
render: (record: Member) => <MoneyCell value={getUserCurrentPeriodSpend(record.user_id)} decimals={2} />,
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<span className="flex items-center gap-1">
|
||||
Total Spend (USD)
|
||||
<SimpleTooltip content="Cumulative spend by this member within this team, across budget cycles. This is a lower bound: lifetime tracking began 2026-04-21, so cycles completed before then are not included. It is never less than Current Cycle Spend.">
|
||||
<SimpleTooltip content="Cumulative spend by this member within this team, across budget periods. This is a lower bound: lifetime tracking began 2026-04-21, so periods that reset before then are not included. It is never less than Current Period Spend.">
|
||||
<CircleHelp className="size-4" aria-label="Total spend information" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
|
|
@ -263,10 +267,10 @@ export default function TeamMemberTab({
|
|||
},
|
||||
},
|
||||
{
|
||||
title: "Budget Reset",
|
||||
title: "Budget Period",
|
||||
key: "budget_reset",
|
||||
sortValue: (record: Member) => getUserBudgetReset(record.user_id),
|
||||
render: (record: Member) => renderBudgetReset(record.user_id),
|
||||
render: (record: Member) => renderBudgetPeriod(record.user_id),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
|
|
@ -314,10 +318,12 @@ export default function TeamMemberTab({
|
|||
|
||||
const resetSpendDisabledReason = (member: Member): string | null => {
|
||||
if (!isProxyAdmin && member.user_id === userId) return "Ask a proxy admin to reset your own spend";
|
||||
if (getUserCurrentCycleSpend(member.user_id) <= 0) return "No current cycle spend to reset";
|
||||
if (getUserCurrentPeriodSpend(member.user_id) <= 0) return "No current period spend to reset";
|
||||
return null;
|
||||
};
|
||||
|
||||
const resetSpendPeriod = getUserBudgetSchedule(memberToResetSpend?.user_id ?? null).period;
|
||||
|
||||
return (
|
||||
<>
|
||||
<MemberTable
|
||||
|
|
@ -352,13 +358,16 @@ export default function TeamMemberTab({
|
|||
<DialogTitle>Reset Team Member Spend</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p>
|
||||
Reset current cycle spend for{" "}
|
||||
Reset current period spend for{" "}
|
||||
<strong>{memberToResetSpend?.user_email || memberToResetSpend?.user_id}</strong> in this team to{" "}
|
||||
<strong>$0</strong>?
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Current cycle spend:{" "}
|
||||
<strong>${formatNumberWithCommas(getUserCurrentCycleSpend(memberToResetSpend?.user_id ?? null), 4)}</strong>
|
||||
Current period spend
|
||||
{resetSpendPeriod ? ` (${resetSpendPeriod})` : ""}:{" "}
|
||||
<strong>
|
||||
${formatNumberWithCommas(getUserCurrentPeriodSpend(memberToResetSpend?.user_id ?? null), 4)}
|
||||
</strong>
|
||||
. This is the value checked against the member's budget. Total spend and logs are preserved.
|
||||
</p>
|
||||
<DialogFooter>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ import { afterEach, describe, expect, it } from "vitest";
|
|||
|
||||
import { budgetPeriodRange, describeBudgetResetSchedule, formatBudgetReset, formatBudgetResetUtc } from "./budgetUtils";
|
||||
|
||||
const localIso = (year: number, monthIndex: number, day: number, hour = 12): string =>
|
||||
new Date(year, monthIndex, day, hour, 0, 0).toISOString();
|
||||
|
||||
const originalTimezone: string | undefined = process.env.TZ;
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -23,15 +26,18 @@ describe("formatBudgetResetUtc", () => {
|
|||
});
|
||||
|
||||
describe("describeBudgetResetSchedule", () => {
|
||||
it("reports Never resets when the budget has no duration and no scheduled reset", () => {
|
||||
it("reports lifetime spend with no period when the budget has no duration and no scheduled reset", () => {
|
||||
const schedule = describeBudgetResetSchedule(null, null);
|
||||
expect(schedule.label).toBe("Never resets");
|
||||
expect(schedule.resetAt).toBeNull();
|
||||
expect(schedule.period).toBeNull();
|
||||
expect(schedule.tooltip).toContain("lifetime spend");
|
||||
});
|
||||
|
||||
it("reports Unknown when a duration is set but no reset has been scheduled yet", () => {
|
||||
const schedule = describeBudgetResetSchedule("30d", null);
|
||||
expect(schedule.label).toBe("Unknown");
|
||||
expect(schedule.period).toBeNull();
|
||||
expect(schedule.tooltip).toContain("every 30d");
|
||||
});
|
||||
|
||||
|
|
@ -41,7 +47,74 @@ describe("describeBudgetResetSchedule", () => {
|
|||
expect(schedule.label).toBe("Jul 15, 2026 12:00");
|
||||
expect(schedule.resetAt).toBe(resetAt.toISOString());
|
||||
expect(schedule.tooltip).toContain(Intl.DateTimeFormat().resolvedOptions().timeZone);
|
||||
expect(schedule.tooltip).toContain("carries into the next cycle");
|
||||
expect(schedule.tooltip).toContain("carries into the next period");
|
||||
});
|
||||
|
||||
it("treats 30d as one calendar month back from the reset, like the proxy does", () => {
|
||||
const schedule = describeBudgetResetSchedule("30d", localIso(2026, 10, 1, 0));
|
||||
expect(schedule.period).toBe("Oct 1 - Nov 1, 2026");
|
||||
expect(schedule.tooltip).toContain("covers Oct 1 - Nov 1, 2026 and resets every 30d");
|
||||
});
|
||||
|
||||
it("subtracts the exact duration for day and week windows", () => {
|
||||
expect(describeBudgetResetSchedule("7d", localIso(2026, 6, 15)).period).toBe("Jul 8 - Jul 15, 2026");
|
||||
expect(describeBudgetResetSchedule("2w", localIso(2026, 6, 15)).period).toBe("Jul 1 - Jul 15, 2026");
|
||||
expect(describeBudgetResetSchedule("weekly", localIso(2026, 6, 15)).period).toBe("Jul 8 - Jul 15, 2026");
|
||||
});
|
||||
|
||||
it("walks the month back in UTC so a non-UTC viewer sees the proxy's window", () => {
|
||||
const originalTz = process.env.TZ;
|
||||
process.env.TZ = "America/New_York";
|
||||
try {
|
||||
const schedule = describeBudgetResetSchedule("30d", "2026-03-01T00:00:00Z");
|
||||
expect(schedule.period).toBe("Jan 31 - Feb 28, 2026");
|
||||
} finally {
|
||||
process.env.TZ = originalTz;
|
||||
}
|
||||
});
|
||||
|
||||
it("clamps month windows to the last day of a shorter month", () => {
|
||||
expect(describeBudgetResetSchedule("1mo", localIso(2026, 2, 31)).period).toBe("Feb 28 - Mar 31, 2026");
|
||||
});
|
||||
|
||||
it("spells out both years when the period crosses a year boundary", () => {
|
||||
expect(describeBudgetResetSchedule("30d", localIso(2027, 0, 10)).period).toBe("Dec 10, 2026 - Jan 10, 2027");
|
||||
});
|
||||
|
||||
it("keeps both clock times when an hourly window crosses midnight", () => {
|
||||
expect(describeBudgetResetSchedule("1h", localIso(2026, 6, 16, 0)).period).toBe(
|
||||
"Jul 15, 2026 23:00 - Jul 16, 2026 00:00",
|
||||
);
|
||||
});
|
||||
|
||||
it("shows seconds for a window shorter than a minute", () => {
|
||||
expect(describeBudgetResetSchedule("30s", localIso(2026, 6, 15)).period).toBe("Jul 15, 2026 11:59:30 - 12:00:00");
|
||||
});
|
||||
|
||||
it("shows clock times for a window that starts and ends on the same day", () => {
|
||||
expect(describeBudgetResetSchedule("1h", localIso(2026, 6, 15)).period).toBe("Jul 15, 2026 11:00 - 12:00");
|
||||
});
|
||||
|
||||
it("falls back to a one day window for a duration the proxy would not recognise", () => {
|
||||
expect(describeBudgetResetSchedule("fortnightly", localIso(2026, 6, 15)).period).toBe("Jul 14 - Jul 15, 2026");
|
||||
});
|
||||
|
||||
it("shows no period when the reset date is unparseable", () => {
|
||||
const schedule = describeBudgetResetSchedule("30d", "not-a-date");
|
||||
expect(schedule.label).toBe("Unknown");
|
||||
expect(schedule.period).toBeNull();
|
||||
expect(schedule.tooltip).not.toMatch(/NaN|Invalid/);
|
||||
});
|
||||
|
||||
it("never describes the window as a cycle", () => {
|
||||
const schedules = [
|
||||
describeBudgetResetSchedule("30d", localIso(2026, 6, 15)),
|
||||
describeBudgetResetSchedule("30d", null),
|
||||
describeBudgetResetSchedule(null, null),
|
||||
];
|
||||
for (const schedule of schedules) {
|
||||
expect(`${schedule.label} ${schedule.tooltip}`).not.toMatch(/cycle/i);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import dayjs from "dayjs";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
|
@ -20,10 +20,55 @@ export function formatBudgetResetUtc(iso: string | null | undefined): string | n
|
|||
export interface BudgetResetSchedule {
|
||||
label: string;
|
||||
resetAt: string | null;
|
||||
period: string | null;
|
||||
tooltip: string;
|
||||
}
|
||||
|
||||
const ROLLOVER_NOTE = "Spend above a rollover cap at reset carries into the next cycle.";
|
||||
const ROLLOVER_NOTE = "Spend above a rollover cap at reset carries into the next period.";
|
||||
|
||||
const DURATION_WORD_ALIASES: Record<string, string> = {
|
||||
hourly: "1h",
|
||||
daily: "24h",
|
||||
weekly: "7d",
|
||||
monthly: "30d",
|
||||
};
|
||||
|
||||
const DURATION_PATTERN = /^(\d+)(mo|[smhdw])$/;
|
||||
|
||||
// Mirrors the proxy's get_budget_window_start: "30d" and "Nmo" span calendar months,
|
||||
// everything else is a fixed length, and an unrecognised duration resets at the next midnight.
|
||||
export function getBudgetPeriodStart(budgetDuration: string, resetAt: Dayjs): Dayjs {
|
||||
const trimmed = budgetDuration.trim();
|
||||
const normalized = DURATION_WORD_ALIASES[trimmed.toLowerCase()] ?? trimmed;
|
||||
const match = DURATION_PATTERN.exec(normalized);
|
||||
if (!match) return resetAt.subtract(1, "day");
|
||||
const value = Number(match[1]);
|
||||
switch (match[2]) {
|
||||
case "mo":
|
||||
return resetAt.subtract(value, "month");
|
||||
case "d":
|
||||
return value === 30 ? resetAt.subtract(1, "month") : resetAt.subtract(value, "day");
|
||||
case "w":
|
||||
return resetAt.subtract(value, "week");
|
||||
case "h":
|
||||
return resetAt.subtract(value, "hour");
|
||||
case "m":
|
||||
return resetAt.subtract(value, "minute");
|
||||
default:
|
||||
return resetAt.subtract(value, "second");
|
||||
}
|
||||
}
|
||||
|
||||
export function formatBudgetPeriod(start: Dayjs, end: Dayjs): string {
|
||||
const windowSeconds = end.diff(start, "second");
|
||||
if (windowSeconds < 24 * 60 * 60) {
|
||||
const time = windowSeconds < 60 ? "HH:mm:ss" : "HH:mm";
|
||||
const endFormat = start.isSame(end, "day") ? time : `MMM D, YYYY ${time}`;
|
||||
return `${start.format(`MMM D, YYYY ${time}`)} - ${end.format(endFormat)}`;
|
||||
}
|
||||
if (start.isSame(end, "year")) return `${start.format("MMM D")} - ${end.format("MMM D, YYYY")}`;
|
||||
return `${start.format("MMM D, YYYY")} - ${end.format("MMM D, YYYY")}`;
|
||||
}
|
||||
|
||||
export function describeBudgetResetSchedule(
|
||||
budgetDuration: string | null | undefined,
|
||||
|
|
@ -31,26 +76,33 @@ export function describeBudgetResetSchedule(
|
|||
): BudgetResetSchedule {
|
||||
const resetDate = budgetResetAt ? dayjs(budgetResetAt) : null;
|
||||
if (resetDate?.isValid()) {
|
||||
const cadence = budgetDuration ? `every ${budgetDuration}` : "on a schedule";
|
||||
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const label = resetDate.format("MMM D, YYYY HH:mm");
|
||||
const period = budgetDuration
|
||||
? formatBudgetPeriod(getBudgetPeriodStart(budgetDuration, resetDate.utc()).local(), resetDate)
|
||||
: null;
|
||||
const coverage = period
|
||||
? `Current Period Spend covers ${period} and resets every ${budgetDuration}.`
|
||||
: "Current Period Spend resets on a schedule.";
|
||||
return {
|
||||
label,
|
||||
resetAt: resetDate.toISOString(),
|
||||
tooltip: `Current Cycle Spend resets ${cadence}. Next reset ${label} (${timeZone}). ${ROLLOVER_NOTE}`,
|
||||
period,
|
||||
tooltip: `${coverage} Next reset ${label} (${timeZone}). ${ROLLOVER_NOTE}`,
|
||||
};
|
||||
}
|
||||
if (!budgetDuration) {
|
||||
return {
|
||||
label: "Never resets",
|
||||
resetAt: null,
|
||||
tooltip:
|
||||
"This budget has no reset window, so Current Cycle Spend accumulates for as long as the membership exists.",
|
||||
period: null,
|
||||
tooltip: "This budget has no reset window, so this is lifetime spend for as long as the membership exists.",
|
||||
};
|
||||
}
|
||||
return {
|
||||
label: "Unknown",
|
||||
resetAt: null,
|
||||
period: null,
|
||||
tooltip: `Resets every ${budgetDuration}, but the next reset time has not been scheduled yet. ${ROLLOVER_NOTE}`,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue