mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 08:03:13 +00:00
Merge pull request #279 from Sea-Haven-Industries/feat/ab/sh-389-rejected-uplift-queue
feat(uplifts): add the Rejected archive tab to Uplift Approvals (SH-389)
This commit is contained in:
commit
85a45e9fc0
9 changed files with 519 additions and 118 deletions
|
|
@ -0,0 +1,52 @@
|
||||||
|
export const UPLIFT_APPROVAL_TABS = ["pending", "approved", "rejected"] as const;
|
||||||
|
export type UpliftApprovalTab = (typeof UPLIFT_APPROVAL_TABS)[number];
|
||||||
|
|
||||||
|
export function isUpliftApprovalTab(value: string): value is UpliftApprovalTab {
|
||||||
|
return (UPLIFT_APPROVAL_TABS as readonly string[]).includes(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const PENDING_HEADER_CELLS = [
|
||||||
|
"Work Order",
|
||||||
|
"Service / Vendor",
|
||||||
|
"Amount",
|
||||||
|
"Approved on WO",
|
||||||
|
"Notes",
|
||||||
|
"Attachments",
|
||||||
|
"Requested By",
|
||||||
|
"Waiting",
|
||||||
|
"Actions",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const APPROVED_HEADER_CELLS = [
|
||||||
|
"Work Order",
|
||||||
|
"Service / Vendor",
|
||||||
|
"Amount",
|
||||||
|
"Approved on WO",
|
||||||
|
"Notes",
|
||||||
|
"Attachments",
|
||||||
|
"Requested By",
|
||||||
|
"Approved By",
|
||||||
|
"Approved At",
|
||||||
|
"Actions",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
// A rejection is terminal, so the archive ends with the decision and its reason
|
||||||
|
// instead of an Actions column.
|
||||||
|
const REJECTED_HEADER_CELLS = [
|
||||||
|
"Work Order",
|
||||||
|
"Service / Vendor",
|
||||||
|
"Amount",
|
||||||
|
"Approved on WO",
|
||||||
|
"Notes",
|
||||||
|
"Attachments",
|
||||||
|
"Requested By",
|
||||||
|
"Rejected By",
|
||||||
|
"Rejected At",
|
||||||
|
"Rejection Reason",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const UPLIFT_APPROVAL_HEADER_CELLS: Record<UpliftApprovalTab, readonly string[]> = {
|
||||||
|
pending: PENDING_HEADER_CELLS,
|
||||||
|
approved: APPROVED_HEADER_CELLS,
|
||||||
|
rejected: REJECTED_HEADER_CELLS,
|
||||||
|
};
|
||||||
|
|
@ -16,14 +16,20 @@ import {
|
||||||
formatUpliftMoney,
|
formatUpliftMoney,
|
||||||
isGrantedAmount,
|
isGrantedAmount,
|
||||||
} from "@/app/(protected)/uplifts/_components/format-uplift-money";
|
} from "@/app/(protected)/uplifts/_components/format-uplift-money";
|
||||||
|
import {
|
||||||
|
UPLIFT_APPROVAL_HEADER_CELLS,
|
||||||
|
type UpliftApprovalTab,
|
||||||
|
} from "@/app/(protected)/uplifts/_components/uplift-approval-tabs";
|
||||||
|
import {
|
||||||
|
RejectedEmptyState,
|
||||||
|
RejectedRowCells,
|
||||||
|
} from "@/app/(protected)/uplifts/_components/uplift-rejected-cells";
|
||||||
import { Text } from "@/components/ui/text";
|
import { Text } from "@/components/ui/text";
|
||||||
import { useUpliftsCanApprove } from "@/domain/uplifts/use-cases/use-uplifts-can-approve";
|
import { useUpliftsCanApprove } from "@/domain/uplifts/use-cases/use-uplifts-can-approve";
|
||||||
import { upliftAmount } from "@/domain/uplifts/utils/uplift-amount";
|
import { upliftAmount } from "@/domain/uplifts/utils/uplift-amount";
|
||||||
import type { UpliftQueueItem } from "@/domain/uplifts/types/uplift";
|
import type { UpliftQueueItem } from "@/domain/uplifts/types/uplift";
|
||||||
import { formatDateTime, getWaitTimeTextClass, timeSince, waitTimeColor } from "@/lib/time-utils";
|
import { formatDateTime, getWaitTimeTextClass, timeSince, waitTimeColor } from "@/lib/time-utils";
|
||||||
|
|
||||||
export type UpliftApprovalTab = "pending" | "approved";
|
|
||||||
|
|
||||||
const CLOSED_WO_TOOLTIP = "This work order is closed. Uplifts can no longer be revoked.";
|
const CLOSED_WO_TOOLTIP = "This work order is closed. Uplifts can no longer be revoked.";
|
||||||
const ADMIN_ONLY_REVOKE_TOOLTIP = "Only admins can revoke uplifts";
|
const ADMIN_ONLY_REVOKE_TOOLTIP = "Only admins can revoke uplifts";
|
||||||
|
|
||||||
|
|
@ -236,65 +242,84 @@ function UpliftApprovalRow({
|
||||||
<AttachmentsCell row={row} onOpenAttachment={onOpenAttachment} />
|
<AttachmentsCell row={row} onOpenAttachment={onOpenAttachment} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{row.requestedByVendorName || "—"}</TableCell>
|
<TableCell>{row.requestedByVendorName || "—"}</TableCell>
|
||||||
{tab === "pending" ? (
|
{tab === "pending" && (
|
||||||
<TableCell>
|
<>
|
||||||
<WaitingCell row={row} />
|
<TableCell>
|
||||||
</TableCell>
|
<WaitingCell row={row} />
|
||||||
) : (
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<PendingRowActions
|
||||||
|
row={row}
|
||||||
|
onApprove={onApprove}
|
||||||
|
onReject={onReject}
|
||||||
|
isDecisionPending={isDecisionPending}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{tab === "approved" && (
|
||||||
<>
|
<>
|
||||||
<TableCell>{row.decidedByName || "—"}</TableCell>
|
<TableCell>{row.decidedByName || "—"}</TableCell>
|
||||||
<TableCell>{row.decidedAt ? formatDateTime(row.decidedAt) : "—"}</TableCell>
|
<TableCell>{row.decidedAt ? formatDateTime(row.decidedAt) : "—"}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<ApprovedRowActions
|
||||||
|
row={row}
|
||||||
|
canRevoke={canRevoke}
|
||||||
|
onRevoke={onRevoke}
|
||||||
|
isDecisionPending={isDecisionPending}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<TableCell>
|
{tab === "rejected" && <RejectedRowCells row={row} />}
|
||||||
{tab === "pending" ? (
|
</TableRow>
|
||||||
<PendingRowActions
|
);
|
||||||
row={row}
|
}
|
||||||
onApprove={onApprove}
|
|
||||||
onReject={onReject}
|
function UpliftApprovalsEmptyState({ tab }: { tab: UpliftApprovalTab }) {
|
||||||
isDecisionPending={isDecisionPending}
|
if (tab === "rejected") return <RejectedEmptyState />;
|
||||||
/>
|
if (tab === "approved") {
|
||||||
) : (
|
return (
|
||||||
<ApprovedRowActions
|
<Box>
|
||||||
row={row}
|
<Text variant="description">No approved uplifts yet</Text>
|
||||||
canRevoke={canRevoke}
|
<Text variant="caption" tone="muted">
|
||||||
onRevoke={onRevoke}
|
Uplifts you approve are archived here for reference.
|
||||||
isDecisionPending={isDecisionPending}
|
</Text>
|
||||||
/>
|
</Box>
|
||||||
)}
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Text variant="description" tone="muted">
|
||||||
|
No uplifts waiting for approval.
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A failed queue is not an empty one: the page's error alert explains it, so the
|
||||||
|
// empty-state copy must not also claim the list has nothing in it.
|
||||||
|
function UpliftApprovalsEmptyRow({
|
||||||
|
tab,
|
||||||
|
colSpan,
|
||||||
|
hasError,
|
||||||
|
}: {
|
||||||
|
tab: UpliftApprovalTab;
|
||||||
|
colSpan: number;
|
||||||
|
hasError: boolean;
|
||||||
|
}) {
|
||||||
|
if (hasError) return null;
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={colSpan} align="center" className="py-10">
|
||||||
|
<UpliftApprovalsEmptyState tab={tab} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const PENDING_HEADER_CELLS = [
|
|
||||||
"Work Order",
|
|
||||||
"Service / Vendor",
|
|
||||||
"Amount",
|
|
||||||
"Approved on WO",
|
|
||||||
"Notes",
|
|
||||||
"Attachments",
|
|
||||||
"Requested By",
|
|
||||||
"Waiting",
|
|
||||||
"Actions",
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const APPROVED_HEADER_CELLS = [
|
|
||||||
"Work Order",
|
|
||||||
"Service / Vendor",
|
|
||||||
"Amount",
|
|
||||||
"Approved on WO",
|
|
||||||
"Notes",
|
|
||||||
"Attachments",
|
|
||||||
"Requested By",
|
|
||||||
"Approved By",
|
|
||||||
"Approved At",
|
|
||||||
"Actions",
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export function UpliftApprovalsTable({
|
export function UpliftApprovalsTable({
|
||||||
tab,
|
tab,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
hasError,
|
||||||
rows,
|
rows,
|
||||||
canRevoke,
|
canRevoke,
|
||||||
onOpenDetail,
|
onOpenDetail,
|
||||||
|
|
@ -306,6 +331,7 @@ export function UpliftApprovalsTable({
|
||||||
}: {
|
}: {
|
||||||
tab: UpliftApprovalTab;
|
tab: UpliftApprovalTab;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
hasError: boolean;
|
||||||
rows: UpliftQueueItem[];
|
rows: UpliftQueueItem[];
|
||||||
canRevoke: boolean;
|
canRevoke: boolean;
|
||||||
onOpenDetail: (row: UpliftQueueItem) => void;
|
onOpenDetail: (row: UpliftQueueItem) => void;
|
||||||
|
|
@ -315,7 +341,7 @@ export function UpliftApprovalsTable({
|
||||||
onOpenAttachment: (row: UpliftQueueItem) => void;
|
onOpenAttachment: (row: UpliftQueueItem) => void;
|
||||||
isDecisionPending: boolean;
|
isDecisionPending: boolean;
|
||||||
}) {
|
}) {
|
||||||
const headerCells = tab === "pending" ? PENDING_HEADER_CELLS : APPROVED_HEADER_CELLS;
|
const headerCells = UPLIFT_APPROVAL_HEADER_CELLS[tab];
|
||||||
return (
|
return (
|
||||||
<TableContainer component={Paper} variant="outlined">
|
<TableContainer component={Paper} variant="outlined">
|
||||||
<Table size="small" aria-label={`${tab} uplift requests`}>
|
<Table size="small" aria-label={`${tab} uplift requests`}>
|
||||||
|
|
@ -336,22 +362,7 @@ export function UpliftApprovalsTable({
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : rows.length === 0 ? (
|
) : rows.length === 0 ? (
|
||||||
<TableRow>
|
<UpliftApprovalsEmptyRow tab={tab} colSpan={headerCells.length} hasError={hasError} />
|
||||||
<TableCell colSpan={headerCells.length} align="center" className="py-10">
|
|
||||||
{tab === "pending" ? (
|
|
||||||
<Text variant="description" tone="muted">
|
|
||||||
No uplifts waiting for approval.
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<Box>
|
|
||||||
<Text variant="description">No approved uplifts yet</Text>
|
|
||||||
<Text variant="caption" tone="muted">
|
|
||||||
Uplifts you approve are archived here for reference.
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
) : (
|
) : (
|
||||||
rows.map((row) => (
|
rows.map((row) => (
|
||||||
<UpliftApprovalRow
|
<UpliftApprovalRow
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import HighlightOffOutlinedIcon from "@mui/icons-material/HighlightOffOutlined";
|
||||||
|
import { Box, TableCell } from "@mui/material";
|
||||||
|
import { Text } from "@/components/ui/text";
|
||||||
|
import type { UpliftQueueItem } from "@/domain/uplifts/types/uplift";
|
||||||
|
import { formatDateTime } from "@/lib/time-utils";
|
||||||
|
|
||||||
|
export function RejectedRowCells({ row }: { row: UpliftQueueItem }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TableCell>{row.decidedByName || "—"}</TableCell>
|
||||||
|
<TableCell>{row.decidedAt ? formatDateTime(row.decidedAt) : "—"}</TableCell>
|
||||||
|
<TableCell sx={{ maxWidth: 260 }}>
|
||||||
|
<Text variant="caption" sx={{ whiteSpace: "pre-wrap" }}>
|
||||||
|
{row.decisionNote || "—"}
|
||||||
|
</Text>
|
||||||
|
</TableCell>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RejectedEmptyState() {
|
||||||
|
return (
|
||||||
|
<Box className="flex flex-col items-center gap-2">
|
||||||
|
<HighlightOffOutlinedIcon color="disabled" aria-hidden />
|
||||||
|
<Text variant="description">No rejected uplifts.</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Alert, Box, Chip, Stack, Tab, Tabs, TablePagination } from "@mui/material";
|
import { Alert, Box, Chip, Stack, Tab, Tabs, TablePagination } from "@mui/material";
|
||||||
import {
|
import {
|
||||||
UpliftApprovalsTable,
|
UPLIFT_APPROVAL_TABS,
|
||||||
|
isUpliftApprovalTab,
|
||||||
type UpliftApprovalTab,
|
type UpliftApprovalTab,
|
||||||
} from "@/app/(protected)/uplifts/_components/uplift-approvals-table";
|
} 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 { UpliftDecisionDialogs } from "@/app/(protected)/uplifts/_components/uplift-decision-dialogs";
|
||||||
import { UpliftDetailModal } from "@/app/(protected)/uplifts/_components/uplift-detail-modal";
|
import { UpliftDetailModal } from "@/app/(protected)/uplifts/_components/uplift-detail-modal";
|
||||||
import {
|
import {
|
||||||
|
|
@ -16,6 +18,7 @@ import { useAuthContext } from "@/providers/auth-context";
|
||||||
import { useUpliftApprovalController } from "@/domain/uplifts/use-cases/use-uplift-approval-controller";
|
import { useUpliftApprovalController } from "@/domain/uplifts/use-cases/use-uplift-approval-controller";
|
||||||
import { useUpliftsQueue } from "@/domain/uplifts/use-cases/use-uplifts-queue";
|
import { useUpliftsQueue } from "@/domain/uplifts/use-cases/use-uplifts-queue";
|
||||||
import { Text } from "@/components/ui/text";
|
import { Text } from "@/components/ui/text";
|
||||||
|
import type { UpliftsQueueResult } from "@/domain/uplifts/types/uplift";
|
||||||
|
|
||||||
const PAGE_SIZE = 25;
|
const PAGE_SIZE = 25;
|
||||||
|
|
||||||
|
|
@ -39,58 +42,71 @@ function UpliftApprovalsHeader({ pendingExposureTotal }: { pendingExposureTotal:
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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({
|
function UpliftApprovalsTabs({
|
||||||
tab,
|
tab,
|
||||||
pendingCount,
|
counts,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
tab: UpliftApprovalTab;
|
tab: UpliftApprovalTab;
|
||||||
pendingCount: number;
|
counts: Record<UpliftApprovalTab, number | null>;
|
||||||
onChange: (_event: unknown, value: string) => void;
|
onChange: (_event: unknown, value: string) => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Tabs value={tab} onChange={onChange}>
|
<Tabs value={tab} onChange={onChange}>
|
||||||
<Tab
|
{UPLIFT_APPROVAL_TABS.map((value) => (
|
||||||
value="pending"
|
<Tab
|
||||||
label={
|
key={value}
|
||||||
<Stack direction="row" spacing={1} sx={{ alignItems: "center" }}>
|
value={value}
|
||||||
<span>Pending</span>
|
label={
|
||||||
<Chip
|
<Stack direction="row" spacing={1} sx={{ alignItems: "center" }}>
|
||||||
size="small"
|
<span>{TAB_LABELS[value]}</span>
|
||||||
label={pendingCount}
|
<TabCountPill tab={value} count={counts[value]} />
|
||||||
aria-label={`${pendingCount} pending uplifts`}
|
</Stack>
|
||||||
/>
|
}
|
||||||
</Stack>
|
/>
|
||||||
}
|
))}
|
||||||
/>
|
|
||||||
<Tab value="approved" label="Approved" />
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function UpliftApprovalsPagination({
|
function UpliftApprovalsPagination({
|
||||||
tab,
|
|
||||||
count,
|
count,
|
||||||
pendingPage,
|
page,
|
||||||
approvedPage,
|
onPageChange,
|
||||||
onPendingPageChange,
|
|
||||||
onApprovedPageChange,
|
|
||||||
}: {
|
}: {
|
||||||
tab: UpliftApprovalTab;
|
|
||||||
count: number;
|
count: number;
|
||||||
pendingPage: number;
|
page: number;
|
||||||
approvedPage: number;
|
onPageChange: (page: number) => void;
|
||||||
onPendingPageChange: (page: number) => void;
|
|
||||||
onApprovedPageChange: (page: number) => void;
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<TablePagination
|
<TablePagination
|
||||||
component="div"
|
component="div"
|
||||||
count={count}
|
count={count}
|
||||||
page={tab === "pending" ? pendingPage : approvedPage}
|
page={page}
|
||||||
onPageChange={(_event, nextPage) =>
|
onPageChange={(_event, nextPage) => onPageChange(nextPage)}
|
||||||
tab === "pending" ? onPendingPageChange(nextPage) : onApprovedPageChange(nextPage)
|
|
||||||
}
|
|
||||||
rowsPerPage={PAGE_SIZE}
|
rowsPerPage={PAGE_SIZE}
|
||||||
rowsPerPageOptions={[PAGE_SIZE]}
|
rowsPerPageOptions={[PAGE_SIZE]}
|
||||||
/>
|
/>
|
||||||
|
|
@ -108,22 +124,31 @@ function UpliftQueueError({ error }: { error: unknown }) {
|
||||||
|
|
||||||
export default function UpliftQueuePage() {
|
export default function UpliftQueuePage() {
|
||||||
const [tab, setTab] = useState<UpliftApprovalTab>("pending");
|
const [tab, setTab] = useState<UpliftApprovalTab>("pending");
|
||||||
const [pendingPage, setPendingPage] = useState(0);
|
const [pages, setPages] = useState<Record<UpliftApprovalTab, number>>({
|
||||||
const [approvedPage, setApprovedPage] = useState(0);
|
pending: 0,
|
||||||
|
approved: 0,
|
||||||
|
rejected: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const { user } = useAuthContext();
|
const { user } = useAuthContext();
|
||||||
const canRevokeUplifts = isAdminUser(user?.userRoles);
|
const canRevokeUplifts = isAdminUser(user?.userRoles);
|
||||||
|
|
||||||
const pendingQuery = useUpliftsQueue({
|
const pendingQuery = useUpliftsQueue({
|
||||||
status: "Pending",
|
status: "Pending",
|
||||||
page: pendingPage + 1,
|
page: pages.pending + 1,
|
||||||
pageSize: PAGE_SIZE,
|
pageSize: PAGE_SIZE,
|
||||||
});
|
});
|
||||||
const approvedQuery = useUpliftsQueue({
|
const approvedQuery = useUpliftsQueue({
|
||||||
status: "Approved",
|
status: "Approved",
|
||||||
page: approvedPage + 1,
|
page: pages.approved + 1,
|
||||||
pageSize: PAGE_SIZE,
|
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 controller = useUpliftApprovalController();
|
||||||
const {
|
const {
|
||||||
|
|
@ -138,13 +163,18 @@ export default function UpliftQueuePage() {
|
||||||
openDetail,
|
openDetail,
|
||||||
} = controller;
|
} = controller;
|
||||||
|
|
||||||
const activeQuery = tab === "pending" ? pendingQuery : approvedQuery;
|
const activeQuery = queries[tab];
|
||||||
const rows = activeQuery.data?.items ?? [];
|
const rows = activeQuery.data?.items ?? [];
|
||||||
const pendingCount = pendingQuery.data?.totalCount ?? 0;
|
const counts = {
|
||||||
|
pending: knownTotal(pendingQuery),
|
||||||
|
approved: knownTotal(approvedQuery),
|
||||||
|
rejected: knownTotal(rejectedQuery),
|
||||||
|
};
|
||||||
|
const activeTotal = counts[tab];
|
||||||
const pendingExposureTotal = pendingQuery.data?.pendingExposureTotal ?? null;
|
const pendingExposureTotal = pendingQuery.data?.pendingExposureTotal ?? null;
|
||||||
|
|
||||||
const handleTabChange = (_event: unknown, value: string) => {
|
const handleTabChange = (_event: unknown, value: string) => {
|
||||||
if (value === "pending" || value === "approved") {
|
if (isUpliftApprovalTab(value)) {
|
||||||
setTab(value);
|
setTab(value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -152,11 +182,12 @@ export default function UpliftQueuePage() {
|
||||||
return (
|
return (
|
||||||
<Box className="flex flex-col gap-4 p-4">
|
<Box className="flex flex-col gap-4 p-4">
|
||||||
<UpliftApprovalsHeader pendingExposureTotal={pendingExposureTotal} />
|
<UpliftApprovalsHeader pendingExposureTotal={pendingExposureTotal} />
|
||||||
<UpliftApprovalsTabs tab={tab} pendingCount={pendingCount} onChange={handleTabChange} />
|
<UpliftApprovalsTabs tab={tab} counts={counts} onChange={handleTabChange} />
|
||||||
<UpliftQueueError error={activeQuery.error} />
|
<UpliftQueueError error={activeQuery.error} />
|
||||||
<UpliftApprovalsTable
|
<UpliftApprovalsTable
|
||||||
tab={tab}
|
tab={tab}
|
||||||
isLoading={activeQuery.isLoading}
|
isLoading={activeQuery.isLoading}
|
||||||
|
hasError={Boolean(activeQuery.error)}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
canRevoke={canRevokeUplifts}
|
canRevoke={canRevokeUplifts}
|
||||||
onOpenDetail={openDetail}
|
onOpenDetail={openDetail}
|
||||||
|
|
@ -166,14 +197,13 @@ export default function UpliftQueuePage() {
|
||||||
onOpenAttachment={openUpliftEvidence}
|
onOpenAttachment={openUpliftEvidence}
|
||||||
isDecisionPending={approvePending || rejectPending || revokePending}
|
isDecisionPending={approvePending || rejectPending || revokePending}
|
||||||
/>
|
/>
|
||||||
<UpliftApprovalsPagination
|
{activeTotal != null && (
|
||||||
tab={tab}
|
<UpliftApprovalsPagination
|
||||||
count={activeQuery.data?.totalCount ?? 0}
|
count={activeTotal}
|
||||||
pendingPage={pendingPage}
|
page={pages[tab]}
|
||||||
approvedPage={approvedPage}
|
onPageChange={(page) => setPages((current) => ({ ...current, [tab]: page }))}
|
||||||
onPendingPageChange={setPendingPage}
|
/>
|
||||||
onApprovedPageChange={setApprovedPage}
|
)}
|
||||||
/>
|
|
||||||
{detailItem != null && (
|
{detailItem != null && (
|
||||||
<UpliftDetailModal
|
<UpliftDetailModal
|
||||||
item={detailItem}
|
item={detailItem}
|
||||||
|
|
|
||||||
|
|
@ -10,5 +10,8 @@ export function useUpliftsQueue(
|
||||||
queryKey: queryKeys.uplifts.queue(params as Record<string, unknown>),
|
queryKey: queryKeys.uplifts.queue(params as Record<string, unknown>),
|
||||||
queryFn: () => upliftsApi.list(params),
|
queryFn: () => upliftsApi.list(params),
|
||||||
placeholderData: keepPreviousData,
|
placeholderData: keepPreviousData,
|
||||||
|
// The Uplift Approvals page reports a failed queue inline, so the global toast
|
||||||
|
// would announce the same failure twice.
|
||||||
|
meta: { suppressErrorToast: true },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ describe("Uplift Approvals approved tab", () => {
|
||||||
const { default: UpliftQueuePage } = await import("@/app/(protected)/uplifts/index");
|
const { default: UpliftQueuePage } = await import("@/app/(protected)/uplifts/index");
|
||||||
renderWithProviders(<UpliftQueuePage />);
|
renderWithProviders(<UpliftQueuePage />);
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("tab", { name: "Approved" }));
|
fireEvent.click(screen.getByRole("tab", { name: /^Approved/ }));
|
||||||
expect(screen.getByRole("columnheader", { name: "Approved By" })).toBeInTheDocument();
|
expect(screen.getByRole("columnheader", { name: "Approved By" })).toBeInTheDocument();
|
||||||
expect(screen.getByRole("columnheader", { name: "Approved At" })).toBeInTheDocument();
|
expect(screen.getByRole("columnheader", { name: "Approved At" })).toBeInTheDocument();
|
||||||
expect(screen.queryByRole("columnheader", { name: "Waiting" })).not.toBeInTheDocument();
|
expect(screen.queryByRole("columnheader", { name: "Waiting" })).not.toBeInTheDocument();
|
||||||
|
|
@ -125,7 +125,7 @@ describe("Uplift Approvals approved tab", () => {
|
||||||
const { default: UpliftQueuePage } = await import("@/app/(protected)/uplifts/index");
|
const { default: UpliftQueuePage } = await import("@/app/(protected)/uplifts/index");
|
||||||
renderWithProviders(<UpliftQueuePage />);
|
renderWithProviders(<UpliftQueuePage />);
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("tab", { name: "Approved" }));
|
fireEvent.click(screen.getByRole("tab", { name: /^Approved/ }));
|
||||||
const revoke = await screen.findByRole("button", { name: "Revoke" });
|
const revoke = await screen.findByRole("button", { name: "Revoke" });
|
||||||
expect(revoke).toBeEnabled();
|
expect(revoke).toBeEnabled();
|
||||||
fireEvent.click(revoke);
|
fireEvent.click(revoke);
|
||||||
|
|
@ -149,7 +149,7 @@ describe("Uplift Approvals approved tab", () => {
|
||||||
const { default: UpliftQueuePage } = await import("@/app/(protected)/uplifts/index");
|
const { default: UpliftQueuePage } = await import("@/app/(protected)/uplifts/index");
|
||||||
renderWithProviders(<UpliftQueuePage />);
|
renderWithProviders(<UpliftQueuePage />);
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("tab", { name: "Approved" }));
|
fireEvent.click(screen.getByRole("tab", { name: /^Approved/ }));
|
||||||
const revoke = await screen.findByRole("button", { name: "Revoke" });
|
const revoke = await screen.findByRole("button", { name: "Revoke" });
|
||||||
expect(revoke).toBeDisabled();
|
expect(revoke).toBeDisabled();
|
||||||
fireEvent.mouseOver(revoke);
|
fireEvent.mouseOver(revoke);
|
||||||
|
|
@ -172,7 +172,7 @@ describe("Uplift Approvals approved tab", () => {
|
||||||
const { default: UpliftQueuePage } = await import("@/app/(protected)/uplifts/index");
|
const { default: UpliftQueuePage } = await import("@/app/(protected)/uplifts/index");
|
||||||
renderWithProviders(<UpliftQueuePage />);
|
renderWithProviders(<UpliftQueuePage />);
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("tab", { name: "Approved" }));
|
fireEvent.click(screen.getByRole("tab", { name: /^Approved/ }));
|
||||||
expect(await screen.findByText("No approved uplifts yet")).toBeInTheDocument();
|
expect(await screen.findByText("No approved uplifts yet")).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
screen.getByText("Uplifts you approve are archived here for reference."),
|
screen.getByText("Uplifts you approve are archived here for reference."),
|
||||||
|
|
|
||||||
|
|
@ -167,7 +167,7 @@ describe("Uplift Approvals shows one amount per request", () => {
|
||||||
queueState.data = queueOf(approvedItem);
|
queueState.data = queueOf(approvedItem);
|
||||||
await renderQueue();
|
await renderQueue();
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("tab", { name: "Approved" }));
|
fireEvent.click(screen.getByRole("tab", { name: /^Approved/ }));
|
||||||
expect(amountCell()).toHaveTextContent(UPLIFT);
|
expect(amountCell()).toHaveTextContent(UPLIFT);
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Revoke" }));
|
fireEvent.click(screen.getByRole("button", { name: "Revoke" }));
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,237 @@
|
||||||
|
import { fireEvent, screen, within } from "@testing-library/react";
|
||||||
|
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import type {
|
||||||
|
UpliftQueueItem,
|
||||||
|
UpliftsQueueParams,
|
||||||
|
UpliftsQueueResult,
|
||||||
|
} from "@/domain/uplifts/types/uplift";
|
||||||
|
import { formatDateTime } from "@/lib/time-utils";
|
||||||
|
import { renderWithProviders } from "@/test/test-utils";
|
||||||
|
|
||||||
|
const queueState = vi.hoisted(() => ({
|
||||||
|
byStatus: {} as Record<string, UpliftsQueueResult | undefined>,
|
||||||
|
errorByStatus: {} as Record<string, Error | undefined>,
|
||||||
|
requestedStatuses: [] as Array<string | undefined>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const baseItem: UpliftQueueItem = {
|
||||||
|
id: 1,
|
||||||
|
status: "Pending",
|
||||||
|
currentNTE: 500,
|
||||||
|
requestedNTE: 750,
|
||||||
|
delta: 250,
|
||||||
|
vendorReason: "Second hydro-jetting pass on the adjoining line",
|
||||||
|
requestedAt: "2026-06-21T10:05:00Z",
|
||||||
|
requestedByVendorName: "Alyssa Dispatcher",
|
||||||
|
decidedAt: "",
|
||||||
|
decidedByName: "",
|
||||||
|
decisionNote: "",
|
||||||
|
requiredTier: 1,
|
||||||
|
canDecide: true,
|
||||||
|
expiresAt: "",
|
||||||
|
notificationStatus: "",
|
||||||
|
notificationError: "",
|
||||||
|
evidenceDocumentId: "doc-9",
|
||||||
|
evidenceFileName: "quote.pdf",
|
||||||
|
evidenceContentType: "application/pdf",
|
||||||
|
evidenceSizeBytes: 1024,
|
||||||
|
dispatchNumber: "DSP-1",
|
||||||
|
poNumber: "PO-1",
|
||||||
|
vendorCompanyName: "Drain Cleaning Bros",
|
||||||
|
workOrderId: 10,
|
||||||
|
dispatchId: 20,
|
||||||
|
woNumber: "WO-25603",
|
||||||
|
site: "CHI2",
|
||||||
|
serviceName: "Drains",
|
||||||
|
technicianName: "",
|
||||||
|
workOrderDispatcherName: "",
|
||||||
|
workOrderScheduledDate: "",
|
||||||
|
attachmentCount: 1,
|
||||||
|
approvedOnWoAuto: null,
|
||||||
|
approvedOnWoAdmin: null,
|
||||||
|
approvedOnWoTotal: null,
|
||||||
|
workOrderClosed: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const newestRejection: UpliftQueueItem = {
|
||||||
|
...baseItem,
|
||||||
|
id: 31,
|
||||||
|
status: "Rejected",
|
||||||
|
woNumber: "WO-31",
|
||||||
|
decidedAt: "2026-06-21T16:40:00Z",
|
||||||
|
decidedByName: "Adam Moussa",
|
||||||
|
decisionNote: "Outside this work order's scope — raise a separate reactive WO.",
|
||||||
|
canDecide: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const olderRejection: UpliftQueueItem = {
|
||||||
|
...baseItem,
|
||||||
|
id: 30,
|
||||||
|
status: "Rejected",
|
||||||
|
woNumber: "WO-30",
|
||||||
|
requestedByVendorName: "Derek Dispatcher",
|
||||||
|
decidedAt: "2026-05-26T15:05:00Z",
|
||||||
|
decidedByName: "Grace Admin",
|
||||||
|
decisionNote: "Quote is above market for this scope.",
|
||||||
|
canDecide: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const pendingItem: UpliftQueueItem = { ...baseItem, id: 40, woNumber: "WO-PENDING" };
|
||||||
|
const approvedItem: UpliftQueueItem = {
|
||||||
|
...baseItem,
|
||||||
|
id: 50,
|
||||||
|
status: "Approved",
|
||||||
|
woNumber: "WO-APPROVED",
|
||||||
|
decidedAt: "2026-06-01T09:00:00Z",
|
||||||
|
decidedByName: "Adam Moussa",
|
||||||
|
};
|
||||||
|
|
||||||
|
function page(items: UpliftQueueItem[], totalCount = items.length): UpliftsQueueResult {
|
||||||
|
return { items, totalCount, page: 1, pageSize: 25, pendingExposureTotal: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mock("@/domain/uplifts/use-cases/use-uplifts-queue", () => ({
|
||||||
|
useUpliftsQueue: (params: UpliftsQueueParams) => {
|
||||||
|
queueState.requestedStatuses.push(params.status);
|
||||||
|
const data = queueState.byStatus[params.status ?? ""];
|
||||||
|
const error = queueState.errorByStatus[params.status ?? ""] ?? null;
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
isLoading: data === undefined && error === null,
|
||||||
|
isFetching: false,
|
||||||
|
error,
|
||||||
|
refetch: vi.fn(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/domain/uplifts/use-cases/use-uplift-actions", () => ({
|
||||||
|
useApproveUplift: () => ({ isPending: false, mutate: vi.fn() }),
|
||||||
|
useRejectUplift: () => ({ isPending: false, mutate: vi.fn() }),
|
||||||
|
useRevokeUplift: () => ({ isPending: false, mutate: vi.fn() }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/providers/auth-context", async (importOriginal) => ({
|
||||||
|
...(await importOriginal<object>()),
|
||||||
|
useAuthContext: () => ({ user: { userRoles: "Admin" } }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/domain/uplifts/use-cases/use-uplifts-can-approve", () => ({
|
||||||
|
useUpliftsCanApprove: () => ({ data: true }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
async function openRejectedTab() {
|
||||||
|
const { default: UpliftQueuePage } = await import("@/app/(protected)/uplifts/index");
|
||||||
|
renderWithProviders(<UpliftQueuePage />);
|
||||||
|
fireEvent.click(screen.getByRole("tab", { name: /^Rejected/ }));
|
||||||
|
return screen.getByRole("table", { name: "rejected uplift requests" });
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Uplift Approvals rejected tab", () => {
|
||||||
|
// Load the page module once up front so the first test's budget is not spent on a
|
||||||
|
// cold import of the whole route.
|
||||||
|
beforeAll(async () => {
|
||||||
|
await import("@/app/(protected)/uplifts/index");
|
||||||
|
}, 60_000);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
queueState.requestedStatuses = [];
|
||||||
|
queueState.errorByStatus = {};
|
||||||
|
queueState.byStatus = {
|
||||||
|
Pending: page([pendingItem]),
|
||||||
|
Approved: page([approvedItem]),
|
||||||
|
Rejected: page([newestRejection, olderRejection], 12),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows the rejected total in the tab count pill next to the other tabs' pills", async () => {
|
||||||
|
const { default: UpliftQueuePage } = await import("@/app/(protected)/uplifts/index");
|
||||||
|
renderWithProviders(<UpliftQueuePage />);
|
||||||
|
|
||||||
|
const rejectedTab = screen.getByRole("tab", { name: /^Rejected/ });
|
||||||
|
expect(within(rejectedTab).getByLabelText("12 rejected uplifts")).toHaveTextContent("12");
|
||||||
|
expect(screen.getByLabelText("1 pending uplifts")).toBeInTheDocument();
|
||||||
|
expect(screen.getByLabelText("1 approved uplifts")).toBeInTheDocument();
|
||||||
|
expect(queueState.requestedStatuses).toContain("Rejected");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("never claims a count for a queue that is still loading or failed to load", async () => {
|
||||||
|
queueState.byStatus.Rejected = undefined;
|
||||||
|
queueState.errorByStatus.Rejected = new Error("Forbidden");
|
||||||
|
queueState.byStatus.Approved = undefined;
|
||||||
|
const { default: UpliftQueuePage } = await import("@/app/(protected)/uplifts/index");
|
||||||
|
renderWithProviders(<UpliftQueuePage />);
|
||||||
|
|
||||||
|
const rejectedPill = screen.getByLabelText("rejected uplift count unavailable");
|
||||||
|
expect(rejectedPill).toHaveTextContent("—");
|
||||||
|
expect(screen.getByLabelText("approved uplift count unavailable")).toHaveTextContent("—");
|
||||||
|
expect(screen.queryByLabelText("0 rejected uplifts")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByLabelText("0 approved uplifts")).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByLabelText("1 pending uplifts")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the count and pager for kept rows when a rejected refetch fails", async () => {
|
||||||
|
queueState.errorByStatus.Rejected = new Error("Failed to load uplift queue");
|
||||||
|
const table = await openRejectedTab();
|
||||||
|
|
||||||
|
expect(screen.getByRole("alert")).toHaveTextContent("Failed to load uplift queue");
|
||||||
|
expect(within(table).getAllByRole("row")).toHaveLength(3);
|
||||||
|
expect(screen.getByLabelText("12 rejected uplifts")).toHaveTextContent("12");
|
||||||
|
expect(screen.getByText(/of 12/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows the load error instead of an empty archive when the rejected queue fails", async () => {
|
||||||
|
queueState.byStatus.Rejected = undefined;
|
||||||
|
queueState.errorByStatus.Rejected = new Error("Failed to load uplift queue");
|
||||||
|
const table = await openRejectedTab();
|
||||||
|
|
||||||
|
expect(screen.getByRole("alert")).toHaveTextContent("Failed to load uplift queue");
|
||||||
|
expect(within(table).queryByText("No rejected uplifts.")).not.toBeInTheDocument();
|
||||||
|
expect(within(table).queryByTestId("HighlightOffOutlinedIcon")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText(/of 0/)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lists only rejected uplifts, in the server's most-recently-rejected order", async () => {
|
||||||
|
const table = await openRejectedTab();
|
||||||
|
|
||||||
|
const dataRows = within(table).getAllByRole("row").slice(1);
|
||||||
|
expect(dataRows).toHaveLength(2);
|
||||||
|
expect(dataRows[0]).toHaveTextContent("WO-31");
|
||||||
|
expect(dataRows[1]).toHaveTextContent("WO-30");
|
||||||
|
expect(within(table).queryByText("WO-PENDING")).not.toBeInTheDocument();
|
||||||
|
expect(within(table).queryByText("WO-APPROVED")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows who requested, who rejected, when, and the rejection reason inline", async () => {
|
||||||
|
const table = await openRejectedTab();
|
||||||
|
|
||||||
|
for (const header of ["Requested By", "Rejected By", "Rejected At", "Rejection Reason"]) {
|
||||||
|
expect(within(table).getByRole("columnheader", { name: header })).toBeInTheDocument();
|
||||||
|
}
|
||||||
|
const [, firstRow] = within(table).getAllByRole("row");
|
||||||
|
expect(firstRow).toHaveTextContent("Alyssa Dispatcher");
|
||||||
|
expect(firstRow).toHaveTextContent("Adam Moussa");
|
||||||
|
expect(firstRow).toHaveTextContent(formatDateTime(newestRejection.decidedAt));
|
||||||
|
expect(
|
||||||
|
within(firstRow).getByText("Outside this work order's scope — raise a separate reactive WO."),
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("offers no action on a rejected uplift", async () => {
|
||||||
|
const table = await openRejectedTab();
|
||||||
|
|
||||||
|
expect(within(table).queryByRole("columnheader", { name: "Actions" })).not.toBeInTheDocument();
|
||||||
|
for (const name of [/^Approve/, /^Reject$/, /^Revoke/]) {
|
||||||
|
expect(within(table).queryByRole("button", { name })).not.toBeInTheDocument();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows an icon and the empty copy when nothing has been rejected", async () => {
|
||||||
|
queueState.byStatus.Rejected = page([]);
|
||||||
|
const table = await openRejectedTab();
|
||||||
|
|
||||||
|
expect(within(table).getByText("No rejected uplifts.")).toBeInTheDocument();
|
||||||
|
expect(within(table).getByTestId("HighlightOffOutlinedIcon")).toBeInTheDocument();
|
||||||
|
expect(screen.getByLabelText("0 rejected uplifts")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
40
src/test/domain/uplifts/use-cases/use-uplifts-queue.test.tsx
Normal file
40
src/test/domain/uplifts/use-cases/use-uplifts-queue.test.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import { renderHook, waitFor } from "@testing-library/react";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { upliftsApi } from "@/domain/uplifts/api/uplifts-api";
|
||||||
|
import { useUpliftsQueue } from "@/domain/uplifts/use-cases/use-uplifts-queue";
|
||||||
|
import { createAppQueryClient } from "@/lib/query/query-client";
|
||||||
|
|
||||||
|
const toastMocks = vi.hoisted(() => ({ error: vi.fn() }));
|
||||||
|
|
||||||
|
vi.mock("react-toastify", () => ({
|
||||||
|
toast: { error: toastMocks.error, success: vi.fn() },
|
||||||
|
}));
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
toastMocks.error.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("useUpliftsQueue", () => {
|
||||||
|
it("does not show the global error toast, since the approvals page reports the failure inline", async () => {
|
||||||
|
vi.spyOn(upliftsApi, "list").mockRejectedValue(new Error("Failed to load uplift queue"));
|
||||||
|
// The real app query client runs the QueryCache's global onError handler, which
|
||||||
|
// toasts unless the query opts out.
|
||||||
|
const queryClient = createAppQueryClient();
|
||||||
|
queryClient.setDefaultOptions({ queries: { retry: false } });
|
||||||
|
|
||||||
|
const { result } = renderHook(
|
||||||
|
() => useUpliftsQueue({ status: "Rejected", page: 1, pageSize: 25 }),
|
||||||
|
{
|
||||||
|
wrapper: ({ children }: { children: ReactNode }) => (
|
||||||
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => expect(result.current.error).toBeInstanceOf(Error));
|
||||||
|
expect(toastMocks.error).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue