diff --git a/src/app/(protected)/uplifts/index.tsx b/src/app/(protected)/uplifts/index.tsx
new file mode 100644
index 00000000..89fb736e
--- /dev/null
+++ b/src/app/(protected)/uplifts/index.tsx
@@ -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 (
+
+
+
+
+ Uplift Queue
+
+
+ {totalCount} total
+
+ {isFetching && !isLoading && }
+
+
+
+
+ Status
+
+
+
+ Tier
+
+
+
+
+
+ {error && (
+
+ {error instanceof Error ? error.message : "Failed to load uplift queue"}
+
+ )}
+
+
+
+
+
+ Dispatch
+ PO
+ Vendor
+ Current NTE
+ Requested
+ Delta
+ Tier
+ Status
+ Requested At
+ Actions
+
+
+
+ {isLoading ? (
+
+
+
+
+
+ ) : rows.length === 0 ? (
+
+
+
+ No uplift requests match these filters.
+
+
+
+ ) : (
+ rows.map((row) => (
+
+ {row.dispatchNumber}
+ {row.poNumber}
+ {row.vendorCompanyName}
+ {formatMoney(row.currentNTE)}
+ {formatMoney(row.requestedNTE)}
+ {formatMoney(row.delta)}
+
+
+
+
+
+
+ {row.requestedAt ? formatDateTime(row.requestedAt) : ""}
+
+
+ {row.workOrderId && (
+
+ )}
+ {row.status === "Pending" && (
+ <>
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+
+
+ ))
+ )}
+
+
+
+
+ setPage(nextPage)}
+ rowsPerPage={pageSize}
+ rowsPerPageOptions={[25]}
+ />
+
+
+
+ );
+}
diff --git a/src/domain/uplifts/api/uplifts-api.ts b/src/domain/uplifts/api/uplifts-api.ts
new file mode 100644
index 00000000..bdb3fdc3
--- /dev/null
+++ b/src/domain/uplifts/api/uplifts-api.ts
@@ -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 {
+ const searchParams: Record = {
+ 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 => {
+ const data = await apiGet(API_PATHS.rest.uplifts, {
+ searchParams: buildQueueParams(params),
+ });
+ const response = handleApiResponse>(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 => {
+ const data = await apiGet(`${API_PATHS.rest.uplifts}/dispatch/${dispatchId}`);
+ const response = handleApiResponse(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 => {
+ await apiPost(`${API_PATHS.rest.uplifts}/${id}/approve`, { note });
+ },
+
+ deny: async (id: string | number, note: string): Promise => {
+ await apiPost(`${API_PATHS.rest.uplifts}/${id}/deny`, { note });
+ },
+};
diff --git a/src/domain/uplifts/mappers/uplift-mapper.ts b/src/domain/uplifts/mappers/uplift-mapper.ts
new file mode 100644
index 00000000..132ab0c3
--- /dev/null
+++ b/src/domain/uplifts/mappers/uplift-mapper.ts
@@ -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 {
+ return typeof raw === "object" && raw !== null ? (raw as Record) : {};
+}
+
+function readString(record: Record, ...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,
+ ...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);
+}
diff --git a/src/domain/uplifts/types/uplift.ts b/src/domain/uplifts/types/uplift.ts
new file mode 100644
index 00000000..8d6d3503
--- /dev/null
+++ b/src/domain/uplifts/types/uplift.ts
@@ -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;
+}
diff --git a/src/domain/uplifts/use-cases/use-uplift-actions.ts b/src/domain/uplifts/use-cases/use-uplift-actions.ts
new file mode 100644
index 00000000..071ce7e3
--- /dev/null
+++ b/src/domain/uplifts/use-cases/use-uplift-actions.ts
@@ -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 {
+ return useQuery({
+ queryKey: queryKeys.uplifts.forDispatch(dispatchId ?? ""),
+ queryFn: () => upliftsApi.listForDispatch(requireQueryParam(dispatchId, "Dispatch id")),
+ enabled: dispatchId !== undefined && dispatchId !== "",
+ });
+}
+
+function invalidateUpliftQueries(
+ queryClient: ReturnType,
+ 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 {
+ 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 {
+ 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");
+ },
+ });
+}
diff --git a/src/domain/uplifts/use-cases/use-uplifts-queue.ts b/src/domain/uplifts/use-cases/use-uplifts-queue.ts
new file mode 100644
index 00000000..9ebcf4bd
--- /dev/null
+++ b/src/domain/uplifts/use-cases/use-uplifts-queue.ts
@@ -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 {
+ return useQuery({
+ queryKey: queryKeys.uplifts.queue(params as Record),
+ queryFn: () => upliftsApi.list(params),
+ placeholderData: keepPreviousData,
+ });
+}