diff --git a/src/App.js b/src/App.js index ccb45c25..9512c6d0 100644 --- a/src/App.js +++ b/src/App.js @@ -54,6 +54,11 @@ import VendorFormPage from "./pages/vendors/form/VendorFormPage"; import DropdownOptionsPage from "./pages/settings/DropdownOptionsPage"; import TaskTemplatesPage from "./pages/settings/TaskTemplatesPage"; +// VENDOR PORTAL (public, token-authenticated, separate tree) +import VendorPortalLayout from "./pages/vendor-portal/VendorPortalLayout"; +import VendorPortalDashboard from "./pages/vendor-portal/DashboardPage"; +import VendorPortalDispatchDetail from "./pages/vendor-portal/DispatchDetailPage"; + // Protected Route Component function ProtectedRoute({ children }) { return isAuthenticated() ? children : ; @@ -68,6 +73,13 @@ export default function App() { {/* Public Routes */} } /> + {/* Vendor Portal — public, token-authenticated via URL */} + }> + } /> + } /> + } /> + + {/* Protected Routes */} d.status !== "Completed" && d.status !== "Verified" && d.status !== "Cancelled" }, + { key: "completed", label: "Completed", match: (d) => d.status === "Completed" || d.status === "Verified" }, + { key: "cancelled", label: "Cancelled", match: (d) => d.status === "Cancelled" }, + { key: "all", label: "All", match: () => true }, +]; + +export default function DashboardPage() { + const { token } = useVendorPortal(); + const navigate = useNavigate(); + const [dispatches, setDispatches] = useState([]); + const [loading, setLoading] = useState(true); + const [err, setErr] = useState(null); + const [filter, setFilter] = useState("open"); + + useEffect(() => { + setLoading(true); + vendorPortalApi + .listDispatches(token) + .then((data) => setDispatches(Array.isArray(data) ? data : [])) + .catch((e) => setErr(e.message)) + .finally(() => setLoading(false)); + }, [token]); + + const activeFilter = FILTERS.find((f) => f.key === filter) ?? FILTERS[0]; + const filtered = dispatches.filter(activeFilter.match); + + return ( + <> +
+

Your Dispatches

+
+ View and manage work orders assigned to your company. +
+
+ {FILTERS.map((f) => ( + + ))} +
+
+ + {loading &&
Loading dispatches…
} + {err &&
{err}
} + + {!loading && !err && filtered.length === 0 && ( +
No dispatches in this view.
+ )} + +
+ {filtered.map((d) => ( +
navigate(`../dispatch/${d.id}`)} + > +
+
+ {d.dispatchNumber} — {d.workOrderTitle || "(untitled)"} +
+
+ WO#{d.internalWONumber} + {d.locationName ? ` · ${d.locationName}` : ""} + {d.locationCity ? `, ${d.locationCity}` : ""} + {d.locationState ? `, ${d.locationState}` : ""} + {d.scheduledDate ? ` · Scheduled ${formatDateTime(d.scheduledDate)}` : ""} +
+
+ {d.status} +
+ ))} +
+ + ); +} diff --git a/src/pages/vendor-portal/DispatchDetailPage.js b/src/pages/vendor-portal/DispatchDetailPage.js new file mode 100644 index 00000000..bc9dc777 --- /dev/null +++ b/src/pages/vendor-portal/DispatchDetailPage.js @@ -0,0 +1,337 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { Link, useParams } from "react-router-dom"; +import { useVendorPortal } from "./VendorPortalContext"; +import { vendorPortalApi } from "./api"; +import { statusClass, formatDateTime } from "./statusHelpers"; +import SignaturePad from "./SignaturePad"; + +function ChecklistSection({ token, dispatchId, items, locked, onItemUpdated }) { + async function toggle(item) { + if (locked) return; + try { + const updated = await vendorPortalApi.updateChecklistItem(token, dispatchId, item.id, !item.isCompleted); + onItemUpdated(updated); + } catch (e) { + alert(e.message); + } + } + + if (!items?.length) { + return
No checklist items for this dispatch.
; + } + + return ( +
+ {items.map((item) => ( +
+ toggle(item)} + disabled={locked} + /> + + {item.isCompleted && item.completedAt && ( + {formatDateTime(item.completedAt)} + )} +
+ ))} +
+ ); +} + +function CommentsSection({ token, dispatchId, comments, onAdded }) { + const [text, setText] = useState(""); + const [submitting, setSubmitting] = useState(false); + + async function submit(e) { + e.preventDefault(); + if (!text.trim()) return; + setSubmitting(true); + try { + const added = await vendorPortalApi.addComment(token, dispatchId, text.trim()); + onAdded(added); + setText(""); + } catch (err) { + alert(err.message); + } finally { + setSubmitting(false); + } + } + + return ( + <> + {comments.length === 0 &&
No comments yet.
} + {comments.map((c) => ( +
+
+ {c.commenter || "Unknown"} · {formatDateTime(c.createdDate)} +
+
{c.commenttext}
+
+ ))} +
+