mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-06 15:52:04 +00:00
fix(uplifts): complete approval queue review fixes (SH-208)
This commit is contained in:
parent
6f8b502116
commit
5aed31392c
4 changed files with 60 additions and 27 deletions
|
|
@ -15,7 +15,7 @@ import {
|
||||||
import { formatUpliftMoney } from "@/app/(protected)/uplifts/_components/format-uplift-money";
|
import { formatUpliftMoney } from "@/app/(protected)/uplifts/_components/format-uplift-money";
|
||||||
import { Text } from "@/components/ui/text";
|
import { Text } from "@/components/ui/text";
|
||||||
import type { UpliftQueueItem } from "@/domain/uplifts/types/uplift";
|
import type { UpliftQueueItem } from "@/domain/uplifts/types/uplift";
|
||||||
import { getWaitTimeTextClass, timeSince, waitTimeColor } from "@/lib/time-utils";
|
import { formatDateTime, getWaitTimeTextClass, timeSince, waitTimeColor } from "@/lib/time-utils";
|
||||||
|
|
||||||
export type UpliftApprovalTab = "pending" | "approved";
|
export type UpliftApprovalTab = "pending" | "approved";
|
||||||
|
|
||||||
|
|
@ -223,9 +223,16 @@ function UpliftApprovalRow({
|
||||||
<AttachmentsCell row={row} onOpenAttachment={onOpenAttachment} />
|
<AttachmentsCell row={row} onOpenAttachment={onOpenAttachment} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{row.requestedByVendorName || "—"}</TableCell>
|
<TableCell>{row.requestedByVendorName || "—"}</TableCell>
|
||||||
<TableCell>
|
{tab === "pending" ? (
|
||||||
<WaitingCell row={row} />
|
<TableCell>
|
||||||
</TableCell>
|
<WaitingCell row={row} />
|
||||||
|
</TableCell>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<TableCell>{row.decidedByName || "—"}</TableCell>
|
||||||
|
<TableCell>{row.decidedAt ? formatDateTime(row.decidedAt) : "—"}</TableCell>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{tab === "pending" ? (
|
{tab === "pending" ? (
|
||||||
<PendingRowActions
|
<PendingRowActions
|
||||||
|
|
@ -242,7 +249,7 @@ function UpliftApprovalRow({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const HEADER_CELLS = [
|
const PENDING_HEADER_CELLS = [
|
||||||
"Work Order",
|
"Work Order",
|
||||||
"Service / Vendor",
|
"Service / Vendor",
|
||||||
"Amount",
|
"Amount",
|
||||||
|
|
@ -254,6 +261,19 @@ const HEADER_CELLS = [
|
||||||
"Actions",
|
"Actions",
|
||||||
] as const;
|
] 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,
|
||||||
|
|
@ -275,12 +295,13 @@ 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;
|
||||||
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`}>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
{HEADER_CELLS.map((label) => (
|
{headerCells.map((label) => (
|
||||||
<TableCell key={label} className="font-semibold">
|
<TableCell key={label} className="font-semibold">
|
||||||
{label}
|
{label}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
@ -290,13 +311,13 @@ export function UpliftApprovalsTable({
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={HEADER_CELLS.length} align="center" className="py-10">
|
<TableCell colSpan={headerCells.length} align="center" className="py-10">
|
||||||
<CircularProgress size={28} />
|
<CircularProgress size={28} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : rows.length === 0 ? (
|
) : rows.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={HEADER_CELLS.length} align="center" className="py-10">
|
<TableCell colSpan={headerCells.length} align="center" className="py-10">
|
||||||
{tab === "pending" ? (
|
{tab === "pending" ? (
|
||||||
<Text variant="description" tone="muted">
|
<Text variant="description" tone="muted">
|
||||||
No uplifts waiting for approval.
|
No uplifts waiting for approval.
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ type ModalCallbacks = {
|
||||||
onApprove: (row: UpliftQueueItem) => void;
|
onApprove: (row: UpliftQueueItem) => void;
|
||||||
onReject: (row: UpliftQueueItem) => void;
|
onReject: (row: UpliftQueueItem) => void;
|
||||||
onRevoke: (row: UpliftQueueItem) => void;
|
onRevoke: (row: UpliftQueueItem) => void;
|
||||||
|
onOpenAttachment: (row: UpliftQueueItem) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function DetailField({ label, children }: { label: string; children: ReactNode }) {
|
function DetailField({ label, children }: { label: string; children: ReactNode }) {
|
||||||
|
|
@ -49,7 +50,13 @@ function DetailSection({ title, children }: { title: string; children: ReactNode
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function UpliftAttachments({ item }: { item: UpliftQueueItem }) {
|
function UpliftAttachments({
|
||||||
|
item,
|
||||||
|
onOpenAttachment,
|
||||||
|
}: {
|
||||||
|
item: UpliftQueueItem;
|
||||||
|
onOpenAttachment: (row: UpliftQueueItem) => void;
|
||||||
|
}) {
|
||||||
if (item.evidenceDocumentId == null && !item.evidenceFileName) {
|
if (item.evidenceDocumentId == null && !item.evidenceFileName) {
|
||||||
return (
|
return (
|
||||||
<Text variant="description" tone="muted">
|
<Text variant="description" tone="muted">
|
||||||
|
|
@ -63,7 +70,12 @@ function UpliftAttachments({ item }: { item: UpliftQueueItem }) {
|
||||||
: 0;
|
: 0;
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5, flexWrap: "wrap" }}>
|
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5, flexWrap: "wrap" }}>
|
||||||
<Chip size="small" label={item.evidenceFileName || "Attachment"} />
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label={item.evidenceFileName || "Attachment"}
|
||||||
|
onClick={item.evidenceDocumentId != null ? () => onOpenAttachment(item) : undefined}
|
||||||
|
clickable={item.evidenceDocumentId != null}
|
||||||
|
/>
|
||||||
{extraCount > 0 && <Chip size="small" label={`+${extraCount}`} />}
|
{extraCount > 0 && <Chip size="small" label={`+${extraCount}`} />}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
@ -91,7 +103,13 @@ function WorkOrderSection({ item, info }: { item: UpliftQueueItem; info?: WorkOr
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RequestSection({ item }: { item: UpliftQueueItem }) {
|
function RequestSection({
|
||||||
|
item,
|
||||||
|
onOpenAttachment,
|
||||||
|
}: {
|
||||||
|
item: UpliftQueueItem;
|
||||||
|
onOpenAttachment: (row: UpliftQueueItem) => void;
|
||||||
|
}) {
|
||||||
const waitingClass = item.requestedAt
|
const waitingClass = item.requestedAt
|
||||||
? getWaitTimeTextClass(waitTimeColor(item.requestedAt))
|
? getWaitTimeTextClass(waitTimeColor(item.requestedAt))
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
@ -114,7 +132,7 @@ function RequestSection({ item }: { item: UpliftQueueItem }) {
|
||||||
<Text variant="description">{item.vendorReason || "No justification provided."}</Text>
|
<Text variant="description">{item.vendorReason || "No justification provided."}</Text>
|
||||||
</DetailField>
|
</DetailField>
|
||||||
<DetailField label="Attachments">
|
<DetailField label="Attachments">
|
||||||
<UpliftAttachments item={item} />
|
<UpliftAttachments item={item} onOpenAttachment={onOpenAttachment} />
|
||||||
</DetailField>
|
</DetailField>
|
||||||
</DetailSection>
|
</DetailSection>
|
||||||
);
|
);
|
||||||
|
|
@ -269,6 +287,7 @@ export function UpliftDetailModal({
|
||||||
onApprove,
|
onApprove,
|
||||||
onReject,
|
onReject,
|
||||||
onRevoke,
|
onRevoke,
|
||||||
|
onOpenAttachment,
|
||||||
}: {
|
}: {
|
||||||
item: UpliftQueueItem | null;
|
item: UpliftQueueItem | null;
|
||||||
approvePending: boolean;
|
approvePending: boolean;
|
||||||
|
|
@ -326,7 +345,7 @@ export function UpliftDetailModal({
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Box sx={{ display: "grid", gap: 3 }}>
|
<Box sx={{ display: "grid", gap: 3 }}>
|
||||||
<WorkOrderSection item={item} info={info} />
|
<WorkOrderSection item={item} info={info} />
|
||||||
<RequestSection item={item} />
|
<RequestSection item={item} onOpenAttachment={onOpenAttachment} />
|
||||||
<DetailSection title="Approved on work order">
|
<DetailSection title="Approved on work order">
|
||||||
<ApprovedOnWoBreakdown
|
<ApprovedOnWoBreakdown
|
||||||
auto={breakdown.auto}
|
auto={breakdown.auto}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useMemo, 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 { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { RejectDialog } from "@/app/(protected)/uplifts/_components/reject-dialog";
|
import { RejectDialog } from "@/app/(protected)/uplifts/_components/reject-dialog";
|
||||||
|
|
@ -17,18 +17,6 @@ import { Text } from "@/components/ui/text";
|
||||||
|
|
||||||
const PAGE_SIZE = 25;
|
const PAGE_SIZE = 25;
|
||||||
|
|
||||||
function sortRows(rows: UpliftQueueItem[], tab: UpliftApprovalTab): UpliftQueueItem[] {
|
|
||||||
const copy = [...rows];
|
|
||||||
if (tab === "pending") {
|
|
||||||
copy.sort((a, b) =>
|
|
||||||
a.requestedAt < b.requestedAt ? -1 : a.requestedAt > b.requestedAt ? 1 : 0,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
copy.sort((a, b) => (a.decidedAt > b.decidedAt ? -1 : a.decidedAt < b.decidedAt ? 1 : 0));
|
|
||||||
}
|
|
||||||
return copy;
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpliftApprovalsHeader({ pendingExposureTotal }: { pendingExposureTotal: number | null }) {
|
function UpliftApprovalsHeader({ pendingExposureTotal }: { pendingExposureTotal: number | null }) {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
|
|
@ -151,7 +139,7 @@ export default function UpliftQueuePage() {
|
||||||
} = useUpliftApprovalController();
|
} = useUpliftApprovalController();
|
||||||
|
|
||||||
const activeQuery = tab === "pending" ? pendingQuery : approvedQuery;
|
const activeQuery = tab === "pending" ? pendingQuery : approvedQuery;
|
||||||
const rows = useMemo(() => sortRows(activeQuery.data?.items ?? [], tab), [activeQuery.data, tab]);
|
const rows = activeQuery.data?.items ?? [];
|
||||||
const pendingCount = pendingQuery.data?.totalCount ?? 0;
|
const pendingCount = pendingQuery.data?.totalCount ?? 0;
|
||||||
const pendingExposureTotal = pendingQuery.data?.pendingExposureTotal ?? null;
|
const pendingExposureTotal = pendingQuery.data?.pendingExposureTotal ?? null;
|
||||||
|
|
||||||
|
|
@ -204,6 +192,7 @@ export default function UpliftQueuePage() {
|
||||||
onApprove={handleApprove}
|
onApprove={handleApprove}
|
||||||
onReject={handleRejectRequest}
|
onReject={handleRejectRequest}
|
||||||
onRevoke={handleRevokeRequest}
|
onRevoke={handleRevokeRequest}
|
||||||
|
onOpenAttachment={handleOpenAttachment}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<RejectDialog
|
<RejectDialog
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,10 @@ describe("Uplift Approvals approved tab", () => {
|
||||||
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 At" })).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole("columnheader", { name: "Waiting" })).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Admin User")).toBeInTheDocument();
|
||||||
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);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue