Files
govoplan-cases/webui/src/features/cases/CasesPage.tsx
T

154 lines
5.6 KiB
TypeScript

import { Search } from "lucide-react";
import { useEffect, useMemo, useState, type FormEvent } from "react";
import { ActionToolbar,
Button,
DocumentationHelpLink,
DismissibleAlert,
FilterBar,
LoadingIndicator,
PageScrollViewport,
SelectionList,
SelectionListItem,
SelectionListItemContent,
StatePanel,
StatusBadge,
i18nMessage,
useGuardedNavigate,
WorkspaceFrame,
type PlatformRouteContext
} from "@govoplan/core-webui";
import {
listCaseCatalog,
listCases,
type CaseCatalog,
type CaseRecord
} from "../../api/cases";
import { CASES_DOCUMENTATION, CASES_I18N } from "./interfacePatterns";
export default function CasesPage({ settings }: PlatformRouteContext) {
const navigate = useGuardedNavigate();
const [query, setQuery] = useState("");
const [submittedQuery, setSubmittedQuery] = useState("");
const [status, setStatus] = useState("");
const [cases, setCases] = useState<CaseRecord[]>([]);
const [catalog, setCatalog] = useState<CaseCatalog>({ statuses: [], types: [] });
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
useEffect(() => {
const controller = new AbortController();
setLoading(true);
setError("");
Promise.all([
listCases(settings, {
query: submittedQuery,
statuses: status ? [status] : undefined,
limit: 200
}, controller.signal),
listCaseCatalog(settings, controller.signal)
]).
then(([result, nextCatalog]) => {
setCases(result.cases);
setTotal(result.total);
setCatalog(nextCatalog);
}).
catch((reason) => {
if ((reason as Error).name !== "AbortError") {
setError(reason instanceof Error ? reason.message : "Cases could not be loaded.");
}
}).
finally(() => setLoading(false));
return () => controller.abort();
}, [settings, status, submittedQuery]);
const statusLabels = useMemo(
() => new Map(catalog.statuses.map((item) => [item.status_key, item])),
[catalog.statuses]
);
const typeLabels = useMemo(
() => new Map(catalog.types.map((item) => [item.type_key, item.label])),
[catalog.types]
);
function submit(event: FormEvent) {
event.preventDefault();
setSubmittedQuery(query.trim());
}
return (
<main className="cases-page">
<WorkspaceFrame className="cases-shell" label="Cases" interfaceId="cases.catalogue" helpContextId="cases.page.catalogue" helpModuleId="cases">
<ActionToolbar surface="panel-header" className="cases-toolbar">
<FilterBar as="form" surface="control" wrap="never" width="default" className="cases-search" onSubmit={submit}>
<Search size={17} aria-hidden="true" />
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
aria-label="Search cases"
placeholder="Search cases"
/>
<Button
type="submit"
variant="primary"
disabledReason={loading ? CASES_I18N.loading : undefined}
>
Search
</Button>
</FilterBar>
<label className="cases-status-filter">
<span>Status</span>
<select value={status} onChange={(event) => setStatus(event.target.value)}>
<option value="">All statuses</option>
{catalog.statuses.map((item) =>
<option key={item.status_key} value={item.status_key}>{item.label}</option>
)}
</select>
</label>
<span className="cases-count">{i18nMessage("i18n:govoplan-cases.case_count", { value0: total })}</span>
<DocumentationHelpLink reference={CASES_DOCUMENTATION} />
</ActionToolbar>
<PageScrollViewport className="cases-list-viewport">
{error &&
<DismissibleAlert tone="error" onDismiss={() => setError("")}>
{error}
</DismissibleAlert>
}
{loading && <LoadingIndicator label="Loading cases" />}
{!loading && !error && cases.length === 0 &&
<StatePanel size="compact" description="No matching cases." />
}
{!loading && cases.length > 0 &&
<SelectionList variant="navigation" label="Cases">
{cases.map((item) => {
const statusDefinition = statusLabels.get(item.status_key);
return (
<SelectionListItem
selected={false}
key={item.reference.object_id}
onClick={() => navigate(`/cases/${encodeURIComponent(item.reference.object_id)}`)}>
<SelectionListItemContent title={item.title} description={`${item.case_number} · ${typeLabels.get(item.case_type_key) ?? humanize(item.case_type_key)} · ${formatDate(item.deadline_at)}`} />
<StatusBadge
status={statusDefinition?.terminal ? "inactive" : "active"}
label={statusDefinition?.label ?? humanize(item.status_key)}
/>
</SelectionListItem>
);
})}
</SelectionList>
}
</PageScrollViewport>
</WorkspaceFrame>
</main>
);
}
function formatDate(value?: string | null): string {
return value ? new Intl.DateTimeFormat(undefined, { dateStyle: "medium" }).format(new Date(value)) : "No deadline";
}
function humanize(value: string): string {
return value.replace(/[_:.\-]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
}