mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-07 11:39:00 +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}` : ""}`)) || [];
|
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) => {
|
export const getDispatchById = async (id) => {
|
||||||
const { get: apiGet } = await import("../../apiUtil");
|
const { get: apiGet } = await import("../../apiUtil");
|
||||||
return apiGet(`${API_URL}/WorkOrder/GetDispatch/${id}`);
|
return apiGet(`${API_URL}/WorkOrder/GetDispatch/${id}`);
|
||||||
|
|
|
||||||
|
|
@ -734,6 +734,48 @@
|
||||||
padding-top: 4px;
|
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 {
|
.dispatchThreadContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@ import React, { useState, useEffect, useRef } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { useParams, useNavigate } from "react-router";
|
import { useParams, useNavigate } from "react-router";
|
||||||
import { toast } from "react-toastify";
|
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";
|
import "./WorkOrderViewPage.css";
|
||||||
|
|
||||||
function AssignDropdown({ value, onAssign }) {
|
function AssignDropdown({ value, onAssign }) {
|
||||||
|
|
@ -329,6 +330,38 @@ function DispatchDetailModal({ dispatchId, onClose, onUpdated }) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="dispatchDetailSection">
|
<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>
|
<h4>Vendor Communication</h4>
|
||||||
<div className="dispatchThreadContainer">
|
<div className="dispatchThreadContainer">
|
||||||
<div className="dispatchThreadScroll">
|
<div className="dispatchThreadScroll">
|
||||||
|
|
@ -386,6 +419,8 @@ function DispatchModal({ wo, onClose, onDispatched }) {
|
||||||
const [description, setDescription] = useState(wo.description || "");
|
const [description, setDescription] = useState(wo.description || "");
|
||||||
const [vendorSearch, setVendorSearch] = useState("");
|
const [vendorSearch, setVendorSearch] = useState("");
|
||||||
const [sending, setSending] = useState(false);
|
const [sending, setSending] = useState(false);
|
||||||
|
const [taskTemplates, setTaskTemplates] = useState([]);
|
||||||
|
const [selectedTemplateId, setSelectedTemplateId] = useState("");
|
||||||
const [loaded, setLoaded] = useState(false);
|
const [loaded, setLoaded] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -393,6 +428,7 @@ function DispatchModal({ wo, onClose, onDispatched }) {
|
||||||
setVendors(data);
|
setVendors(data);
|
||||||
setLoaded(true);
|
setLoaded(true);
|
||||||
}).catch(() => setLoaded(true));
|
}).catch(() => setLoaded(true));
|
||||||
|
fetchTaskTemplates().then(setTaskTemplates).catch(() => {});
|
||||||
}, [wo.trade]);
|
}, [wo.trade]);
|
||||||
|
|
||||||
const selectVendor = (id) => {
|
const selectVendor = (id) => {
|
||||||
|
|
@ -414,6 +450,7 @@ function DispatchModal({ wo, onClose, onDispatched }) {
|
||||||
NTEAmount: nteAmount ? parseFloat(nteAmount) : null,
|
NTEAmount: nteAmount ? parseFloat(nteAmount) : null,
|
||||||
ScheduledDate: scheduledDate || null,
|
ScheduledDate: scheduledDate || null,
|
||||||
Description: description,
|
Description: description,
|
||||||
|
TaskListTemplateId: selectedTemplateId ? parseInt(selectedTemplateId) : null,
|
||||||
});
|
});
|
||||||
toast.success("Dispatched successfully");
|
toast.success("Dispatched successfully");
|
||||||
onDispatched();
|
onDispatched();
|
||||||
|
|
@ -481,6 +518,15 @@ function DispatchModal({ wo, onClose, onDispatched }) {
|
||||||
<input type="date" className="form-control" value={scheduledDate} onChange={(e) => setScheduledDate(e.target.value)} />
|
<input type="date" className="form-control" value={scheduledDate} onChange={(e) => setScheduledDate(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div className="woDispatchField">
|
||||||
<label>Description</label>
|
<label>Description</label>
|
||||||
<textarea className="form-control" value={description} onChange={(e) => setDescription(e.target.value)} rows="4" style={{ resize: "none" }} />
|
<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