mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
backoffice
This commit is contained in:
@@ -26,6 +26,7 @@ export * from "./lib/input/CountrySelect";
|
||||
export * from "./lib/input/PhoneInput";
|
||||
export * from "./lib/input/phone";
|
||||
export * from "./lib/data/AdvancedTable";
|
||||
export * from "./lib/data/WaitingFor";
|
||||
export * from "./lib/feedback/use-error-handler";
|
||||
export * from "./lib/data/useServerTable";
|
||||
export * from "./lib/landing/LandingPage";
|
||||
|
||||
83
libs/ui/src/lib/data/WaitingFor.tsx
Normal file
83
libs/ui/src/lib/data/WaitingFor.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Text, Tooltip } from '@mantine/core';
|
||||
import { statusToneVars } from '@ema-platform/shared';
|
||||
|
||||
export interface WaitingForProps {
|
||||
/** When the clock started — normally the moment the applicant submitted. */
|
||||
since: string | null | undefined;
|
||||
/**
|
||||
* Days after which the wait reads as overdue. Amber at half of it, red past
|
||||
* it. Defaults to 14, which is the shortest SLA any configured licence type
|
||||
* currently uses; pass the real one where a queue knows it.
|
||||
*/
|
||||
slaDays?: number;
|
||||
/** Set once the item is decided — a closed item is not waiting for anyone. */
|
||||
done?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fraction of the target elapsed before the wait reads as at-risk. Matches the
|
||||
* licence queue's own `WARNING_RATIO`, so a queue using this component and one
|
||||
* using `computeSla` turn amber at the same point rather than disagreeing.
|
||||
*/
|
||||
const WARNING_RATIO = 0.7;
|
||||
|
||||
/** Whole days between `since` and now, floored. Negative clock skew reads as 0. */
|
||||
function daysSince(since: string): number {
|
||||
const ms = Date.now() - new Date(since).getTime();
|
||||
return Math.max(0, Math.floor(ms / 86_400_000));
|
||||
}
|
||||
|
||||
/**
|
||||
* How long an item has been sitting in a queue.
|
||||
*
|
||||
* No review queue showed this. An officer opening a list of thirty
|
||||
* registrations could see what each one *was*, but not which had been waiting
|
||||
* three days and which had been waiting three weeks — so the queue was worked
|
||||
* top-down by whatever the sort happened to be rather than by urgency.
|
||||
*
|
||||
* Colour comes from the platform's status tones rather than its own scale, so
|
||||
* "overdue" here is the same red as "rejected" everywhere else.
|
||||
*/
|
||||
export function WaitingFor({ since, slaDays = 14, done }: WaitingForProps) {
|
||||
if (!since) {
|
||||
return (
|
||||
<Text size="sm" c="dimmed">
|
||||
—
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
const days = daysSince(since);
|
||||
|
||||
// A decided item keeps its elapsed time visible — useful when reviewing how
|
||||
// long something took — but never coloured, because nothing is pending.
|
||||
const tone = done
|
||||
? 'neutral'
|
||||
: days >= slaDays
|
||||
? 'danger'
|
||||
: days >= slaDays * WARNING_RATIO
|
||||
? 'pending'
|
||||
: 'neutral';
|
||||
|
||||
const label = days === 0 ? 'today' : `${days}d`;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
label={
|
||||
done
|
||||
? `Took ${days} day${days === 1 ? '' : 's'}`
|
||||
: `Waiting ${days} day${days === 1 ? '' : 's'} · ${slaDays}-day target`
|
||||
}
|
||||
withArrow
|
||||
>
|
||||
<Text
|
||||
size="sm"
|
||||
fw={tone === 'neutral' ? 400 : 600}
|
||||
c={tone === 'neutral' ? 'dimmed' : undefined}
|
||||
style={tone === 'neutral' ? undefined : { color: statusToneVars(tone).color }}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -2,17 +2,54 @@ import { Group, Stack, Text, Title } from '@mantine/core';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface PageHeaderProps {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
/**
|
||||
* The page's name. A `ReactNode` rather than a string because detail pages
|
||||
* title themselves with the record they are showing, which is often a
|
||||
* localised or composed value rather than a literal.
|
||||
*/
|
||||
title: ReactNode;
|
||||
subtitle?: ReactNode;
|
||||
/**
|
||||
* Identifiers that belong beside the title rather than under it — a
|
||||
* reference number, a status badge. Rendered inline, small and dimmed.
|
||||
*/
|
||||
meta?: ReactNode;
|
||||
/** Right-aligned actions (e.g. a primary button). */
|
||||
action?: ReactNode;
|
||||
/**
|
||||
* Set when the header sits directly inside a gapped `<Stack>`, which already
|
||||
* spaces it from what follows — the built-in margin would double that gap.
|
||||
*/
|
||||
noMargin?: boolean;
|
||||
}
|
||||
|
||||
export function PageHeader({ title, subtitle, action }: PageHeaderProps) {
|
||||
/**
|
||||
* The heading every backoffice page starts with.
|
||||
*
|
||||
* It exists because 25 pages each rolled their own: eight at `order={2}`,
|
||||
* seventeen at `order={3}`, with `mb="xs"`, `mb={4}`, `mb="lg"` and nothing at
|
||||
* all between them. Clicking between two screens moved the title, which is the
|
||||
* single loudest signal that a product was assembled rather than designed.
|
||||
*
|
||||
* `order={2}` is deliberate — it is the page's only `h2`, sitting under the
|
||||
* app-level `h1`, so the heading outline reads correctly for a screen reader.
|
||||
* Section headings inside cards stay at `order={4}`/`{5}` and are not this
|
||||
* component's business.
|
||||
*/
|
||||
export function PageHeader({ title, subtitle, meta, action, noMargin }: PageHeaderProps) {
|
||||
return (
|
||||
<Group justify="space-between" align="flex-end" wrap="wrap" gap="sm">
|
||||
<Group
|
||||
justify="space-between"
|
||||
align="flex-end"
|
||||
wrap="wrap"
|
||||
gap="sm"
|
||||
mb={noMargin ? undefined : 'lg'}
|
||||
>
|
||||
<Stack gap={2}>
|
||||
<Title order={2}>{title}</Title>
|
||||
<Group gap="sm" align="center" wrap="wrap">
|
||||
<Title order={2}>{title}</Title>
|
||||
{meta}
|
||||
</Group>
|
||||
{subtitle && (
|
||||
<Text c="dimmed" size="sm">
|
||||
{subtitle}
|
||||
|
||||
Reference in New Issue
Block a user