mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
giveme git commit message 50 char
This commit is contained in:
@@ -31,6 +31,19 @@ export function paymentDrainMs(): number {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ISO timestamp of the end of a pay window's drain tail, for client display
|
||||||
|
* (the "payment processing" countdown). Null in ⇒ null out.
|
||||||
|
*/
|
||||||
|
export function paymentDrainEndsAtIso(
|
||||||
|
deadline: Date | string | null | undefined,
|
||||||
|
): string | null {
|
||||||
|
if (deadline == null) return null;
|
||||||
|
const ms = new Date(deadline).getTime();
|
||||||
|
if (!Number.isFinite(ms)) return null;
|
||||||
|
return new Date(ms + paymentDrainMs()).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A pay window AND its drain tail have closed.
|
* A pay window AND its drain tail have closed.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { Server, Socket } from 'socket.io';
|
|||||||
|
|
||||||
import { WsAuthService } from '../notification-inbox/ws-auth.service';
|
import { WsAuthService } from '../notification-inbox/ws-auth.service';
|
||||||
import { TrainSchedule } from '../train-schedules/entities/train-schedule.entity';
|
import { TrainSchedule } from '../train-schedules/entities/train-schedule.entity';
|
||||||
|
import { paymentDrainEndsAtIso } from './booking-batch.constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Server → client push for booking-window state changes. Same handshake model
|
* Server → client push for booking-window state changes. Same handshake model
|
||||||
@@ -61,6 +62,7 @@ export class BookingWindowGateway implements OnGatewayConnection {
|
|||||||
windowClosesAt: schedule.windowClosesAt?.toISOString() ?? null,
|
windowClosesAt: schedule.windowClosesAt?.toISOString() ?? null,
|
||||||
docReviewEndsAt: schedule.docReviewEndsAt?.toISOString() ?? null,
|
docReviewEndsAt: schedule.docReviewEndsAt?.toISOString() ?? null,
|
||||||
paymentPhaseEndsAt: schedule.paymentPhaseEndsAt?.toISOString() ?? null,
|
paymentPhaseEndsAt: schedule.paymentPhaseEndsAt?.toISOString() ?? null,
|
||||||
|
paymentDrainEndsAt: paymentDrainEndsAtIso(schedule.paymentPhaseEndsAt),
|
||||||
scheduledDepartureDate: schedule.scheduledDepartureDate?.toISOString() ?? null,
|
scheduledDepartureDate: schedule.scheduledDepartureDate?.toISOString() ?? null,
|
||||||
};
|
};
|
||||||
this.server.emit(BOOKING_WINDOW_WS_EVENTS.PHASE, payload);
|
this.server.emit(BOOKING_WINDOW_WS_EVENTS.PHASE, payload);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
DEFAULT_PAYMENT_DRAIN_MINUTES,
|
DEFAULT_PAYMENT_DRAIN_MINUTES,
|
||||||
|
paymentDrainEndsAtIso,
|
||||||
paymentDrainMs,
|
paymentDrainMs,
|
||||||
payWindowLapsed,
|
payWindowLapsed,
|
||||||
} from "./booking-batch.constants";
|
} from "./booking-batch.constants";
|
||||||
@@ -64,4 +65,16 @@ describe("payWindowLapsed — pay-window drain tail", () => {
|
|||||||
expect(paymentDrainMs()).toBe(DEFAULT_PAYMENT_DRAIN_MINUTES * MIN);
|
expect(paymentDrainMs()).toBe(DEFAULT_PAYMENT_DRAIN_MINUTES * MIN);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("paymentDrainEndsAtIso reports deadline + drain, null/garbage-safe", () => {
|
||||||
|
expect(paymentDrainEndsAtIso(deadline)).toBe(
|
||||||
|
new Date(at(DEFAULT_PAYMENT_DRAIN_MINUTES * MIN)).toISOString(),
|
||||||
|
);
|
||||||
|
expect(paymentDrainEndsAtIso(deadline.toISOString())).toBe(
|
||||||
|
new Date(at(DEFAULT_PAYMENT_DRAIN_MINUTES * MIN)).toISOString(),
|
||||||
|
);
|
||||||
|
expect(paymentDrainEndsAtIso(null)).toBeNull();
|
||||||
|
expect(paymentDrainEndsAtIso(undefined)).toBeNull();
|
||||||
|
expect(paymentDrainEndsAtIso("not-a-date")).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ import {
|
|||||||
DEFAULT_CONTAINER_WAGON_CAPACITY_TONS,
|
DEFAULT_CONTAINER_WAGON_CAPACITY_TONS,
|
||||||
DEFAULT_CONTAINER_WAGON_LENGTH_METERS,
|
DEFAULT_CONTAINER_WAGON_LENGTH_METERS,
|
||||||
DEFAULT_CONTAINER_WAGON_TARE_TONS,
|
DEFAULT_CONTAINER_WAGON_TARE_TONS,
|
||||||
|
paymentDrainEndsAtIso,
|
||||||
} from './booking-batch.constants';
|
} from './booking-batch.constants';
|
||||||
import { orderConsistWagons } from './consist-order.util';
|
import { orderConsistWagons } from './consist-order.util';
|
||||||
import {
|
import {
|
||||||
@@ -6940,6 +6941,7 @@ export class TrainSchedulingService {
|
|||||||
windowClosesAt: r.window_closes_at,
|
windowClosesAt: r.window_closes_at,
|
||||||
docReviewEndsAt: r.doc_review_ends_at,
|
docReviewEndsAt: r.doc_review_ends_at,
|
||||||
paymentPhaseEndsAt: r.payment_phase_ends_at,
|
paymentPhaseEndsAt: r.payment_phase_ends_at,
|
||||||
|
paymentDrainEndsAt: paymentDrainEndsAtIso(r.payment_phase_ends_at),
|
||||||
bookingWindowStatus: r.booking_window_status,
|
bookingWindowStatus: r.booking_window_status,
|
||||||
bookingCycleNo: r.booking_cycle_no,
|
bookingCycleNo: r.booking_cycle_no,
|
||||||
departureDate: r.scheduled_departure_date,
|
departureDate: r.scheduled_departure_date,
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ interface WindowRow {
|
|||||||
windowClosesAt: string | null;
|
windowClosesAt: string | null;
|
||||||
docReviewEndsAt: string | null;
|
docReviewEndsAt: string | null;
|
||||||
paymentPhaseEndsAt: string | null;
|
paymentPhaseEndsAt: string | null;
|
||||||
|
/** End of the payment drain tail — pending payments may settle until then. */
|
||||||
|
paymentDrainEndsAt?: string | null;
|
||||||
bookingWindowStatus: string;
|
bookingWindowStatus: string;
|
||||||
bookingCycleNo: number;
|
bookingCycleNo: number;
|
||||||
departureDate: string;
|
departureDate: string;
|
||||||
@@ -94,16 +96,29 @@ const COUNTDOWN_TEXT: Partial<
|
|||||||
PRE_WINDOW: { label: "Opens in", expiredText: "Opening now…" },
|
PRE_WINDOW: { label: "Opens in", expiredText: "Opening now…" },
|
||||||
OPEN: { label: "Closes in", expiredText: "Review starting…" },
|
OPEN: { label: "Closes in", expiredText: "Review starting…" },
|
||||||
DOC_REVIEW: { label: "Doc review ends in", expiredText: "Payment starting…" },
|
DOC_REVIEW: { label: "Doc review ends in", expiredText: "Payment starting…" },
|
||||||
PAYMENT: { label: "Payment ends in", expiredText: "Closing…" },
|
PAYMENT: { label: "Payment ends in", expiredText: "Finalizing…" },
|
||||||
};
|
};
|
||||||
|
|
||||||
function phaseCountdown(
|
function phaseCountdown(w: WindowRow): {
|
||||||
w: WindowRow,
|
label: string;
|
||||||
): { label: string; deadline: string; expiredText: string } | null {
|
deadline: string;
|
||||||
|
expiredText: string;
|
||||||
|
graceDeadline?: string | null;
|
||||||
|
graceLabel?: string;
|
||||||
|
} | null {
|
||||||
const state = bookingWindowUiState(w);
|
const state = bookingWindowUiState(w);
|
||||||
const text = COUNTDOWN_TEXT[state.kind];
|
const text = COUNTDOWN_TEXT[state.kind];
|
||||||
if (!state.countdownTo || !text) return null;
|
if (!state.countdownTo || !text) return null;
|
||||||
return { ...text, deadline: state.countdownTo };
|
// Once the pay deadline lapses, pending payments still settle during the
|
||||||
|
// drain tail — count it down as "processing" instead of a stale "closing".
|
||||||
|
const grace =
|
||||||
|
state.kind === "PAYMENT" && w.paymentDrainEndsAt
|
||||||
|
? {
|
||||||
|
graceDeadline: w.paymentDrainEndsAt,
|
||||||
|
graceLabel: "Processing payments — closes in",
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
return { ...text, deadline: state.countdownTo, ...grace };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Badge label + Mantine color per UI state — same state the countdown uses. */
|
/** Badge label + Mantine color per UI state — same state the countdown uses. */
|
||||||
@@ -225,6 +240,8 @@ function WindowCard({ w }: { w: WindowRow }) {
|
|||||||
deadline={cd.deadline}
|
deadline={cd.deadline}
|
||||||
label={cd.label}
|
label={cd.label}
|
||||||
expiredText={cd.expiredText}
|
expiredText={cd.expiredText}
|
||||||
|
graceDeadline={cd.graceDeadline}
|
||||||
|
graceLabel={cd.graceLabel}
|
||||||
size="xs"
|
size="xs"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ interface WindowRow {
|
|||||||
windowClosesAt: string | null;
|
windowClosesAt: string | null;
|
||||||
docReviewEndsAt: string | null;
|
docReviewEndsAt: string | null;
|
||||||
paymentPhaseEndsAt: string | null;
|
paymentPhaseEndsAt: string | null;
|
||||||
|
paymentDrainEndsAt?: string | null;
|
||||||
bookingWindowStatus: string;
|
bookingWindowStatus: string;
|
||||||
bookingCycleNo: number;
|
bookingCycleNo: number;
|
||||||
departureDate: string;
|
departureDate: string;
|
||||||
@@ -55,6 +56,7 @@ function applyEvent<T extends WindowRow>(row: T, event: BookingWindowPhaseEvent)
|
|||||||
windowClosesAt: event.windowClosesAt,
|
windowClosesAt: event.windowClosesAt,
|
||||||
docReviewEndsAt: event.docReviewEndsAt,
|
docReviewEndsAt: event.docReviewEndsAt,
|
||||||
paymentPhaseEndsAt: event.paymentPhaseEndsAt,
|
paymentPhaseEndsAt: event.paymentPhaseEndsAt,
|
||||||
|
paymentDrainEndsAt: event.paymentDrainEndsAt,
|
||||||
departureDate: event.scheduledDepartureDate ?? row.departureDate,
|
departureDate: event.scheduledDepartureDate ?? row.departureDate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ function applyEvent(
|
|||||||
windowClosesAt: event.windowClosesAt,
|
windowClosesAt: event.windowClosesAt,
|
||||||
docReviewEndsAt: event.docReviewEndsAt,
|
docReviewEndsAt: event.docReviewEndsAt,
|
||||||
paymentPhaseEndsAt: event.paymentPhaseEndsAt,
|
paymentPhaseEndsAt: event.paymentPhaseEndsAt,
|
||||||
|
paymentDrainEndsAt: event.paymentDrainEndsAt,
|
||||||
departureDate: event.scheduledDepartureDate ?? row.departureDate,
|
departureDate: event.scheduledDepartureDate ?? row.departureDate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,16 +68,29 @@ const COUNTDOWN_TEXT: Partial<
|
|||||||
PRE_WINDOW: { label: "Booking opens in", expiredText: "Booking opening now…" },
|
PRE_WINDOW: { label: "Booking opens in", expiredText: "Booking opening now…" },
|
||||||
OPEN: { label: "Window closes in", expiredText: "Document review starting…" },
|
OPEN: { label: "Window closes in", expiredText: "Document review starting…" },
|
||||||
DOC_REVIEW: { label: "Document review ends in", expiredText: "Payment starting…" },
|
DOC_REVIEW: { label: "Document review ends in", expiredText: "Payment starting…" },
|
||||||
PAYMENT: { label: "Payment due in", expiredText: "Payment window closing…" },
|
PAYMENT: { label: "Payment due in", expiredText: "Finalizing payments…" },
|
||||||
};
|
};
|
||||||
|
|
||||||
function phaseCountdown(
|
function phaseCountdown(w: MyBookingWindow): {
|
||||||
w: MyBookingWindow,
|
label: string;
|
||||||
): { label: string; deadline: string; expiredText: string } | null {
|
deadline: string;
|
||||||
|
expiredText: string;
|
||||||
|
graceDeadline?: string | null;
|
||||||
|
graceLabel?: string;
|
||||||
|
} | null {
|
||||||
const state = bookingWindowUiState(w);
|
const state = bookingWindowUiState(w);
|
||||||
const text = COUNTDOWN_TEXT[state.kind];
|
const text = COUNTDOWN_TEXT[state.kind];
|
||||||
if (!state.countdownTo || !text) return null;
|
if (!state.countdownTo || !text) return null;
|
||||||
return { ...text, deadline: state.countdownTo };
|
// Once the pay deadline lapses, pending payments still settle during the
|
||||||
|
// drain tail — count it down as "processing" instead of a stale "closing".
|
||||||
|
const grace =
|
||||||
|
state.kind === "PAYMENT" && w.paymentDrainEndsAt
|
||||||
|
? {
|
||||||
|
graceDeadline: w.paymentDrainEndsAt,
|
||||||
|
graceLabel: "Payment processing — closes in",
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
return { ...text, deadline: state.countdownTo, ...grace };
|
||||||
}
|
}
|
||||||
|
|
||||||
function Pill({
|
function Pill({
|
||||||
@@ -336,6 +349,8 @@ export const UpcomingWindowsSection = memo(function UpcomingWindowsSection({
|
|||||||
deadline={cd.deadline}
|
deadline={cd.deadline}
|
||||||
label={cd.label}
|
label={cd.label}
|
||||||
expiredText={cd.expiredText}
|
expiredText={cd.expiredText}
|
||||||
|
graceDeadline={cd.graceDeadline}
|
||||||
|
graceLabel={cd.graceLabel}
|
||||||
size="xs"
|
size="xs"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -104,16 +104,29 @@ const COUNTDOWN_TEXT: Partial<
|
|||||||
PRE_WINDOW: { label: "Booking opens in", expiredText: "Booking opening now…" },
|
PRE_WINDOW: { label: "Booking opens in", expiredText: "Booking opening now…" },
|
||||||
OPEN: { label: "Window closes in", expiredText: "Document review starting…" },
|
OPEN: { label: "Window closes in", expiredText: "Document review starting…" },
|
||||||
DOC_REVIEW: { label: "Document review ends in", expiredText: "Payment starting…" },
|
DOC_REVIEW: { label: "Document review ends in", expiredText: "Payment starting…" },
|
||||||
PAYMENT: { label: "Payment due in", expiredText: "Payment window closing…" },
|
PAYMENT: { label: "Payment due in", expiredText: "Finalizing payments…" },
|
||||||
};
|
};
|
||||||
|
|
||||||
function phaseCountdown(
|
function phaseCountdown(w: MyBookingWindow): {
|
||||||
w: MyBookingWindow,
|
label: string;
|
||||||
): { label: string; deadline: string; expiredText: string } | null {
|
deadline: string;
|
||||||
|
expiredText: string;
|
||||||
|
graceDeadline?: string | null;
|
||||||
|
graceLabel?: string;
|
||||||
|
} | null {
|
||||||
const state = bookingWindowUiState(w);
|
const state = bookingWindowUiState(w);
|
||||||
const text = COUNTDOWN_TEXT[state.kind];
|
const text = COUNTDOWN_TEXT[state.kind];
|
||||||
if (!state.countdownTo || !text) return null;
|
if (!state.countdownTo || !text) return null;
|
||||||
return { ...text, deadline: state.countdownTo };
|
// Once the pay deadline lapses, pending payments still settle during the
|
||||||
|
// drain tail — count it down as "processing" instead of a stale "closing".
|
||||||
|
const grace =
|
||||||
|
state.kind === "PAYMENT" && w.paymentDrainEndsAt
|
||||||
|
? {
|
||||||
|
graceDeadline: w.paymentDrainEndsAt,
|
||||||
|
graceLabel: "Payment processing — closes in",
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
return { ...text, deadline: state.countdownTo, ...grace };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -226,6 +239,8 @@ function WindowCard({ w }: { w: MyBookingWindow }) {
|
|||||||
deadline={cd.deadline}
|
deadline={cd.deadline}
|
||||||
label={cd.label}
|
label={cd.label}
|
||||||
expiredText={cd.expiredText}
|
expiredText={cd.expiredText}
|
||||||
|
graceDeadline={cd.graceDeadline}
|
||||||
|
graceLabel={cd.graceLabel}
|
||||||
size="xs"
|
size="xs"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ export interface MyBookingWindow {
|
|||||||
windowClosesAt: string | null;
|
windowClosesAt: string | null;
|
||||||
docReviewEndsAt: string | null;
|
docReviewEndsAt: string | null;
|
||||||
paymentPhaseEndsAt: string | null;
|
paymentPhaseEndsAt: string | null;
|
||||||
|
/** End of the payment drain tail — pending payments may settle until then. */
|
||||||
|
paymentDrainEndsAt: string | null;
|
||||||
bookingWindowStatus: string;
|
bookingWindowStatus: string;
|
||||||
bookingCycleNo: number;
|
bookingCycleNo: number;
|
||||||
departureDate: string;
|
departureDate: string;
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ export interface BookingWindowPhaseEvent {
|
|||||||
windowClosesAt: string | null;
|
windowClosesAt: string | null;
|
||||||
docReviewEndsAt: string | null;
|
docReviewEndsAt: string | null;
|
||||||
paymentPhaseEndsAt: string | null;
|
paymentPhaseEndsAt: string | null;
|
||||||
|
/**
|
||||||
|
* When the payment drain tail ends (paymentPhaseEndsAt + server drain
|
||||||
|
* minutes) — pending payments may still settle until then. Display only.
|
||||||
|
*/
|
||||||
|
paymentDrainEndsAt: string | null;
|
||||||
scheduledDepartureDate: string | null;
|
scheduledDepartureDate: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Group, Text } from "@mantine/core";
|
import { Group, Loader, Text } from "@mantine/core";
|
||||||
import { Clock } from "lucide-react";
|
import { Clock } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
@@ -9,6 +9,14 @@ export interface CountdownTimerProps {
|
|||||||
label?: string;
|
label?: string;
|
||||||
/** Text shown once the deadline has passed. */
|
/** Text shown once the deadline has passed. */
|
||||||
expiredText?: string;
|
expiredText?: string;
|
||||||
|
/**
|
||||||
|
* Optional grace stage: once `deadline` passes, count down to this later
|
||||||
|
* ISO timestamp instead (e.g. the payment drain tail). `expiredText` then
|
||||||
|
* only shows after the grace deadline has also passed.
|
||||||
|
*/
|
||||||
|
graceDeadline?: string | null;
|
||||||
|
/** Label shown while counting down the grace stage (e.g. "Payment processing — closes in"). */
|
||||||
|
graceLabel?: string;
|
||||||
/** Visual size of the time text. */
|
/** Visual size of the time text. */
|
||||||
size?: "xs" | "sm" | "md" | "lg";
|
size?: "xs" | "sm" | "md" | "lg";
|
||||||
/** Colour once under this many seconds remain (urgency). Default 300 (5 min). */
|
/** Colour once under this many seconds remain (urgency). Default 300 (5 min). */
|
||||||
@@ -35,37 +43,56 @@ function formatRemaining(ms: number): string {
|
|||||||
/**
|
/**
|
||||||
* Live countdown to an ISO deadline. Ticks once a second, shows the remaining
|
* Live countdown to an ISO deadline. Ticks once a second, shows the remaining
|
||||||
* time (d/h/m/s), turns red when under `urgentUnderSeconds`, and shows
|
* time (d/h/m/s), turns red when under `urgentUnderSeconds`, and shows
|
||||||
* `expiredText` once the deadline is in the past. Display only — enforcement
|
* `expiredText` once the deadline is in the past. When `graceDeadline` is set,
|
||||||
* lives server-side.
|
* a lapsed main deadline rolls into a calm second-stage countdown (spinner,
|
||||||
|
* no urgency colours) toward it before `expiredText` takes over. Display only
|
||||||
|
* — enforcement lives server-side.
|
||||||
*/
|
*/
|
||||||
export function CountdownTimer({
|
export function CountdownTimer({
|
||||||
deadline,
|
deadline,
|
||||||
label,
|
label,
|
||||||
expiredText = "Expired",
|
expiredText = "Expired",
|
||||||
|
graceDeadline,
|
||||||
|
graceLabel,
|
||||||
size = "sm",
|
size = "sm",
|
||||||
urgentUnderSeconds = 300,
|
urgentUnderSeconds = 300,
|
||||||
}: CountdownTimerProps) {
|
}: CountdownTimerProps) {
|
||||||
const [remaining, setRemaining] = useState<number | null>(() =>
|
const [now, setNow] = useState(() => Date.now());
|
||||||
deadline ? new Date(deadline).getTime() - Date.now() : null,
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!deadline) {
|
if (!deadline) return;
|
||||||
setRemaining(null);
|
setNow(Date.now());
|
||||||
return;
|
const id = setInterval(() => setNow(Date.now()), 1000);
|
||||||
}
|
|
||||||
const target = new Date(deadline).getTime();
|
|
||||||
const tick = () => setRemaining(target - Date.now());
|
|
||||||
tick();
|
|
||||||
const id = setInterval(tick, 1000);
|
|
||||||
return () => clearInterval(id);
|
return () => clearInterval(id);
|
||||||
}, [deadline]);
|
}, [deadline, graceDeadline]);
|
||||||
|
|
||||||
if (!deadline || remaining == null || Number.isNaN(remaining)) {
|
if (!deadline) return null;
|
||||||
return null;
|
const target = new Date(deadline).getTime();
|
||||||
|
if (Number.isNaN(target)) return null;
|
||||||
|
|
||||||
|
const remaining = target - now;
|
||||||
|
const expired = remaining <= 0;
|
||||||
|
|
||||||
|
const graceTarget = graceDeadline ? new Date(graceDeadline).getTime() : NaN;
|
||||||
|
const graceRemaining = Number.isNaN(graceTarget) ? 0 : graceTarget - now;
|
||||||
|
const inGrace = expired && graceRemaining > 0;
|
||||||
|
|
||||||
|
if (inGrace) {
|
||||||
|
return (
|
||||||
|
<Group gap={6} align="center" wrap="nowrap">
|
||||||
|
<Loader size={size === "lg" ? 18 : 14} color="blue.7" />
|
||||||
|
{graceLabel && (
|
||||||
|
<Text size={size} c="dimmed">
|
||||||
|
{graceLabel}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<Text size={size} fw={600} c="blue.7">
|
||||||
|
{formatRemaining(graceRemaining)}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const expired = remaining <= 0;
|
|
||||||
const urgent = !expired && remaining <= urgentUnderSeconds * 1000;
|
const urgent = !expired && remaining <= urgentUnderSeconds * 1000;
|
||||||
const color = expired ? "red.7" : urgent ? "orange.7" : "dimmed";
|
const color = expired ? "red.7" : urgent ? "orange.7" : "dimmed";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user