Add Verify & Close button and verified banner to dispatch detail

- Verify & Close button at bottom of dispatch modal
- Disabled with tooltip showing missing items (checklist, signoffs)
- Green verified banner after verification with verifier name + timestamp
- Verified status badge (dark green)
- Added Verified to DISPATCH_STATUSES constant
- verifyDispatch API function
This commit is contained in:
Adam Moussa 2026-04-17 16:03:34 -04:00
parent 5c24b9249d
commit 199715d5b6
3 changed files with 78 additions and 2 deletions

View file

@ -134,6 +134,10 @@ export const addChecklistItem = async (dispatchId, itemText, workOrderId) => {
return post(`${API_URL}/WorkOrder/AddChecklistItem`, { DispatchId: dispatchId, ItemText: itemText, WorkOrderId: workOrderId });
};
export const verifyDispatch = async (dispatchId) => {
return post(`${API_URL}/WorkOrder/VerifyDispatch?dispatchId=${dispatchId}`);
};
export const addDispatchSignoff = async (dispatchId, signoffType, name, signature, signatureMethod) => {
return post(`${API_URL}/WorkOrder/AddDispatchSignoff`, {
DispatchId: dispatchId,

View file

@ -629,6 +629,7 @@
.woDispatchStatus.acknowledged { background: #ffc107; color: #333; }
.woDispatchStatus.completed { background: #28a745; }
.woDispatchStatus.cancelled { background: #dc3545; }
.woDispatchStatus.verified { background: #155724; }
.dispatchClickRow {
cursor: pointer;
@ -748,6 +749,34 @@
font-weight: 600;
}
.dispatchVerifiedBanner {
padding: 12px 20px;
background: #d4edda;
color: #155724;
font-size: 13px;
font-weight: 600;
text-align: center;
border-top: 1px solid #c3e6cb;
flex-shrink: 0;
}
.dispatchVerifyFooter {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 12px;
padding: 12px 20px;
border-top: 1px solid #eee;
background: #fafafa;
flex-shrink: 0;
}
.dispatchVerifyMissing {
font-size: 12px;
color: #999;
flex: 1;
}
.dispatchSignoffs {
display: flex;
gap: 16px;

View file

@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from "react";
import ReactDOM from "react-dom";
import { useParams, useNavigate } from "react-router";
import { toast } from "react-toastify";
import { getWorkOrderById, addComment, changeWorkOrderStatus, fetchUsers, changeWorkOrderAssignment, fetchVendorsDropdown, dispatchToVendor, getDispatchById, updateDispatch, addDispatchComment, fetchTaskTemplates, updateChecklistItem, addChecklistItem, addDispatchSignoff } from "../api";
import { getWorkOrderById, addComment, changeWorkOrderStatus, fetchUsers, changeWorkOrderAssignment, fetchVendorsDropdown, dispatchToVendor, getDispatchById, updateDispatch, addDispatchComment, fetchTaskTemplates, updateChecklistItem, addChecklistItem, addDispatchSignoff, verifyDispatch } from "../api";
import { getCurrentUser } from "../../../services/authService";
import { timeSince, timeBetween, waitTimeColor } from "../../../utils/timeSince";
import SignatureCapture from "../../../components/SignatureCapture";
@ -164,7 +164,7 @@ function AuditTimeline({ entries }) {
);
}
const DISPATCH_STATUSES = ["Sent", "Acknowledged", "In Progress", "Completed", "Cancelled"];
const DISPATCH_STATUSES = ["Sent", "Acknowledged", "In Progress", "Completed", "Verified", "Cancelled"];
function DispatchDetailModal({ dispatchId, onClose, onUpdated }) {
const [dispatch, setDispatch] = useState(null);
@ -468,6 +468,49 @@ function DispatchDetailModal({ dispatchId, onClose, onUpdated }) {
</form>
</div>
</div>
{dispatch.verifiedAt ? (
<div className="dispatchVerifiedBanner">
✓ Verified by {dispatch.verifiedBy} on {formatDateTime(dispatch.verifiedAt)}
</div>
) : (
<div className="dispatchVerifyFooter">
{(() => {
const checklistTotal = (dispatch.checklistItems || []).length;
const checklistDone = (dispatch.checklistItems || []).filter(i => i.isCompleted).length;
const signoffCount = (dispatch.signoffs || []).length;
const missing = [];
if (checklistTotal > 0 && checklistDone < checklistTotal) missing.push(`${checklistTotal - checklistDone} checklist item(s)`);
if (signoffCount < 2) {
const types = (dispatch.signoffs || []).map(s => s.signoffType);
if (!types.includes("Customer")) missing.push("Customer sign-off");
if (!types.includes("Vendor")) missing.push("Vendor sign-off");
}
const canVerify = missing.length === 0 && (checklistTotal > 0 || signoffCount >= 2);
return (
<>
{missing.length > 0 && <span className="dispatchVerifyMissing">Missing: {missing.join(", ")}</span>}
<button
className="btn btn-success"
disabled={!canVerify}
onClick={async () => {
try {
const result = await verifyDispatch(dispatchId);
toast.success("Dispatch verified");
loadDispatch();
onUpdated();
} catch (err) {
toast.error(err.message || "Failed to verify");
}
}}
>
Verify &amp; Close
</button>
</>
);
})()}
</div>
)}
</div>
</div>
</div>,