Add multi-WO selection to dispatch modal

- Current WO shown as non-removable chip
- Dropdown shows all WOs at same site, filtered by locationId server-side
- Search bar filters the dropdown list
- Click to add, X to remove additional WOs
- Header shows count when multiple WOs selected
- All selected WO IDs sent to backend
This commit is contained in:
Adam Moussa 2026-04-17 16:13:18 -04:00
parent 199715d5b6
commit dc81078df7
2 changed files with 106 additions and 2 deletions

View file

@ -749,6 +749,60 @@
font-weight: 600;
}
.dispatchWOChips {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.dispatchWOChip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
background: #e9ecef;
border-radius: 4px;
font-size: 12px;
color: #333;
}
.dispatchWOChip button {
background: none;
border: none;
color: #999;
cursor: pointer;
font-size: 14px;
padding: 0;
line-height: 1;
}
.dispatchWOChip button:hover {
color: #dc3545;
}
.dispatchWODropdown {
border: 1px solid #ddd;
border-radius: 4px;
max-height: 160px;
overflow-y: auto;
margin-top: 4px;
}
.dispatchWOOption {
padding: 8px 12px;
font-size: 12px;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
}
.dispatchWOOption:hover {
background: #f0f7ff;
}
.dispatchWOOption:last-child {
border-bottom: none;
}
.dispatchVerifiedBanner {
padding: 12px 20px;
background: #d4edda;

View file

@ -529,6 +529,9 @@ function DispatchModal({ wo, onClose, onDispatched }) {
const [taskTemplates, setTaskTemplates] = useState([]);
const [selectedTemplateId, setSelectedTemplateId] = useState("");
const [loaded, setLoaded] = useState(false);
const [selectedWOs, setSelectedWOs] = useState([{ id: wo.id, number: wo.internalWONumber, title: wo.title }]);
const [siteWOs, setSiteWOs] = useState([]);
const [woSearch, setWoSearch] = useState("");
useEffect(() => {
fetchVendorsDropdown(wo.trade, wo.locationZip).then((data) => {
@ -536,6 +539,13 @@ function DispatchModal({ wo, onClose, onDispatched }) {
setLoaded(true);
}).catch(() => setLoaded(true));
fetchTaskTemplates().then(setTaskTemplates).catch(() => {});
if (wo.locationId) {
const { get: apiGet } = require("../../../apiUtil");
const { API_URL } = require("../../../constants");
apiGet(`${API_URL}/WorkOrder/GetWorkOrderList?search=&page=1&pageSize=500&sortBy=number&sortDir=asc&locationId=${wo.locationId}`).then((res) => {
setSiteWOs((res?.data || []).filter(w => w.id !== wo.id));
}).catch(() => {});
}
}, [wo.trade]);
const selectVendor = (id) => {
@ -552,7 +562,7 @@ function DispatchModal({ wo, onClose, onDispatched }) {
setSending(true);
try {
await dispatchToVendor({
WorkOrderIds: [wo.id],
WorkOrderIds: selectedWOs.map(w => w.id),
VendorIds: [selectedId],
NTEAmount: nteAmount ? parseFloat(nteAmount) : null,
ScheduledDate: scheduledDate || null,
@ -573,10 +583,50 @@ function DispatchModal({ wo, onClose, onDispatched }) {
<div className="woDispatchOverlay" onClick={onClose}>
<div className="woDispatchModal" onClick={(e) => e.stopPropagation()}>
<div className="woDispatchHeader">
<h3>Dispatch Work Order {wo.internalWONumber}</h3>
<h3>Dispatch {selectedWOs.length > 1 ? `${selectedWOs.length} Work Orders` : `Work Order ${wo.internalWONumber}`}</h3>
<button className="woDispatchClose" onClick={onClose}>&times;</button>
</div>
<div className="woDispatchBody">
<div className="woDispatchField">
<label>Work Orders ({selectedWOs.length})</label>
<div className="dispatchWOChips">
{selectedWOs.map((w) => (
<span key={w.id} className="dispatchWOChip">
{w.number} — {w.title}
{w.id !== wo.id && <button onClick={() => setSelectedWOs(prev => prev.filter(p => p.id !== w.id))}>&times;</button>}
</span>
))}
</div>
{siteWOs.length > 0 && (
<>
<input
className="woDispatchSearch"
value={woSearch}
onChange={(e) => setWoSearch(e.target.value)}
placeholder="Search to filter..."
style={{ marginTop: 8 }}
/>
<div className="dispatchWODropdown">
{siteWOs
.filter(w => !selectedWOs.find(s => s.id === w.id))
.filter(w => !woSearch || (w.number || "").includes(woSearch) || (w.title || "").toLowerCase().includes(woSearch.toLowerCase()))
.slice(0, 10)
.map(w => (
<div key={w.id} className="dispatchWOOption" onClick={() => {
setSelectedWOs(prev => [...prev, { id: w.id, number: w.number, title: w.title }]);
setWoSearch("");
}}>
<strong>{w.number}</strong> — {w.title}
</div>
))
}
{siteWOs.filter(w => !selectedWOs.find(s => s.id === w.id)).length === 0 && (
<div className="dispatchWOOption" style={{ color: "#999", cursor: "default" }}>All work orders at this site are selected</div>
)}
</div>
</>
)}
</div>
<div className="woDispatchField">
<label>Select Vendors</label>
<input