mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 05:43:12 +00:00
223 lines
6.8 KiB
TypeScript
223 lines
6.8 KiB
TypeScript
import { useState } from "react";
|
|
import { Alert, Box, Chip, Stack, Tab, Tabs, TablePagination } from "@mui/material";
|
|
import {
|
|
UPLIFT_APPROVAL_TABS,
|
|
isUpliftApprovalTab,
|
|
type UpliftApprovalTab,
|
|
} from "@/app/(protected)/uplifts/_components/uplift-approval-tabs";
|
|
import { UpliftApprovalsTable } from "@/app/(protected)/uplifts/_components/uplift-approvals-table";
|
|
import { UpliftDecisionDialogs } from "@/app/(protected)/uplifts/_components/uplift-decision-dialogs";
|
|
import { UpliftDetailModal } from "@/app/(protected)/uplifts/_components/uplift-detail-modal";
|
|
import {
|
|
openUpliftEvidence,
|
|
openUpliftEvidenceInNewTab,
|
|
} from "@/app/(protected)/uplifts/_components/open-uplift-evidence";
|
|
import { formatUpliftMoney } from "@/app/(protected)/uplifts/_components/format-uplift-money";
|
|
import { isAdminUser } from "@/lib/auth/user-utils";
|
|
import { useAuthContext } from "@/providers/auth-context";
|
|
import { useUpliftApprovalController } from "@/domain/uplifts/use-cases/use-uplift-approval-controller";
|
|
import { useUpliftsQueue } from "@/domain/uplifts/use-cases/use-uplifts-queue";
|
|
import { Text } from "@/components/ui/text";
|
|
import type { UpliftsQueueResult } from "@/domain/uplifts/types/uplift";
|
|
|
|
const PAGE_SIZE = 25;
|
|
|
|
function UpliftApprovalsHeader({ pendingExposureTotal }: { pendingExposureTotal: number | null }) {
|
|
return (
|
|
<Box>
|
|
<Text variant="title" as="h1">
|
|
Uplift Approvals
|
|
</Text>
|
|
<Text variant="description" tone="muted">
|
|
Requests to increase the approved spend on a work order, above its remaining auto-approval
|
|
allowance.
|
|
</Text>
|
|
<Text when={pendingExposureTotal != null} variant="caption" tone="muted">
|
|
Pending exposure:{" "}
|
|
{pendingExposureTotal && pendingExposureTotal > 0
|
|
? formatUpliftMoney(pendingExposureTotal)
|
|
: "—"}
|
|
</Text>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
const TAB_LABELS: Record<UpliftApprovalTab, string> = {
|
|
pending: "Pending",
|
|
approved: "Approved",
|
|
rejected: "Rejected",
|
|
};
|
|
|
|
// A tab's total is only known once its queue has loaded at least once; until then the
|
|
// pill and pager must not claim a count (a loading or failed queue is not an empty one).
|
|
// A failed refetch keeps the last loaded page, so its total stays usable.
|
|
function knownTotal(query: { data?: UpliftsQueueResult | undefined }): number | null {
|
|
return query.data ? query.data.totalCount : null;
|
|
}
|
|
|
|
function TabCountPill({ tab, count }: { tab: UpliftApprovalTab; count: number | null }) {
|
|
return (
|
|
<Chip
|
|
size="small"
|
|
label={count ?? "—"}
|
|
aria-label={count == null ? `${tab} uplift count unavailable` : `${count} ${tab} uplifts`}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function UpliftApprovalsTabs({
|
|
tab,
|
|
counts,
|
|
onChange,
|
|
}: {
|
|
tab: UpliftApprovalTab;
|
|
counts: Record<UpliftApprovalTab, number | null>;
|
|
onChange: (_event: unknown, value: string) => void;
|
|
}) {
|
|
return (
|
|
<Tabs value={tab} onChange={onChange}>
|
|
{UPLIFT_APPROVAL_TABS.map((value) => (
|
|
<Tab
|
|
key={value}
|
|
value={value}
|
|
label={
|
|
<Stack direction="row" spacing={1} sx={{ alignItems: "center" }}>
|
|
<span>{TAB_LABELS[value]}</span>
|
|
<TabCountPill tab={value} count={counts[value]} />
|
|
</Stack>
|
|
}
|
|
/>
|
|
))}
|
|
</Tabs>
|
|
);
|
|
}
|
|
|
|
function UpliftApprovalsPagination({
|
|
count,
|
|
page,
|
|
onPageChange,
|
|
}: {
|
|
count: number;
|
|
page: number;
|
|
onPageChange: (page: number) => void;
|
|
}) {
|
|
return (
|
|
<TablePagination
|
|
component="div"
|
|
count={count}
|
|
page={page}
|
|
onPageChange={(_event, nextPage) => onPageChange(nextPage)}
|
|
rowsPerPage={PAGE_SIZE}
|
|
rowsPerPageOptions={[PAGE_SIZE]}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function UpliftQueueError({ error }: { error: unknown }) {
|
|
if (!error) return null;
|
|
return (
|
|
<Alert severity="error">
|
|
{error instanceof Error ? error.message : "Failed to load uplift queue"}
|
|
</Alert>
|
|
);
|
|
}
|
|
|
|
export default function UpliftQueuePage() {
|
|
const [tab, setTab] = useState<UpliftApprovalTab>("pending");
|
|
const [pages, setPages] = useState<Record<UpliftApprovalTab, number>>({
|
|
pending: 0,
|
|
approved: 0,
|
|
rejected: 0,
|
|
});
|
|
|
|
const { user } = useAuthContext();
|
|
const canRevokeUplifts = isAdminUser(user?.userRoles);
|
|
|
|
const pendingQuery = useUpliftsQueue({
|
|
status: "Pending",
|
|
page: pages.pending + 1,
|
|
pageSize: PAGE_SIZE,
|
|
});
|
|
const approvedQuery = useUpliftsQueue({
|
|
status: "Approved",
|
|
page: pages.approved + 1,
|
|
pageSize: PAGE_SIZE,
|
|
});
|
|
const rejectedQuery = useUpliftsQueue({
|
|
status: "Rejected",
|
|
page: pages.rejected + 1,
|
|
pageSize: PAGE_SIZE,
|
|
});
|
|
const queries = { pending: pendingQuery, approved: approvedQuery, rejected: rejectedQuery };
|
|
|
|
const controller = useUpliftApprovalController();
|
|
const {
|
|
detailItem,
|
|
approvePending,
|
|
rejectPending,
|
|
revokePending,
|
|
handleApprove,
|
|
handleRejectRequest,
|
|
handleRevokeRequest,
|
|
closeDetail,
|
|
openDetail,
|
|
} = controller;
|
|
|
|
const activeQuery = queries[tab];
|
|
const rows = activeQuery.data?.items ?? [];
|
|
const counts = {
|
|
pending: knownTotal(pendingQuery),
|
|
approved: knownTotal(approvedQuery),
|
|
rejected: knownTotal(rejectedQuery),
|
|
};
|
|
const activeTotal = counts[tab];
|
|
const pendingExposureTotal = pendingQuery.data?.pendingExposureTotal ?? null;
|
|
|
|
const handleTabChange = (_event: unknown, value: string) => {
|
|
if (isUpliftApprovalTab(value)) {
|
|
setTab(value);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Box className="flex flex-col gap-4 p-4">
|
|
<UpliftApprovalsHeader pendingExposureTotal={pendingExposureTotal} />
|
|
<UpliftApprovalsTabs tab={tab} counts={counts} onChange={handleTabChange} />
|
|
<UpliftQueueError error={activeQuery.error} />
|
|
<UpliftApprovalsTable
|
|
tab={tab}
|
|
isLoading={activeQuery.isLoading}
|
|
hasError={Boolean(activeQuery.error)}
|
|
rows={rows}
|
|
canRevoke={canRevokeUplifts}
|
|
onOpenDetail={openDetail}
|
|
onApprove={handleApprove}
|
|
onReject={handleRejectRequest}
|
|
onRevoke={handleRevokeRequest}
|
|
onOpenAttachment={openUpliftEvidence}
|
|
isDecisionPending={approvePending || rejectPending || revokePending}
|
|
/>
|
|
{activeTotal != null && (
|
|
<UpliftApprovalsPagination
|
|
count={activeTotal}
|
|
page={pages[tab]}
|
|
onPageChange={(page) => setPages((current) => ({ ...current, [tab]: page }))}
|
|
/>
|
|
)}
|
|
{detailItem != null && (
|
|
<UpliftDetailModal
|
|
item={detailItem}
|
|
canRevoke={canRevokeUplifts}
|
|
approvePending={approvePending}
|
|
revokePending={revokePending}
|
|
onClose={closeDetail}
|
|
onApprove={handleApprove}
|
|
onReject={handleRejectRequest}
|
|
onRevoke={handleRevokeRequest}
|
|
onOpenAttachment={openUpliftEvidenceInNewTab}
|
|
/>
|
|
)}
|
|
<UpliftDecisionDialogs controller={controller} />
|
|
</Box>
|
|
);
|
|
}
|