mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-06 21:42:06 +00:00
feat: add uplifts queue module
This commit is contained in:
parent
1c474914f2
commit
48f1685e4d
6 changed files with 534 additions and 0 deletions
312
src/app/(protected)/uplifts/index.tsx
Normal file
312
src/app/(protected)/uplifts/index.tsx
Normal file
|
|
@ -0,0 +1,312 @@
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Chip,
|
||||||
|
CircularProgress,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
MenuItem,
|
||||||
|
Paper,
|
||||||
|
Select,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TablePagination,
|
||||||
|
TableRow,
|
||||||
|
TextField,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
} from "@mui/material";
|
||||||
|
import type { UpliftQueueItem } from "@/domain/uplifts/types/uplift";
|
||||||
|
import { useApproveUplift, useDenyUplift } from "@/domain/uplifts/use-cases/use-uplift-actions";
|
||||||
|
import { useUpliftsQueue } from "@/domain/uplifts/use-cases/use-uplifts-queue";
|
||||||
|
import { formatDateTime } from "@/lib/time-utils";
|
||||||
|
|
||||||
|
const STATUS_OPTIONS = ["Pending", "Approved", "Denied", "Cancelled", ""];
|
||||||
|
|
||||||
|
function formatMoney(value: number | null | undefined): string {
|
||||||
|
return `$${Number(value ?? 0).toFixed(2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UpliftQueuePage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [status, setStatus] = useState("Pending");
|
||||||
|
const [tier, setTier] = useState("");
|
||||||
|
const [page, setPage] = useState(0);
|
||||||
|
const [pageSize] = useState(25);
|
||||||
|
|
||||||
|
const [upliftAction, setUpliftAction] = useState<{
|
||||||
|
type: "approve" | "deny";
|
||||||
|
item: UpliftQueueItem;
|
||||||
|
} | null>(null);
|
||||||
|
const [note, setNote] = useState("");
|
||||||
|
|
||||||
|
const listParams = useMemo(
|
||||||
|
() => ({ status, tier, page: page + 1, pageSize }),
|
||||||
|
[status, tier, page, pageSize],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data, isLoading, isFetching, error, refetch } = useUpliftsQueue(listParams);
|
||||||
|
const approveUplift = useApproveUplift();
|
||||||
|
const denyUplift = useDenyUplift();
|
||||||
|
|
||||||
|
const rows = data?.items ?? [];
|
||||||
|
const totalCount = data?.totalCount ?? 0;
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
if (!upliftAction) return;
|
||||||
|
if (upliftAction.type === "approve") {
|
||||||
|
approveUplift.mutate(
|
||||||
|
{ id: upliftAction.item.id, note: note || undefined },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
setUpliftAction(null);
|
||||||
|
setNote("");
|
||||||
|
void refetch();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (!note.trim()) return;
|
||||||
|
denyUplift.mutate(
|
||||||
|
{ id: upliftAction.item.id, note: note.trim() },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
setUpliftAction(null);
|
||||||
|
setNote("");
|
||||||
|
void refetch();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className="flex flex-col gap-4 p-4">
|
||||||
|
<Stack
|
||||||
|
direction={{ xs: "column", md: "row" }}
|
||||||
|
spacing={2}
|
||||||
|
alignItems={{ xs: "stretch", md: "center" }}
|
||||||
|
justifyContent="space-between"
|
||||||
|
>
|
||||||
|
<Stack direction="row" spacing={2} alignItems="baseline">
|
||||||
|
<Typography variant="h5" component="h1">
|
||||||
|
Uplift Queue
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{totalCount} total
|
||||||
|
</Typography>
|
||||||
|
{isFetching && !isLoading && <CircularProgress size={16} />}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack direction="row" spacing={2}>
|
||||||
|
<FormControl size="small" className="min-w-[140px]">
|
||||||
|
<InputLabel>Status</InputLabel>
|
||||||
|
<Select
|
||||||
|
label="Status"
|
||||||
|
value={status}
|
||||||
|
onChange={(e) => {
|
||||||
|
setStatus(e.target.value);
|
||||||
|
setPage(0);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{STATUS_OPTIONS.map((s) => (
|
||||||
|
<MenuItem key={s || "all"} value={s}>
|
||||||
|
{s || "All"}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl size="small" className="min-w-[120px]">
|
||||||
|
<InputLabel>Tier</InputLabel>
|
||||||
|
<Select
|
||||||
|
label="Tier"
|
||||||
|
value={tier}
|
||||||
|
onChange={(e) => {
|
||||||
|
setTier(e.target.value);
|
||||||
|
setPage(0);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem value="">All</MenuItem>
|
||||||
|
<MenuItem value="1">Tier 1</MenuItem>
|
||||||
|
<MenuItem value="2">Tier 2</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert severity="error">
|
||||||
|
{error instanceof Error ? error.message : "Failed to load uplift queue"}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TableContainer component={Paper} variant="outlined">
|
||||||
|
<Table size="small">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell className="font-semibold">Dispatch</TableCell>
|
||||||
|
<TableCell className="font-semibold">PO</TableCell>
|
||||||
|
<TableCell className="font-semibold">Vendor</TableCell>
|
||||||
|
<TableCell className="font-semibold">Current NTE</TableCell>
|
||||||
|
<TableCell className="font-semibold">Requested</TableCell>
|
||||||
|
<TableCell className="font-semibold">Delta</TableCell>
|
||||||
|
<TableCell className="font-semibold">Tier</TableCell>
|
||||||
|
<TableCell className="font-semibold">Status</TableCell>
|
||||||
|
<TableCell className="font-semibold">Requested At</TableCell>
|
||||||
|
<TableCell className="font-semibold">Actions</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{isLoading ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={10} align="center" className="py-10">
|
||||||
|
<CircularProgress size={28} />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : rows.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={10} align="center" className="py-10">
|
||||||
|
<Typography color="text.secondary">
|
||||||
|
No uplift requests match these filters.
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
rows.map((row) => (
|
||||||
|
<TableRow key={String(row.id)} hover>
|
||||||
|
<TableCell>{row.dispatchNumber}</TableCell>
|
||||||
|
<TableCell>{row.poNumber}</TableCell>
|
||||||
|
<TableCell>{row.vendorCompanyName}</TableCell>
|
||||||
|
<TableCell>{formatMoney(row.currentNTE)}</TableCell>
|
||||||
|
<TableCell>{formatMoney(row.requestedNTE)}</TableCell>
|
||||||
|
<TableCell>{formatMoney(row.delta)}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Chip size="small" label={`Tier ${row.requiredTier}`} />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Chip size="small" label={row.status} />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{row.requestedAt ? formatDateTime(row.requestedAt) : ""}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Stack direction="row" spacing={1}>
|
||||||
|
{row.workOrderId && (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => navigate(`/workorders/${row.workOrderId}`)}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{row.status === "Pending" && (
|
||||||
|
<>
|
||||||
|
<Tooltip
|
||||||
|
title={!row.canDecide ? `Requires Tier ${row.requiredTier} role` : ""}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
color="success"
|
||||||
|
disabled={!row.canDecide}
|
||||||
|
onClick={() => {
|
||||||
|
setUpliftAction({ type: "approve", item: row });
|
||||||
|
setNote("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Approve
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip
|
||||||
|
title={!row.canDecide ? `Requires Tier ${row.requiredTier} role` : ""}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
color="error"
|
||||||
|
disabled={!row.canDecide}
|
||||||
|
onClick={() => {
|
||||||
|
setUpliftAction({ type: "deny", item: row });
|
||||||
|
setNote("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Deny
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
|
||||||
|
<TablePagination
|
||||||
|
component="div"
|
||||||
|
count={totalCount}
|
||||||
|
page={page}
|
||||||
|
onPageChange={(_e, nextPage) => setPage(nextPage)}
|
||||||
|
rowsPerPage={pageSize}
|
||||||
|
rowsPerPageOptions={[25]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Dialog open={Boolean(upliftAction)} onClose={() => setUpliftAction(null)}>
|
||||||
|
<DialogTitle>
|
||||||
|
{upliftAction?.type === "approve"
|
||||||
|
? `Approve uplift to ${formatMoney(upliftAction.item.requestedNTE)}?`
|
||||||
|
: "Deny Uplift"}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
{upliftAction?.type === "approve" && (
|
||||||
|
<Typography variant="body2" className="mt-1 mb-3">
|
||||||
|
{formatMoney(upliftAction.item.currentNTE)} →{" "}
|
||||||
|
{formatMoney(upliftAction.item.requestedNTE)} (+
|
||||||
|
{formatMoney(upliftAction.item.delta)})
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
minRows={2}
|
||||||
|
className="mt-2"
|
||||||
|
label={upliftAction?.type === "deny" ? "Denial reason (required)" : "Optional note"}
|
||||||
|
value={note}
|
||||||
|
onChange={(e) => setNote(e.target.value)}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setUpliftAction(null)}>Cancel</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color={upliftAction?.type === "approve" ? "success" : "error"}
|
||||||
|
disabled={
|
||||||
|
upliftAction?.type === "deny"
|
||||||
|
? !note.trim() || denyUplift.isPending
|
||||||
|
: approveUplift.isPending
|
||||||
|
}
|
||||||
|
onClick={handleConfirm}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
63
src/domain/uplifts/api/uplifts-api.ts
Normal file
63
src/domain/uplifts/api/uplifts-api.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { API_PATHS } from "@/api/api-paths";
|
||||||
|
import { apiGet, apiPost } from "@/api/api";
|
||||||
|
import { handleApiResponse } from "@/api/handle-api-response";
|
||||||
|
import { mapUpliftQueueItems } from "@/domain/uplifts/mappers/uplift-mapper";
|
||||||
|
import type { UpliftsQueueParams, UpliftsQueueResult } from "@/domain/uplifts/types/uplift";
|
||||||
|
import { mapUpliftRequests } from "@/domain/work-orders/mappers/work-order-mapper";
|
||||||
|
import type { UpliftRequest } from "@/domain/work-orders/types/work-order";
|
||||||
|
|
||||||
|
function buildQueueParams(params: UpliftsQueueParams): Record<string, string | number> {
|
||||||
|
const searchParams: Record<string, string | number> = {
|
||||||
|
page: params.page ?? 1,
|
||||||
|
pageSize: params.pageSize ?? 25,
|
||||||
|
};
|
||||||
|
if (params.status) searchParams.status = params.status;
|
||||||
|
if (params.tier) searchParams.tier = params.tier;
|
||||||
|
return searchParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const upliftsApi = {
|
||||||
|
list: async (params: UpliftsQueueParams = {}): Promise<UpliftsQueueResult> => {
|
||||||
|
const data = await apiGet<unknown>(API_PATHS.rest.uplifts, {
|
||||||
|
searchParams: buildQueueParams(params),
|
||||||
|
});
|
||||||
|
const response = handleApiResponse<Record<string, unknown>>(data);
|
||||||
|
const items = Array.isArray(response?.items)
|
||||||
|
? response.items
|
||||||
|
: Array.isArray((response as { data?: unknown[] })?.data)
|
||||||
|
? (response as { data: unknown[] }).data
|
||||||
|
: [];
|
||||||
|
const total =
|
||||||
|
(typeof response?.total === "number" ? response.total : undefined) ??
|
||||||
|
(typeof response?.totalCount === "number" ? response.totalCount : undefined) ??
|
||||||
|
items.length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: mapUpliftQueueItems(items),
|
||||||
|
totalCount: total,
|
||||||
|
page: params.page ?? 1,
|
||||||
|
pageSize: params.pageSize ?? 25,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
listForDispatch: async (dispatchId: string | number): Promise<UpliftRequest[]> => {
|
||||||
|
const data = await apiGet<unknown>(`${API_PATHS.rest.uplifts}/dispatch/${dispatchId}`);
|
||||||
|
const response = handleApiResponse<unknown>(data);
|
||||||
|
const items = Array.isArray(response)
|
||||||
|
? response
|
||||||
|
: typeof response === "object" &&
|
||||||
|
response !== null &&
|
||||||
|
Array.isArray((response as { data?: unknown[] }).data)
|
||||||
|
? (response as { data: unknown[] }).data
|
||||||
|
: [];
|
||||||
|
return mapUpliftRequests(items);
|
||||||
|
},
|
||||||
|
|
||||||
|
approve: async (id: string | number, note?: string): Promise<void> => {
|
||||||
|
await apiPost<unknown>(`${API_PATHS.rest.uplifts}/${id}/approve`, { note });
|
||||||
|
},
|
||||||
|
|
||||||
|
deny: async (id: string | number, note: string): Promise<void> => {
|
||||||
|
await apiPost<unknown>(`${API_PATHS.rest.uplifts}/${id}/deny`, { note });
|
||||||
|
},
|
||||||
|
};
|
||||||
43
src/domain/uplifts/mappers/uplift-mapper.ts
Normal file
43
src/domain/uplifts/mappers/uplift-mapper.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import type { UpliftQueueItem } from "@/domain/uplifts/types/uplift";
|
||||||
|
import { mapUpliftRequest } from "@/domain/work-orders/mappers/work-order-mapper";
|
||||||
|
|
||||||
|
function asRecord(raw: unknown): Record<string, unknown> {
|
||||||
|
return typeof raw === "object" && raw !== null ? (raw as Record<string, unknown>) : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function readString(record: Record<string, unknown>, ...keys: string[]): string {
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = record[key];
|
||||||
|
if (typeof value === "string") return value;
|
||||||
|
if (typeof value === "number") return String(value);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function readOptionalId(
|
||||||
|
record: Record<string, unknown>,
|
||||||
|
...keys: string[]
|
||||||
|
): string | number | null {
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = record[key];
|
||||||
|
if (typeof value === "string" || typeof value === "number") return value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapUpliftQueueItem(raw: unknown): UpliftQueueItem {
|
||||||
|
const base = mapUpliftRequest(raw);
|
||||||
|
const item = asRecord(raw);
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
dispatchNumber: readString(item, "dispatchNumber", "DispatchNumber"),
|
||||||
|
poNumber: readString(item, "poNumber", "PONumber"),
|
||||||
|
vendorCompanyName: readString(item, "vendorCompanyName", "VendorCompanyName"),
|
||||||
|
workOrderId: readOptionalId(item, "workOrderId", "WorkOrderId"),
|
||||||
|
dispatchId: readOptionalId(item, "dispatchId", "DispatchId"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapUpliftQueueItems(rawItems: unknown[]): UpliftQueueItem[] {
|
||||||
|
return rawItems.map(mapUpliftQueueItem);
|
||||||
|
}
|
||||||
23
src/domain/uplifts/types/uplift.ts
Normal file
23
src/domain/uplifts/types/uplift.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import type { UpliftRequest } from "@/domain/work-orders/types/work-order";
|
||||||
|
|
||||||
|
export interface UpliftQueueItem extends UpliftRequest {
|
||||||
|
dispatchNumber: string;
|
||||||
|
poNumber: string;
|
||||||
|
vendorCompanyName: string;
|
||||||
|
workOrderId: string | number | null;
|
||||||
|
dispatchId: string | number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpliftsQueueParams {
|
||||||
|
status?: string;
|
||||||
|
tier?: string;
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpliftsQueueResult {
|
||||||
|
items: UpliftQueueItem[];
|
||||||
|
totalCount: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
79
src/domain/uplifts/use-cases/use-uplift-actions.ts
Normal file
79
src/domain/uplifts/use-cases/use-uplift-actions.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
import {
|
||||||
|
useMutation,
|
||||||
|
useQuery,
|
||||||
|
useQueryClient,
|
||||||
|
type UseMutationResult,
|
||||||
|
type UseQueryResult,
|
||||||
|
} from "@tanstack/react-query";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { upliftsApi } from "@/domain/uplifts/api/uplifts-api";
|
||||||
|
import type { UpliftRequest } from "@/domain/work-orders/types/work-order";
|
||||||
|
import { queryKeys } from "@/infra/query-key/query-key";
|
||||||
|
import { requireQueryParam } from "@/lib/query/require-query-param";
|
||||||
|
|
||||||
|
export function useUpliftsForDispatch(
|
||||||
|
dispatchId: string | number | undefined,
|
||||||
|
): UseQueryResult<UpliftRequest[], Error> {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: queryKeys.uplifts.forDispatch(dispatchId ?? ""),
|
||||||
|
queryFn: () => upliftsApi.listForDispatch(requireQueryParam(dispatchId, "Dispatch id")),
|
||||||
|
enabled: dispatchId !== undefined && dispatchId !== "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function invalidateUpliftQueries(
|
||||||
|
queryClient: ReturnType<typeof useQueryClient>,
|
||||||
|
dispatchId?: string | number,
|
||||||
|
) {
|
||||||
|
void queryClient.invalidateQueries({ queryKey: queryKeys.uplifts.all });
|
||||||
|
if (dispatchId !== undefined && dispatchId !== "") {
|
||||||
|
void queryClient.invalidateQueries({ queryKey: queryKeys.uplifts.forDispatch(dispatchId) });
|
||||||
|
void queryClient.invalidateQueries({ queryKey: queryKeys.workOrders.dispatch(dispatchId) });
|
||||||
|
void queryClient.invalidateQueries({ queryKey: queryKeys.vendorPos.detail(dispatchId) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApproveUpliftInput {
|
||||||
|
id: string | number;
|
||||||
|
note?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DenyUpliftInput {
|
||||||
|
id: string | number;
|
||||||
|
note: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useApproveUplift(
|
||||||
|
dispatchId?: string | number,
|
||||||
|
): UseMutationResult<void, Error, ApproveUpliftInput> {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, note }: { id: string | number; note?: string }) =>
|
||||||
|
upliftsApi.approve(id, note),
|
||||||
|
onSuccess: () => {
|
||||||
|
invalidateUpliftQueries(queryClient, dispatchId);
|
||||||
|
toast.success("Uplift approved — NTE updated");
|
||||||
|
},
|
||||||
|
onError: (error: Error) => {
|
||||||
|
toast.error(error.message || "Failed to approve uplift");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDenyUplift(
|
||||||
|
dispatchId?: string | number,
|
||||||
|
): UseMutationResult<void, Error, DenyUpliftInput> {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, note }: { id: string | number; note: string }) => upliftsApi.deny(id, note),
|
||||||
|
onSuccess: () => {
|
||||||
|
invalidateUpliftQueries(queryClient, dispatchId);
|
||||||
|
toast.success("Uplift denied");
|
||||||
|
},
|
||||||
|
onError: (error: Error) => {
|
||||||
|
toast.error(error.message || "Failed to deny uplift");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
14
src/domain/uplifts/use-cases/use-uplifts-queue.ts
Normal file
14
src/domain/uplifts/use-cases/use-uplifts-queue.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { keepPreviousData, useQuery, type UseQueryResult } from "@tanstack/react-query";
|
||||||
|
import { upliftsApi } from "@/domain/uplifts/api/uplifts-api";
|
||||||
|
import type { UpliftsQueueParams, UpliftsQueueResult } from "@/domain/uplifts/types/uplift";
|
||||||
|
import { queryKeys } from "@/infra/query-key/query-key";
|
||||||
|
|
||||||
|
export function useUpliftsQueue(
|
||||||
|
params: UpliftsQueueParams,
|
||||||
|
): UseQueryResult<UpliftsQueueResult, Error> {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: queryKeys.uplifts.queue(params as Record<string, unknown>),
|
||||||
|
queryFn: () => upliftsApi.list(params),
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue