Polish dispatch workflow — custom items, name input, read-only after verify

- Custom checklist items in dispatch modal (type + Enter, removable chips)
- Add checklist item input in dispatch detail modal
- SignatureCapture now has proper Full Name field instead of prompt()
- Verified dispatches are read-only: edit hidden, checkboxes disabled, signoff buttons disabled
- Custom items sent as CustomChecklistItems in dispatch payload
This commit is contained in:
Adam Moussa 2026-04-17 16:26:22 -04:00
parent dc81078df7
commit 913d75802c
4 changed files with 122 additions and 13 deletions

View file

@ -40,6 +40,18 @@
border-bottom: 2px solid #e5e5e5;
}
.sigNameField {
margin-bottom: 12px;
}
.sigNameField label {
display: block;
font-size: 12px;
font-weight: 600;
color: #666;
margin-bottom: 4px;
}
.sigModeTab {
padding: 6px 16px;
border: none;

View file

@ -4,6 +4,7 @@ import ReactDOM from "react-dom";
export default function SignatureCapture({ onSave, onClose, title }) {
const [mode, setMode] = useState("draw");
const [typedName, setTypedName] = useState("");
const [signerName, setSignerName] = useState("");
const canvasRef = useRef(null);
const isDrawing = useRef(false);
@ -52,11 +53,12 @@ export default function SignatureCapture({ onSave, onClose, title }) {
};
const handleSave = () => {
if (!signerName.trim()) return;
if (mode === "draw") {
const dataUrl = canvasRef.current.toDataURL("image/png");
onSave(dataUrl, "drawn");
onSave(dataUrl, "drawn", signerName.trim());
} else {
onSave(typedName, "typed");
onSave(typedName, "typed", signerName.trim());
}
};
@ -68,6 +70,15 @@ export default function SignatureCapture({ onSave, onClose, title }) {
<button className="woDispatchClose" onClick={onClose}>&times;</button>
</div>
<div className="sigBody">
<div className="sigNameField">
<label>Full Name</label>
<input
className="form-control"
value={signerName}
onChange={(e) => setSignerName(e.target.value)}
placeholder="Enter signer's full name"
/>
</div>
<div className="sigModeTabs">
<button className={`sigModeTab ${mode === "draw" ? "active" : ""}`} onClick={() => setMode("draw")}>Draw</button>
<button className={`sigModeTab ${mode === "type" ? "active" : ""}`} onClick={() => setMode("type")}>Type</button>
@ -103,7 +114,7 @@ export default function SignatureCapture({ onSave, onClose, title }) {
</div>
<div className="sigFooter">
<button className="btn btn-default" onClick={onClose}>Cancel</button>
<button className="btn btn-primary" onClick={handleSave} disabled={mode === "type" && !typedName.trim()}>Save Signature</button>
<button className="btn btn-primary" onClick={handleSave} disabled={!signerName.trim() || (mode === "type" && !typedName.trim())}>Save Signature</button>
</div>
</div>
</div>,

View file

@ -803,6 +803,43 @@
border-bottom: none;
}
.dispatchChecklistAdd {
margin-top: 8px;
}
.dispatchChecklistAdd .form-control {
font-size: 13px;
}
.dispatchCustomItems {
display: flex;
flex-direction: column;
gap: 4px;
margin-top: 6px;
}
.dispatchCustomItem {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 8px;
background: #f8f9fa;
border-radius: 4px;
font-size: 12px;
}
.dispatchCustomItem button {
background: none;
border: none;
color: #999;
cursor: pointer;
font-size: 14px;
}
.dispatchCustomItem button:hover {
color: #dc3545;
}
.dispatchVerifiedBanner {
padding: 12px 20px;
background: #d4edda;

View file

@ -170,6 +170,7 @@ function DispatchDetailModal({ dispatchId, onClose, onUpdated }) {
const [dispatch, setDispatch] = useState(null);
const [loading, setLoading] = useState(true);
const [editing, setEditing] = useState(false);
const [newChecklistItem, setNewChecklistItem] = useState("");
const [form, setForm] = useState({});
const [saving, setSaving] = useState(false);
const [commentText, setCommentText] = useState("");
@ -336,7 +337,7 @@ function DispatchDetailModal({ dispatchId, onClose, onUpdated }) {
</div>
)}
<div className="dispatchDetailActions">
<button className="btn btn-primary" onClick={() => setEditing(true)}>Edit</button>
{!dispatch.verifiedAt && <button className="btn btn-primary" onClick={() => setEditing(true)}>Edit</button>}
</div>
</>
)}
@ -344,15 +345,16 @@ function DispatchDetailModal({ dispatchId, onClose, onUpdated }) {
</div>
<div className="dispatchDetailSection">
{dispatch.checklistItems && dispatch.checklistItems.length > 0 && (
{((dispatch.checklistItems && dispatch.checklistItems.length > 0) || !dispatch.verifiedAt) && (
<>
<h4>Checklist ({dispatch.checklistItems.filter(i => i.isCompleted).length}/{dispatch.checklistItems.length})</h4>
<h4>Checklist ({(dispatch.checklistItems || []).filter(i => i.isCompleted).length}/{(dispatch.checklistItems || []).length})</h4>
<div className="dispatchChecklist">
{dispatch.checklistItems.map((item) => (
{(dispatch.checklistItems || []).map((item) => (
<div key={item.id} className={`dispatchChecklistItem ${item.isCompleted ? "completed" : ""}`}>
<input
type="checkbox"
checked={item.isCompleted}
disabled={!!dispatch.verifiedAt}
onChange={async () => {
const currentUser = getCurrentUser();
const userName = currentUser?.fullname || "User";
@ -374,6 +376,27 @@ function DispatchDetailModal({ dispatchId, onClose, onUpdated }) {
</div>
))}
</div>
{!dispatch.verifiedAt && (
<div className="dispatchChecklistAdd">
<input
className="form-control"
value={newChecklistItem}
onChange={(e) => setNewChecklistItem(e.target.value)}
onKeyDown={async (e) => {
if (e.key === "Enter" && newChecklistItem.trim()) {
e.preventDefault();
const result = await addChecklistItem(dispatchId, newChecklistItem.trim());
setDispatch((prev) => ({
...prev,
checklistItems: [...(prev.checklistItems || []), { ...result, isCompleted: false }],
}));
setNewChecklistItem("");
}
}}
placeholder="Add checklist item..."
/>
</div>
)}
</>
)}
<h4>Sign-offs</h4>
@ -394,8 +417,8 @@ function DispatchDetailModal({ dispatchId, onClose, onUpdated }) {
<span className="dispatchSignoffMeta">{signoff.name} · {signoff.signedAt ? formatDateTime(signoff.signedAt) : ""}</span>
</div>
) : (
<button className="btn btn-default dispatchSignoffBtn" onClick={() => setSigCapture(type)}>
Collect Signature
<button className="btn btn-default dispatchSignoffBtn" onClick={() => setSigCapture(type)} disabled={!!dispatch.verifiedAt}>
{dispatch.verifiedAt ? "Not signed" : "Collect Signature"}
</button>
)}
</div>
@ -408,9 +431,7 @@ function DispatchDetailModal({ dispatchId, onClose, onUpdated }) {
<SignatureCapture
title={`${sigCapture} Sign-off`}
onClose={() => setSigCapture(null)}
onSave={async (signature, method) => {
const name = prompt(`Enter ${sigCapture.toLowerCase()} name:`);
if (!name) return;
onSave={async (signature, method, name) => {
try {
const result = await addDispatchSignoff(dispatchId, sigCapture, name, signature, method);
setDispatch((prev) => ({
@ -528,6 +549,8 @@ function DispatchModal({ wo, onClose, onDispatched }) {
const [sending, setSending] = useState(false);
const [taskTemplates, setTaskTemplates] = useState([]);
const [selectedTemplateId, setSelectedTemplateId] = useState("");
const [customItems, setCustomItems] = useState([]);
const [newCustomItem, setNewCustomItem] = useState("");
const [loaded, setLoaded] = useState(false);
const [selectedWOs, setSelectedWOs] = useState([{ id: wo.id, number: wo.internalWONumber, title: wo.title }]);
const [siteWOs, setSiteWOs] = useState([]);
@ -568,6 +591,7 @@ function DispatchModal({ wo, onClose, onDispatched }) {
ScheduledDate: scheduledDate || null,
Description: description,
TaskListTemplateId: selectedTemplateId ? parseInt(selectedTemplateId) : null,
CustomChecklistItems: customItems.length > 0 ? customItems : null,
});
toast.success("Dispatched successfully");
onDispatched();
@ -678,11 +702,36 @@ function DispatchModal({ wo, onClose, onDispatched }) {
<div className="woDispatchField">
<label>Task List Template</label>
<select className="form-control" value={selectedTemplateId} onChange={(e) => setSelectedTemplateId(e.target.value)}>
<option value="">No checklist</option>
<option value="">No checklist template</option>
{taskTemplates.map((t) => (
<option key={t.id} value={t.id}>{t.name} ({t.items?.length || 0} items)</option>
))}
</select>
{customItems.length > 0 && (
<div className="dispatchCustomItems">
{customItems.map((item, idx) => (
<div key={idx} className="dispatchCustomItem">
<span>{item}</span>
<button onClick={() => setCustomItems(prev => prev.filter((_, i) => i !== idx))}>&times;</button>
</div>
))}
</div>
)}
<div className="dispatchChecklistAdd" style={{ marginTop: 6 }}>
<input
className="form-control"
value={newCustomItem}
onChange={(e) => setNewCustomItem(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && newCustomItem.trim()) {
e.preventDefault();
setCustomItems(prev => [...prev, newCustomItem.trim()]);
setNewCustomItem("");
}
}}
placeholder="Add custom checklist item..."
/>
</div>
</div>
<div className="woDispatchField">
<label>Description</label>