mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 08:03:13 +00:00
Add checklist UI to dispatch workflow
- Task List Template picker in dispatch modal - Checklist section in dispatch detail modal with toggleable items - Shows completion count, completed by user name + timestamp - Strikethrough styling on completed items - fetchTaskTemplates, updateChecklistItem, addChecklistItem API functions
This commit is contained in:
parent
714376c61b
commit
62bc2f058c
3 changed files with 102 additions and 1 deletions
|
|
@ -121,6 +121,19 @@ export const fetchVendorsDropdown = async (trade, siteZip) => {
|
|||
return (await apiGet(`${API_URL}/Vendor/Dropdown${qs ? `?${qs}` : ""}`)) || [];
|
||||
};
|
||||
|
||||
export const fetchTaskTemplates = async () => {
|
||||
const { get: apiGet } = await import("../../apiUtil");
|
||||
return (await apiGet(`${API_URL}/TaskListTemplate`)) || [];
|
||||
};
|
||||
|
||||
export const updateChecklistItem = async (id, isCompleted, completedBy) => {
|
||||
return post(`${API_URL}/WorkOrder/UpdateChecklistItem`, { Id: id, IsCompleted: isCompleted, CompletedBy: completedBy });
|
||||
};
|
||||
|
||||
export const addChecklistItem = async (dispatchId, itemText, workOrderId) => {
|
||||
return post(`${API_URL}/WorkOrder/AddChecklistItem`, { DispatchId: dispatchId, ItemText: itemText, WorkOrderId: workOrderId });
|
||||
};
|
||||
|
||||
export const getDispatchById = async (id) => {
|
||||
const { get: apiGet } = await import("../../apiUtil");
|
||||
return apiGet(`${API_URL}/WorkOrder/GetDispatch/${id}`);
|
||||
|
|
|
|||
|
|
@ -734,6 +734,48 @@
|
|||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.dispatchChecklist {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.dispatchChecklistItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.dispatchChecklistItem:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.dispatchChecklistItem.completed .dispatchChecklistText {
|
||||
text-decoration: line-through;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.dispatchChecklistText {
|
||||
flex: 1;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.dispatchChecklistDone {
|
||||
font-size: 11px;
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.dispatchChecklistItem input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dispatchThreadContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ 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 } from "../api";
|
||||
import { getWorkOrderById, addComment, changeWorkOrderStatus, fetchUsers, changeWorkOrderAssignment, fetchVendorsDropdown, dispatchToVendor, getDispatchById, updateDispatch, addDispatchComment, fetchTaskTemplates, updateChecklistItem, addChecklistItem } from "../api";
|
||||
import { getCurrentUser } from "../../../services/authService";
|
||||
import "./WorkOrderViewPage.css";
|
||||
|
||||
function AssignDropdown({ value, onAssign }) {
|
||||
|
|
@ -329,6 +330,38 @@ function DispatchDetailModal({ dispatchId, onClose, onUpdated }) {
|
|||
</div>
|
||||
|
||||
<div className="dispatchDetailSection">
|
||||
{dispatch.checklistItems && dispatch.checklistItems.length > 0 && (
|
||||
<>
|
||||
<h4>Checklist ({dispatch.checklistItems.filter(i => i.isCompleted).length}/{dispatch.checklistItems.length})</h4>
|
||||
<div className="dispatchChecklist">
|
||||
{dispatch.checklistItems.map((item) => (
|
||||
<div key={item.id} className={`dispatchChecklistItem ${item.isCompleted ? "completed" : ""}`}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={item.isCompleted}
|
||||
onChange={async () => {
|
||||
const currentUser = getCurrentUser();
|
||||
const userName = currentUser?.fullname || "User";
|
||||
await updateChecklistItem(item.id, !item.isCompleted, userName);
|
||||
setDispatch((prev) => ({
|
||||
...prev,
|
||||
checklistItems: prev.checklistItems.map((ci) =>
|
||||
ci.id === item.id ? { ...ci, isCompleted: !ci.isCompleted, completedBy: !ci.isCompleted ? userName : null, completedAt: !ci.isCompleted ? new Date().toISOString() : null } : ci
|
||||
),
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<span className="dispatchChecklistText">{item.itemText}</span>
|
||||
{item.isCompleted && (
|
||||
<span className="dispatchChecklistDone">
|
||||
{item.completedBy}{item.completedAt ? ` · ${formatDateTime(item.completedAt)}` : ""}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<h4>Vendor Communication</h4>
|
||||
<div className="dispatchThreadContainer">
|
||||
<div className="dispatchThreadScroll">
|
||||
|
|
@ -386,6 +419,8 @@ function DispatchModal({ wo, onClose, onDispatched }) {
|
|||
const [description, setDescription] = useState(wo.description || "");
|
||||
const [vendorSearch, setVendorSearch] = useState("");
|
||||
const [sending, setSending] = useState(false);
|
||||
const [taskTemplates, setTaskTemplates] = useState([]);
|
||||
const [selectedTemplateId, setSelectedTemplateId] = useState("");
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -393,6 +428,7 @@ function DispatchModal({ wo, onClose, onDispatched }) {
|
|||
setVendors(data);
|
||||
setLoaded(true);
|
||||
}).catch(() => setLoaded(true));
|
||||
fetchTaskTemplates().then(setTaskTemplates).catch(() => {});
|
||||
}, [wo.trade]);
|
||||
|
||||
const selectVendor = (id) => {
|
||||
|
|
@ -414,6 +450,7 @@ function DispatchModal({ wo, onClose, onDispatched }) {
|
|||
NTEAmount: nteAmount ? parseFloat(nteAmount) : null,
|
||||
ScheduledDate: scheduledDate || null,
|
||||
Description: description,
|
||||
TaskListTemplateId: selectedTemplateId ? parseInt(selectedTemplateId) : null,
|
||||
});
|
||||
toast.success("Dispatched successfully");
|
||||
onDispatched();
|
||||
|
|
@ -481,6 +518,15 @@ function DispatchModal({ wo, onClose, onDispatched }) {
|
|||
<input type="date" className="form-control" value={scheduledDate} onChange={(e) => setScheduledDate(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
{taskTemplates.map((t) => (
|
||||
<option key={t.id} value={t.id}>{t.name} ({t.items?.length || 0} items)</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="woDispatchField">
|
||||
<label>Description</label>
|
||||
<textarea className="form-control" value={description} onChange={(e) => setDescription(e.target.value)} rows="4" style={{ resize: "none" }} />
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue