mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 08:03:13 +00:00
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:
parent
dc81078df7
commit
913d75802c
4 changed files with 122 additions and 13 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}>×</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>,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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))}>×</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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue