Add searchable vendor picker and scheduled date to dispatch modal

- Vendor list now has search input to filter by name or trade
- Selected vendors shown as removable chips below the list
- NTE and Vendor Scheduled Date side-by-side in the modal
- Sends ScheduledDate with dispatch request
This commit is contained in:
Adam Moussa 2026-04-17 11:47:19 -04:00
parent 0167a1aa75
commit 2a4cd5da75
2 changed files with 87 additions and 5 deletions

View file

@ -443,6 +443,59 @@
color: #333;
}
.woDispatchSearch {
width: 100%;
height: 36px;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 13px;
margin-bottom: 8px;
box-sizing: border-box;
}
.woDispatchSearch:focus {
border-color: #66afe9;
outline: none;
}
.woDispatchSelected {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
.woDispatchSelectedChip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 10px;
background: #e6f0ff;
border-radius: 12px;
font-size: 12px;
color: #007bff;
}
.woDispatchSelectedChip button {
background: none;
border: none;
color: #007bff;
cursor: pointer;
font-size: 14px;
padding: 0;
line-height: 1;
}
.woDispatchFieldRow {
display: flex;
gap: 12px;
}
.woDispatchFieldHalf {
flex: 1;
}
.woDispatchVendorList {
max-height: 200px;
overflow-y: auto;

View file

@ -164,7 +164,9 @@ function DispatchModal({ wo, onClose, onDispatched }) {
const [vendors, setVendors] = useState([]);
const [selectedIds, setSelectedIds] = useState([]);
const [nteAmount, setNteAmount] = useState(wo.vendorNTE || "");
const [scheduledDate, setScheduledDate] = useState("");
const [description, setDescription] = useState(wo.description || "");
const [vendorSearch, setVendorSearch] = useState("");
const [sending, setSending] = useState(false);
const [loaded, setLoaded] = useState(false);
@ -179,6 +181,11 @@ function DispatchModal({ wo, onClose, onDispatched }) {
setSelectedIds(prev => prev.includes(id) ? prev.filter(v => v !== id) : [...prev, id]);
};
const filteredVendors = vendors.filter((v) =>
(v.companyName || "").toLowerCase().includes(vendorSearch.toLowerCase()) ||
(v.tradeSpecialties || "").toLowerCase().includes(vendorSearch.toLowerCase())
);
const handleSubmit = async () => {
if (selectedIds.length === 0) { toast.error("Select at least one vendor"); return; }
setSending(true);
@ -186,6 +193,7 @@ function DispatchModal({ wo, onClose, onDispatched }) {
await dispatchToVendor(wo.id, {
VendorIds: selectedIds,
NTEAmount: nteAmount ? parseFloat(nteAmount) : null,
ScheduledDate: scheduledDate || null,
Description: description,
});
toast.success(`Dispatched to ${selectedIds.length} vendor(s)`);
@ -208,9 +216,16 @@ function DispatchModal({ wo, onClose, onDispatched }) {
<div className="woDispatchBody">
<div className="woDispatchField">
<label>Select Vendors</label>
{!loaded ? <div className="woDispatchLoading">Loading vendors...</div> : vendors.length === 0 ? <div className="woDispatchLoading">No vendors found</div> : (
<input
className="woDispatchSearch"
type="text"
value={vendorSearch}
onChange={(e) => setVendorSearch(e.target.value)}
placeholder="Search vendors..."
/>
{!loaded ? <div className="woDispatchLoading">Loading vendors...</div> : filteredVendors.length === 0 ? <div className="woDispatchLoading">No vendors found</div> : (
<div className="woDispatchVendorList">
{vendors.map((v) => (
{filteredVendors.map((v) => (
<label key={v.id} className={`woDispatchVendorItem ${selectedIds.includes(v.id) ? "selected" : ""}`}>
<input type="checkbox" checked={selectedIds.includes(v.id)} onChange={() => toggleVendor(v.id)} />
<div>
@ -221,10 +236,24 @@ function DispatchModal({ wo, onClose, onDispatched }) {
))}
</div>
)}
{selectedIds.length > 0 && (
<div className="woDispatchSelected">
{selectedIds.map((id) => {
const v = vendors.find((x) => x.id === id);
return v ? <span key={id} className="woDispatchSelectedChip">{v.companyName} <button onClick={() => toggleVendor(id)}>&times;</button></span> : null;
})}
</div>
)}
</div>
<div className="woDispatchField">
<label>NTE Amount</label>
<input type="number" className="form-control" value={nteAmount} onChange={(e) => setNteAmount(e.target.value)} placeholder="0.00" step="0.01" min="0" />
<div className="woDispatchFieldRow">
<div className="woDispatchField woDispatchFieldHalf">
<label>NTE Amount</label>
<input type="number" className="form-control" value={nteAmount} onChange={(e) => setNteAmount(e.target.value)} placeholder="0.00" step="0.01" min="0" />
</div>
<div className="woDispatchField woDispatchFieldHalf">
<label>Vendor Scheduled Date</label>
<input type="date" className="form-control" value={scheduledDate} onChange={(e) => setScheduledDate(e.target.value)} />
</div>
</div>
<div className="woDispatchField">
<label>Description</label>