diff --git a/src/app/(protected)/vendor-pos/[id].tsx b/src/app/(protected)/vendor-pos/[id].tsx
new file mode 100644
index 00000000..b64971d2
--- /dev/null
+++ b/src/app/(protected)/vendor-pos/[id].tsx
@@ -0,0 +1,345 @@
+import { useState, type ReactNode } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+import {
+ Alert,
+ Box,
+ Button,
+ Chip,
+ CircularProgress,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ Paper,
+ Stack,
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableRow,
+ TextField,
+ Typography,
+} from "@mui/material";
+import { useApproveUplift, useDenyUplift } from "@/domain/uplifts/use-cases/use-uplift-actions";
+import { useVendorPoDetail } from "@/domain/vendor-pos/use-cases/use-vendor-po-detail";
+import type { UpliftRequest } from "@/domain/work-orders/types/work-order";
+import { formatDateTime, toDateInputValue } from "@/lib/time-utils";
+
+function formatMoney(value: number | null | undefined): string {
+ if (value == null) return "—";
+ return `$${Number(value).toFixed(2)}`;
+}
+
+function InfoRow({ label, children }: { label: string; children: ReactNode }) {
+ return (
+
+
+ {label}
+
+
+ {children == null || children === "" ? "—" : children}
+
+
+ );
+}
+
+export default function VendorPoViewPage() {
+ const { id } = useParams();
+ const navigate = useNavigate();
+ const { data: dispatch, isLoading, error, refetch } = useVendorPoDetail(id);
+
+ const approveUplift = useApproveUplift(id);
+ const denyUplift = useDenyUplift(id);
+
+ const [upliftAction, setUpliftAction] = useState<{
+ type: "approve" | "deny";
+ uplift: UpliftRequest;
+ } | null>(null);
+ const [note, setNote] = useState("");
+
+ const handleUpliftConfirm = () => {
+ if (!upliftAction) return;
+ if (upliftAction.type === "approve") {
+ approveUplift.mutate(
+ { id: upliftAction.uplift.id, note: note || undefined },
+ {
+ onSuccess: () => {
+ setUpliftAction(null);
+ setNote("");
+ void refetch();
+ },
+ },
+ );
+ } else {
+ if (!note.trim()) return;
+ denyUplift.mutate(
+ { id: upliftAction.uplift.id, note: note.trim() },
+ {
+ onSuccess: () => {
+ setUpliftAction(null);
+ setNote("");
+ void refetch();
+ },
+ },
+ );
+ }
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (error || !dispatch) {
+ return (
+
+
+ {error instanceof Error ? error.message : "Vendor PO not found"}
+
+
+ );
+ }
+
+ const vendor = dispatch.vendor;
+
+ return (
+
+
+
+
+
+
+
+ {dispatch.poNumber || "(no PO #)"}
+
+
+ Dispatch {dispatch.dispatchNumber || "—"}
+
+
+ {vendor?.companyName && (
+
+ · {vendor.companyName}
+
+ )}
+ {dispatch.nteAmount != null && (
+
+ · NTE {formatMoney(dispatch.nteAmount)}
+
+ )}
+
+
+
+
+
+
+ Vendor
+
+ {vendor ? (
+
+ {vendor.companyName}
+ {vendor.contactName}
+ {vendor.email}
+ {vendor.phone}
+
+ {[vendor.address, vendor.city, vendor.state, vendor.zip].filter(Boolean).join(", ")}
+
+ {vendor.tradeSpecialties}
+
+ ) : (
+ No vendor attached.
+ )}
+
+
+
+
+ PO Details
+
+
+ {dispatch.poNumber}
+ {dispatch.dispatchNumber}
+ {dispatch.status}
+ {formatMoney(dispatch.nteAmount)}
+ {formatDateTime(dispatch.dispatchedAt)}
+ {formatDateTime(dispatch.acknowledgedAt)}
+ {toDateInputValue(dispatch.scheduledDate) || "—"}
+ {toDateInputValue(dispatch.completedDate) || "—"}
+ {formatDateTime(dispatch.verifiedAt)}
+ {dispatch.emailSent ? "Yes" : "No"}
+
+ {dispatch.description && (
+
+
+ Description
+
+
+ {dispatch.description}
+
+
+ )}
+
+
+ {dispatch.primaryLocation && (
+
+
+ Site
+
+
+ {dispatch.primaryLocation.name}
+
+ {[
+ dispatch.primaryLocation.address,
+ dispatch.primaryLocation.city,
+ dispatch.primaryLocation.state,
+ dispatch.primaryLocation.zipCode,
+ ]
+ .filter(Boolean)
+ .join(", ")}
+
+
+
+ )}
+
+
+
+ Linked Work Orders ({dispatch.workOrders.length})
+
+ {dispatch.workOrders.length === 0 ? (
+ No work orders linked.
+ ) : (
+
+
+
+ WO #
+ Title
+ Trade
+ Status
+ Location
+
+
+
+ {dispatch.workOrders.map((wo) => (
+
+
+
+
+ {wo.title}
+ {wo.trade}
+ {wo.status}
+ {wo.locationName}
+
+ ))}
+
+
+ )}
+
+
+
+
+ NTE Uplift History ({dispatch.upliftRequests.length})
+
+ {dispatch.upliftRequests.length === 0 ? (
+ No uplift requests on this PO.
+ ) : (
+
+ {dispatch.upliftRequests.map((u) => (
+
+
+
+ {formatMoney(u.currentNTE)} → {formatMoney(u.requestedNTE)}
+
+ (Δ {formatMoney(u.delta)}, Tier {u.requiredTier})
+
+
+
+
+ {u.vendorReason && (
+
+ Reason: {u.vendorReason}
+
+ )}
+ {u.status === "Pending" && (
+
+
+
+
+ )}
+
+ ))}
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/src/app/(protected)/vendor-pos/index.tsx b/src/app/(protected)/vendor-pos/index.tsx
new file mode 100644
index 00000000..ff5971f6
--- /dev/null
+++ b/src/app/(protected)/vendor-pos/index.tsx
@@ -0,0 +1,281 @@
+import { useMemo, useState } from "react";
+import { useNavigate } from "react-router-dom";
+import {
+ Alert,
+ Box,
+ Button,
+ Chip,
+ CircularProgress,
+ FormControl,
+ InputLabel,
+ MenuItem,
+ Paper,
+ Select,
+ Stack,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TablePagination,
+ TableRow,
+ TextField,
+ Typography,
+} from "@mui/material";
+import { useVendorPosList } from "@/domain/vendor-pos/use-cases/use-vendor-pos-list";
+import { useVendorsList } from "@/domain/vendors/use-cases/use-vendors-list";
+import { useDebounce } from "@/hooks/use-debounce";
+import { toDateInputValue } from "@/lib/time-utils";
+
+const STATUS_OPTIONS = [
+ "",
+ "Sent",
+ "Acknowledged",
+ "In Progress",
+ "Completed",
+ "Verified",
+ "Cancelled",
+];
+
+function formatMoney(value: number | null): string {
+ if (value == null) return "—";
+ return `$${value.toFixed(2)}`;
+}
+
+function formatDate(value: string): string {
+ if (!value) return "—";
+ return toDateInputValue(value) || "—";
+}
+
+export default function VendorPosListPage() {
+ const navigate = useNavigate();
+ const [search, setSearch] = useState("");
+ const [page, setPage] = useState(0);
+ const [pageSize, setPageSize] = useState(12);
+ const [status, setStatus] = useState("");
+ const [vendorId, setVendorId] = useState("");
+ const [dateFrom, setDateFrom] = useState("");
+ const [dateTo, setDateTo] = useState("");
+
+ const debouncedSearch = useDebounce(search, 300);
+
+ const listParams = useMemo(
+ () => ({
+ search: debouncedSearch,
+ page: page + 1,
+ pageSize,
+ status,
+ vendorId,
+ dateFrom,
+ dateTo,
+ }),
+ [debouncedSearch, page, pageSize, status, vendorId, dateFrom, dateTo],
+ );
+
+ const { data, isLoading, isFetching, error } = useVendorPosList(listParams);
+ const { data: vendorsData } = useVendorsList({ page: 1, pageSize: 500 });
+
+ const rows = data?.items ?? [];
+ const totalCount = data?.totalCount ?? 0;
+ const vendors = vendorsData?.items ?? [];
+
+ const clearFilters = () => {
+ setStatus("");
+ setVendorId("");
+ setDateFrom("");
+ setDateTo("");
+ setPage(0);
+ };
+
+ return (
+
+
+
+
+ Vendor POs
+
+
+ {totalCount} total
+
+ {isFetching && !isLoading && }
+
+
+ {
+ setSearch(e.target.value);
+ setPage(0);
+ }}
+ placeholder="Search PO #, dispatch, vendor, WO..."
+ className="min-w-[280px]"
+ />
+
+
+
+
+ Status
+
+
+
+
+ Vendor
+
+
+
+ {
+ setDateFrom(e.target.value);
+ setPage(0);
+ }}
+ slotProps={{ inputLabel: { shrink: true } }}
+ />
+ {
+ setDateTo(e.target.value);
+ setPage(0);
+ }}
+ slotProps={{ inputLabel: { shrink: true } }}
+ />
+
+
+
+ {error && (
+
+ {error instanceof Error ? error.message : "Failed to load vendor POs"}
+
+ )}
+
+
+
+
+
+ PO #
+ Dispatch #
+ Vendor
+ WO #
+ Location
+ NTE
+ Status
+ Uplifts
+ Dispatched
+
+
+
+ {isLoading ? (
+
+
+
+
+
+ ) : rows.length === 0 ? (
+
+
+ No vendor POs found
+
+
+ ) : (
+ rows.map((row) => (
+ navigate(`/vendor-pos/${row.id}`)}
+ >
+
+
+
+ {row.dispatchNumber || "—"}
+ {row.vendorName || "—"}
+ {row.primaryWOInternalNumber || "—"}
+ {row.locationName || "—"}
+ {formatMoney(row.nteAmount)}
+
+
+
+
+ {row.pendingUpliftCount > 0 ? (
+
+ ) : (
+ "—"
+ )}
+
+ {formatDate(row.dispatchedAt)}
+
+ ))
+ )}
+
+
+
+
+ setPage(nextPage)}
+ rowsPerPage={pageSize}
+ onRowsPerPageChange={(e) => {
+ setPageSize(Number(e.target.value));
+ setPage(0);
+ }}
+ rowsPerPageOptions={[12, 24, 48]}
+ />
+
+ );
+}
diff --git a/src/domain/vendor-pos/api/vendor-pos-api.ts b/src/domain/vendor-pos/api/vendor-pos-api.ts
new file mode 100644
index 00000000..ef8b1d72
--- /dev/null
+++ b/src/domain/vendor-pos/api/vendor-pos-api.ts
@@ -0,0 +1,57 @@
+import { API_PATHS } from "@/api/api-paths";
+import { apiGet } from "@/api/api";
+import { handleApiResponse } from "@/api/handle-api-response";
+import {
+ mapVendorPoDetail,
+ mapVendorPoListItem,
+} from "@/domain/vendor-pos/mappers/vendor-po-mapper";
+import type {
+ VendorPoDetail,
+ VendorPoListParams,
+ VendorPoListResult,
+} from "@/domain/vendor-pos/types/vendor-po";
+
+function buildListParams(params: VendorPoListParams): Record {
+ const searchParams: Record = {
+ page: params.page ?? 1,
+ pageSize: params.pageSize ?? 12,
+ sortBy: params.sortBy ?? "dispatchedAt",
+ sortDir: params.sortDir ?? "desc",
+ };
+ if (params.search) searchParams.search = params.search;
+ if (params.status) searchParams.status = params.status;
+ if (params.vendorId) searchParams.vendorId = params.vendorId;
+ if (params.dateFrom) searchParams.dateFrom = params.dateFrom;
+ if (params.dateTo) searchParams.dateTo = params.dateTo;
+ return searchParams;
+}
+
+export const vendorPosApi = {
+ getList: async (params: VendorPoListParams = {}): Promise => {
+ const data = await apiGet(API_PATHS.rest.dispatches, {
+ searchParams: buildListParams(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: items.map(mapVendorPoListItem),
+ totalCount: total,
+ page: params.page ?? 1,
+ pageSize: params.pageSize ?? 12,
+ };
+ },
+
+ getById: async (id: string | number): Promise => {
+ const data = await apiGet(`${API_PATHS.rest.dispatches}/${id}`);
+ return mapVendorPoDetail(handleApiResponse(data));
+ },
+};
diff --git a/src/domain/vendor-pos/mappers/vendor-po-mapper.ts b/src/domain/vendor-pos/mappers/vendor-po-mapper.ts
new file mode 100644
index 00000000..e20fe7ff
--- /dev/null
+++ b/src/domain/vendor-pos/mappers/vendor-po-mapper.ts
@@ -0,0 +1,196 @@
+import type {
+ VendorPoAuditEntry,
+ VendorPoChecklistItem,
+ VendorPoComment,
+ VendorPoDetail,
+ VendorPoListItem,
+ VendorPoLocation,
+ VendorPoSignoff,
+ VendorPoVendor,
+ VendorPoWorkOrder,
+} from "@/domain/vendor-pos/types/vendor-po";
+import { mapUpliftRequests } 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 readId(record: Record): string | number {
+ const value = record.id ?? record.Id;
+ if (typeof value === "string" || typeof value === "number") return value;
+ return "";
+}
+
+function readNumber(record: Record, ...keys: string[]): number | null {
+ for (const key of keys) {
+ const value = record[key];
+ if (typeof value === "number") return value;
+ if (typeof value === "string" && value !== "") {
+ const parsed = Number(value);
+ if (!Number.isNaN(parsed)) return parsed;
+ }
+ }
+ return null;
+}
+
+function readBool(record: Record, ...keys: string[]): boolean {
+ for (const key of keys) {
+ const value = record[key];
+ if (typeof value === "boolean") return value;
+ }
+ return false;
+}
+
+export function mapVendorPoListItem(raw: unknown): VendorPoListItem {
+ const item = asRecord(raw);
+ return {
+ id: readId(item),
+ poNumber: readString(item, "poNumber", "PONumber"),
+ dispatchNumber: readString(item, "dispatchNumber", "DispatchNumber"),
+ vendorName: readString(item, "vendorName", "VendorName"),
+ primaryWOInternalNumber: readString(item, "primaryWOInternalNumber", "PrimaryWOInternalNumber"),
+ workOrderCount: readNumber(item, "workOrderCount", "WorkOrderCount") ?? 0,
+ locationName: readString(item, "locationName", "LocationName"),
+ nteAmount: readNumber(item, "nteAmount", "NTEAmount"),
+ status: readString(item, "status", "Status"),
+ pendingUpliftCount: readNumber(item, "pendingUpliftCount", "PendingUpliftCount") ?? 0,
+ dispatchedAt: readString(item, "dispatchedAt", "DispatchedAt"),
+ scheduledDate: readString(item, "scheduledDate", "ScheduledDate"),
+ completedDate: readString(item, "completedDate", "CompletedDate"),
+ };
+}
+
+function mapVendor(raw: unknown): VendorPoVendor | null {
+ if (!raw) return null;
+ const item = asRecord(raw);
+ return {
+ companyName: readString(item, "companyName", "CompanyName"),
+ contactName: readString(item, "contactName", "ContactName"),
+ email: readString(item, "email", "Email"),
+ phone: readString(item, "phone", "Phone"),
+ address: readString(item, "address", "Address"),
+ city: readString(item, "city", "City"),
+ state: readString(item, "state", "State"),
+ zip: readString(item, "zip", "Zip", "zipCode"),
+ tradeSpecialties: readString(item, "tradeSpecialties", "TradeSpecialties"),
+ };
+}
+
+function mapLocation(raw: unknown): VendorPoLocation | null {
+ if (!raw) return null;
+ const item = asRecord(raw);
+ return {
+ name: readString(item, "name", "Name"),
+ address: readString(item, "address", "Address"),
+ city: readString(item, "city", "City"),
+ state: readString(item, "state", "State"),
+ zipCode: readString(item, "zipCode", "ZipCode", "zip"),
+ };
+}
+
+function mapWorkOrder(raw: unknown): VendorPoWorkOrder {
+ const item = asRecord(raw);
+ return {
+ workOrderId: readId(item) || readString(item, "workOrderId", "WorkOrderId"),
+ internalWONumber: readString(item, "internalWONumber", "InternalWONumber"),
+ title: readString(item, "title", "Title"),
+ trade: readString(item, "trade", "Trade"),
+ subTrade: readString(item, "subTrade", "SubTrade"),
+ problem: readString(item, "problem", "Problem"),
+ priority: readString(item, "priority", "Priority"),
+ status: readString(item, "status", "Status"),
+ locationName: readString(item, "locationName", "LocationName"),
+ };
+}
+
+function mapChecklistItem(raw: unknown): VendorPoChecklistItem {
+ const item = asRecord(raw);
+ return {
+ id: readId(item),
+ itemText: readString(item, "itemText", "ItemText"),
+ isCompleted: readBool(item, "isCompleted", "IsCompleted"),
+ completedBy: readString(item, "completedBy", "CompletedBy"),
+ completedAt: readString(item, "completedAt", "CompletedAt"),
+ };
+}
+
+function mapSignoff(raw: unknown): VendorPoSignoff {
+ const item = asRecord(raw);
+ return {
+ id: readId(item),
+ signoffType: readString(item, "signoffType", "SignoffType"),
+ name: readString(item, "name", "Name"),
+ signatureMethod: readString(item, "signatureMethod", "SignatureMethod"),
+ signedAt: readString(item, "signedAt", "SignedAt"),
+ signature: readString(item, "signature", "Signature"),
+ };
+}
+
+function mapComment(raw: unknown): VendorPoComment {
+ const item = asRecord(raw);
+ return {
+ id: readId(item),
+ userName: readString(item, "userName", "UserName"),
+ commenter: readString(item, "commenter", "Commenter"),
+ commentType: readString(item, "commentType", "CommentType"),
+ createdDate: readString(item, "createdDate", "CreatedDate"),
+ commenttext: readString(item, "commenttext", "CommentText", "commentText"),
+ };
+}
+
+function mapAuditEntry(raw: unknown): VendorPoAuditEntry {
+ const item = asRecord(raw);
+ return {
+ id: readId(item),
+ createdAt: readString(item, "createdAt", "CreatedAt"),
+ action: readString(item, "action", "Action"),
+ fieldName: readString(item, "fieldName", "FieldName"),
+ oldValue: readString(item, "oldValue", "OldValue"),
+ newValue: readString(item, "newValue", "NewValue"),
+ userName: readString(item, "userName", "UserName"),
+ };
+}
+
+export function mapVendorPoDetail(raw: unknown): VendorPoDetail {
+ const item = asRecord(raw);
+ const vendor = item.vendor ?? item.Vendor;
+ const location = item.primaryLocation ?? item.PrimaryLocation;
+ const workOrders = (item.workOrders ?? item.WorkOrders) as unknown[];
+ const checklist = (item.checklist ?? item.Checklist) as unknown[];
+ const signoffs = (item.signoffs ?? item.Signoffs) as unknown[];
+ const comments = (item.comments ?? item.Comments) as unknown[];
+ const uplifts = (item.upliftRequests ?? item.UpliftRequests) as unknown[];
+ const auditLog = (item.auditLog ?? item.AuditLog) as unknown[];
+
+ return {
+ id: readId(item),
+ poNumber: readString(item, "poNumber", "PONumber"),
+ dispatchNumber: readString(item, "dispatchNumber", "DispatchNumber"),
+ status: readString(item, "status", "Status"),
+ nteAmount: readNumber(item, "nteAmount", "NTEAmount"),
+ dispatchedAt: readString(item, "dispatchedAt", "DispatchedAt"),
+ acknowledgedAt: readString(item, "acknowledgedAt", "AcknowledgedAt"),
+ scheduledDate: readString(item, "scheduledDate", "ScheduledDate"),
+ completedDate: readString(item, "completedDate", "CompletedDate"),
+ verifiedAt: readString(item, "verifiedAt", "VerifiedAt"),
+ emailSent: readBool(item, "emailSent", "EmailSent"),
+ description: readString(item, "description", "Description"),
+ vendor: mapVendor(vendor),
+ primaryLocation: mapLocation(location),
+ workOrders: Array.isArray(workOrders) ? workOrders.map(mapWorkOrder) : [],
+ checklist: Array.isArray(checklist) ? checklist.map(mapChecklistItem) : [],
+ signoffs: Array.isArray(signoffs) ? signoffs.map(mapSignoff) : [],
+ comments: Array.isArray(comments) ? comments.map(mapComment) : [],
+ upliftRequests: Array.isArray(uplifts) ? mapUpliftRequests(uplifts) : [],
+ auditLog: Array.isArray(auditLog) ? auditLog.map(mapAuditEntry) : [],
+ };
+}
diff --git a/src/domain/vendor-pos/types/vendor-po.ts b/src/domain/vendor-pos/types/vendor-po.ts
new file mode 100644
index 00000000..d1fef92b
--- /dev/null
+++ b/src/domain/vendor-pos/types/vendor-po.ts
@@ -0,0 +1,127 @@
+import type { UpliftRequest } from "@/domain/work-orders/types/work-order";
+
+export interface VendorPoListItem {
+ id: string | number;
+ poNumber: string;
+ dispatchNumber: string;
+ vendorName: string;
+ primaryWOInternalNumber: string;
+ workOrderCount: number;
+ locationName: string;
+ nteAmount: number | null;
+ status: string;
+ pendingUpliftCount: number;
+ dispatchedAt: string;
+ scheduledDate: string;
+ completedDate: string;
+}
+
+export interface VendorPoListParams {
+ search?: string;
+ page?: number;
+ pageSize?: number;
+ sortBy?: string;
+ sortDir?: string;
+ status?: string;
+ vendorId?: string;
+ dateFrom?: string;
+ dateTo?: string;
+}
+
+export interface VendorPoListResult {
+ items: VendorPoListItem[];
+ totalCount: number;
+ page: number;
+ pageSize: number;
+}
+
+export interface VendorPoVendor {
+ companyName: string;
+ contactName: string;
+ email: string;
+ phone: string;
+ address: string;
+ city: string;
+ state: string;
+ zip: string;
+ tradeSpecialties: string;
+}
+
+export interface VendorPoLocation {
+ name: string;
+ address: string;
+ city: string;
+ state: string;
+ zipCode: string;
+}
+
+export interface VendorPoWorkOrder {
+ workOrderId: string | number;
+ internalWONumber: string;
+ title: string;
+ trade: string;
+ subTrade: string;
+ problem: string;
+ priority: string;
+ status: string;
+ locationName: string;
+}
+
+export interface VendorPoChecklistItem {
+ id: string | number;
+ itemText: string;
+ isCompleted: boolean;
+ completedBy: string;
+ completedAt: string;
+}
+
+export interface VendorPoSignoff {
+ id: string | number;
+ signoffType: string;
+ name: string;
+ signatureMethod: string;
+ signedAt: string;
+ signature: string;
+}
+
+export interface VendorPoComment {
+ id: string | number;
+ userName: string;
+ commenter: string;
+ commentType: string;
+ createdDate: string;
+ commenttext: string;
+}
+
+export interface VendorPoAuditEntry {
+ id: string | number;
+ createdAt: string;
+ action: string;
+ fieldName: string;
+ oldValue: string;
+ newValue: string;
+ userName: string;
+}
+
+export interface VendorPoDetail {
+ id: string | number;
+ poNumber: string;
+ dispatchNumber: string;
+ status: string;
+ nteAmount: number | null;
+ dispatchedAt: string;
+ acknowledgedAt: string;
+ scheduledDate: string;
+ completedDate: string;
+ verifiedAt: string;
+ emailSent: boolean;
+ description: string;
+ vendor: VendorPoVendor | null;
+ primaryLocation: VendorPoLocation | null;
+ workOrders: VendorPoWorkOrder[];
+ checklist: VendorPoChecklistItem[];
+ signoffs: VendorPoSignoff[];
+ comments: VendorPoComment[];
+ upliftRequests: UpliftRequest[];
+ auditLog: VendorPoAuditEntry[];
+}
diff --git a/src/domain/vendor-pos/use-cases/use-vendor-po-detail.ts b/src/domain/vendor-pos/use-cases/use-vendor-po-detail.ts
new file mode 100644
index 00000000..d68eca77
--- /dev/null
+++ b/src/domain/vendor-pos/use-cases/use-vendor-po-detail.ts
@@ -0,0 +1,13 @@
+import { useQuery, type UseQueryResult } from "@tanstack/react-query";
+import { vendorPosApi } from "@/domain/vendor-pos/api/vendor-pos-api";
+import type { VendorPoDetail } from "@/domain/vendor-pos/types/vendor-po";
+import { queryKeys } from "@/infra/query-key/query-key";
+import { requireQueryParam } from "@/lib/query/require-query-param";
+
+export function useVendorPoDetail(id: string | undefined): UseQueryResult {
+ return useQuery({
+ queryKey: queryKeys.vendorPos.detail(id ?? ""),
+ queryFn: () => vendorPosApi.getById(requireQueryParam(id, "Vendor PO id")),
+ enabled: Boolean(id),
+ });
+}
diff --git a/src/domain/vendor-pos/use-cases/use-vendor-pos-list.ts b/src/domain/vendor-pos/use-cases/use-vendor-pos-list.ts
new file mode 100644
index 00000000..3cda45d1
--- /dev/null
+++ b/src/domain/vendor-pos/use-cases/use-vendor-pos-list.ts
@@ -0,0 +1,14 @@
+import { keepPreviousData, useQuery, type UseQueryResult } from "@tanstack/react-query";
+import { vendorPosApi } from "@/domain/vendor-pos/api/vendor-pos-api";
+import type { VendorPoListParams, VendorPoListResult } from "@/domain/vendor-pos/types/vendor-po";
+import { queryKeys } from "@/infra/query-key/query-key";
+
+export function useVendorPosList(
+ params: VendorPoListParams,
+): UseQueryResult {
+ return useQuery({
+ queryKey: queryKeys.vendorPos.list(params as Record),
+ queryFn: () => vendorPosApi.getList(params),
+ placeholderData: keepPreviousData,
+ });
+}