updates on the create order design and select location updates
This commit is contained in:
@@ -96,6 +96,25 @@ export default function Deliveries() {
|
||||
const notifyMiler = useNotifyMiler();
|
||||
const { selectedZone, isAllZones, matchesZone } = useZone();
|
||||
|
||||
const riderLookup = useMemo(() => {
|
||||
const map = new Map();
|
||||
(riders || []).forEach((r) => {
|
||||
const name =
|
||||
r.displayname ||
|
||||
[r.firstname, r.lastname].filter(Boolean).join(' ').trim() ||
|
||||
r.name ||
|
||||
r.authname ||
|
||||
(r.label ? r.label.split('|')[0].trim() : '');
|
||||
const phone = r.contactno || r.phone || '';
|
||||
if (r.userid != null) map.set(String(r.userid), { name, phone });
|
||||
if (r.milerid != null) map.set(String(r.milerid), { name, phone });
|
||||
if (r.milerprofileid != null) map.set(String(r.milerprofileid), { name, phone });
|
||||
if (r.id != null) map.set(String(r.id), { name, phone });
|
||||
if (r.value != null) map.set(String(r.value), { name, phone });
|
||||
});
|
||||
return map;
|
||||
}, [riders]);
|
||||
|
||||
const allRows = useMemo(() => {
|
||||
return [...(data?.rows || [])].sort((a, b) => {
|
||||
const ta = parseDoormileTimestamp(a.orderdate || a.assigntime).valueOf() || 0;
|
||||
@@ -138,15 +157,14 @@ export default function Deliveries() {
|
||||
}, [batchRows, unmapped]);
|
||||
|
||||
const rows = useMemo(() => {
|
||||
const scoped =
|
||||
tab === 'all'
|
||||
? batchRows
|
||||
: tab === 'other'
|
||||
? unmapped
|
||||
: batchRows.filter((row) => {
|
||||
const status = String(row.orderstatus || '').toLowerCase();
|
||||
return tab === 'cancelled' ? status === 'cancelled' || status === 'canceled' : status === tab;
|
||||
});
|
||||
let scoped = batchRows;
|
||||
if (tab === 'other') scoped = unmapped;
|
||||
else if (tab !== 'all') {
|
||||
scoped = batchRows.filter((row) => {
|
||||
const status = String(row.orderstatus || '').toLowerCase();
|
||||
return tab === 'cancelled' ? status === 'cancelled' || status === 'canceled' : status === tab;
|
||||
});
|
||||
}
|
||||
|
||||
return scoped.filter((row) =>
|
||||
matchesQuery(
|
||||
@@ -213,12 +231,24 @@ export default function Deliveries() {
|
||||
key: 'ridername',
|
||||
header: 'Rider',
|
||||
sortable: true,
|
||||
cell: (row) => (
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-body-sm text-ink-1">{orDash(row.ridername)}</p>
|
||||
<p className="text-caption text-ink-3">{orDash(row.ridercontact)}</p>
|
||||
</div>
|
||||
),
|
||||
cell: (row) => {
|
||||
const riderKey = String(row.userid || row.milerprofileid || '');
|
||||
const info = riderLookup.get(riderKey);
|
||||
const name =
|
||||
(info?.name && !info.name.startsWith('Rider #'))
|
||||
? info.name
|
||||
: (row.ridername && !row.ridername.startsWith('Rider #'))
|
||||
? row.ridername
|
||||
: (info?.name || row.ridername || (row.userid ? `Rider #${row.userid}` : '—'));
|
||||
const contact = info?.phone || row.ridercontact;
|
||||
|
||||
return (
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-body-sm font-semibold text-ink-1">{name}</p>
|
||||
<p className="text-caption text-ink-3">{orDash(contact)}</p>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
hideBelow: 'md',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import AddressAutocomplete, { geocodeAddress } from '@/components/doormile/AddressAutocomplete';
|
||||
import { HubLocationSelect } from '@/components/doormile/HubLocationSelect';
|
||||
import {
|
||||
createExpressBooking,
|
||||
getAdminCustomers,
|
||||
@@ -31,9 +32,20 @@ import {
|
||||
import { calculateDrivingDistance, calculateTotalCharge, getLastRouteDurationMin } from '@/lib/distance';
|
||||
import { OpenToast } from '@/components/third-party/OpenToast';
|
||||
|
||||
const SERVICE_OPTIONS = ['Normal', 'Express', 'Scheduled'];
|
||||
const SERVICE_OPTIONS = ['Normal', 'Fast', 'Superfast'];
|
||||
const ITEM_CATEGORIES = ['General', 'Food & Beverage', 'Grocery', 'Pharmacy', 'Documents', 'Electronics'];
|
||||
|
||||
const WEIGHT_OPTIONS = [
|
||||
{ value: '1 - 10 kg', label: '1 to 10 kg' },
|
||||
{ value: '10 - 20 kg', label: '10 to 20 kg' },
|
||||
{ value: '20 - 30 kg', label: '20 to 30 kg' },
|
||||
{ value: '30 - 50 kg', label: '30 to 50 kg' },
|
||||
{ value: '50+ kg', label: '50+ kg' },
|
||||
{ value: 'custom', label: 'Custom weight…' }
|
||||
];
|
||||
|
||||
const COUNT_OPTIONS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
|
||||
const EMPTY_PARTY = {
|
||||
firstname: '',
|
||||
contactno: '',
|
||||
@@ -46,7 +58,7 @@ const EMPTY_PARTY = {
|
||||
longitude: ''
|
||||
};
|
||||
|
||||
const EMPTY_PACKAGE = { category: 'General', weight: '', description: '', count: 1 };
|
||||
const EMPTY_PACKAGE = { category: 'General', weight: '1 - 10 kg', count: 1, isCustomWeight: false, isCustomCount: false };
|
||||
|
||||
const buildDefaultPickupSlots = () => {
|
||||
const arr = [];
|
||||
@@ -331,21 +343,16 @@ export default function CreateOrder() {
|
||||
const deliverytime = dayjs(`${pickupDate} ${selectedSlotTime}`, 'YYYY-MM-DD HH:mm').format('YYYY-MM-DD HH:mm:ss');
|
||||
const finalprice = Number(totalCharge.toFixed(2)) || 0;
|
||||
|
||||
const parcels = packages.some((pkg) => pkg.category || pkg.description)
|
||||
? packages.flatMap((pkg) =>
|
||||
Array.from({ length: Math.max(1, Number(pkg.count) || 1) }, () => ({
|
||||
itemcategory: pkg.category || 'General',
|
||||
itemdescription: [pkg.description, pkg.weight ? `${pkg.weight}kg` : ''].filter(Boolean).join(' · ') || 'Order',
|
||||
declaredvalue: finalprice
|
||||
}))
|
||||
)
|
||||
: [
|
||||
{
|
||||
itemcategory: 'General',
|
||||
itemdescription: 'Order',
|
||||
declaredvalue: finalprice
|
||||
}
|
||||
];
|
||||
const parcels = packages.flatMap((pkg) => {
|
||||
const weightStr = pkg.weight ? (String(pkg.weight).toLowerCase().includes('kg') ? pkg.weight : `${pkg.weight} kg`) : '1 - 10 kg';
|
||||
const countNum = Math.max(1, Number(pkg.count) || 1);
|
||||
const desc = `${pkg.category || 'Package'} (${weightStr})`;
|
||||
return Array.from({ length: countNum }, () => ({
|
||||
itemcategory: pkg.category || 'General',
|
||||
itemdescription: desc,
|
||||
declaredvalue: finalprice
|
||||
}));
|
||||
});
|
||||
|
||||
const payload = {
|
||||
tenantid: Number(selectedTenantId),
|
||||
@@ -362,7 +369,7 @@ export default function CreateOrder() {
|
||||
deliverylatitude: Number(dropCust.latitude) || 0,
|
||||
deliverylongitude: Number(dropCust.longitude) || 0,
|
||||
deliverytime,
|
||||
service_option: serviceOption,
|
||||
service_option: SERVICE_OPTIONS.includes(serviceOption) ? serviceOption : 'Normal',
|
||||
finalprice,
|
||||
notes: notes || '',
|
||||
parcels
|
||||
@@ -380,7 +387,8 @@ export default function CreateOrder() {
|
||||
OpenToast(res?.message || 'Failed to create order', 'error', 3500);
|
||||
}
|
||||
} catch (err) {
|
||||
OpenToast(err?.message || 'Cannot create order. Please check inputs.', 'error', 3500);
|
||||
const serverMsg = err?.response?.data?.message || err?.message || 'Cannot create order. Please check inputs.';
|
||||
OpenToast(serverMsg, 'error', 4000);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
@@ -416,27 +424,13 @@ export default function CreateOrder() {
|
||||
whole page sideways. */}
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
{/* Header Hub Quick Picker */}
|
||||
<div className="flex items-center gap-2 bg-slate-50 border border-slate-200 rounded-lg px-3 py-1.5 shadow-2xs">
|
||||
<Building2 className="w-4 h-4 text-slate-400 shrink-0" />
|
||||
<div className="text-[11px] font-semibold text-slate-500 shrink-0">Hub:</div>
|
||||
<select
|
||||
value={selectedLocationId}
|
||||
onChange={(e) => handleSelectLocation(e.target.value)}
|
||||
disabled={!selectedTenantId || isLocationsLoading}
|
||||
className="bg-transparent text-xs font-semibold text-slate-900 focus:outline-none cursor-pointer max-w-[220px] truncate"
|
||||
>
|
||||
<option value="">{selectedLocation ? selectedLocation.locationname : 'Select Hub Location'}</option>
|
||||
{sortedLocations.map((loc) => {
|
||||
const id = String(loc.tenantlocationid || loc.locationid);
|
||||
return (
|
||||
<option key={id} value={id}>
|
||||
{locationDisplayName(loc)}
|
||||
{loc.isprimary ? ' · PRIMARY' : ''}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
<HubLocationSelect
|
||||
locations={tenantLocations}
|
||||
selectedLocationId={selectedLocationId}
|
||||
onSelectLocation={handleSelectLocation}
|
||||
disabled={!selectedTenantId || isLocationsLoading}
|
||||
placeholder={!selectedTenantId ? 'Choose a client' : 'Select Hub Location'}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@@ -489,7 +483,7 @@ export default function CreateOrder() {
|
||||
setSelectedLocationId('');
|
||||
setSelectedLocation(null);
|
||||
}}
|
||||
className="w-full h-9 px-3 bg-white border border-slate-200 rounded-lg text-xs font-medium text-slate-900 focus:outline-none focus:ring-2 focus:ring-black/10 focus:border-black transition-colors cursor-pointer"
|
||||
className="w-full h-10 px-3 bg-white border border-slate-200 rounded-xl text-xs font-medium text-slate-900 focus:outline-none focus:ring-2 focus:ring-black/10 focus:border-black transition-colors cursor-pointer"
|
||||
>
|
||||
<option value="" disabled>
|
||||
{isTenantsLoading ? 'Loading tenants…' : 'Select client'}
|
||||
@@ -506,32 +500,14 @@ export default function CreateOrder() {
|
||||
<label className="block text-xs font-semibold text-slate-800 mb-1.5">
|
||||
Business Hub <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
value={selectedLocationId}
|
||||
onChange={(e) => handleSelectLocation(e.target.value)}
|
||||
<HubLocationSelect
|
||||
variant="field"
|
||||
locations={tenantLocations}
|
||||
selectedLocationId={selectedLocationId}
|
||||
onSelectLocation={handleSelectLocation}
|
||||
disabled={!selectedTenantId || isLocationsLoading}
|
||||
className="w-full h-9 px-3 bg-white border border-slate-200 rounded-lg text-xs font-medium text-slate-900 focus:outline-none focus:ring-2 focus:ring-black/10 focus:border-black transition-colors cursor-pointer disabled:bg-slate-50 disabled:opacity-60"
|
||||
>
|
||||
<option value="">
|
||||
{!selectedTenantId
|
||||
? 'Choose a client first'
|
||||
: isLocationsLoading
|
||||
? 'Loading locations…'
|
||||
: sortedLocations.length === 0
|
||||
? 'No saved locations for this client'
|
||||
: 'Select pickup location'}
|
||||
</option>
|
||||
{sortedLocations.map((loc) => {
|
||||
const id = String(loc.tenantlocationid || loc.locationid);
|
||||
return (
|
||||
<option key={id} value={id}>
|
||||
{locationDisplayName(loc)}
|
||||
{loc.isprimary ? ' · PRIMARY' : ''}
|
||||
{loc.address ? ` — ${loc.address}` : ''}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
placeholder={!selectedTenantId ? 'Choose a client first' : 'Select pickup location'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -756,7 +732,7 @@ export default function CreateOrder() {
|
||||
</div>
|
||||
|
||||
{/* Schedule & Packages Card */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-5 shadow-2xs space-y-4">
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-5 shadow-2xs space-y-5">
|
||||
<div className="flex items-center gap-2.5 pb-3 border-b border-slate-100">
|
||||
<div className="w-7 h-7 rounded-lg bg-amber-50 text-amber-600 flex items-center justify-center">
|
||||
<Clock className="w-4 h-4" />
|
||||
@@ -775,7 +751,7 @@ export default function CreateOrder() {
|
||||
min={dayjs().format('YYYY-MM-DD')}
|
||||
value={pickupDate}
|
||||
onChange={(e) => setPickupDate(e.target.value)}
|
||||
className="w-full h-9 px-3 bg-white border border-slate-200 rounded-lg text-xs font-medium text-slate-900 focus:outline-none focus:ring-2 focus:ring-black/10 focus:border-black transition-colors cursor-pointer"
|
||||
className="w-full h-10 px-3 bg-white border border-slate-200 rounded-xl text-xs font-medium text-slate-900 focus:outline-none focus:ring-2 focus:ring-black/10 focus:border-black transition-colors cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -784,7 +760,7 @@ export default function CreateOrder() {
|
||||
<select
|
||||
value={selectedSlotTime}
|
||||
onChange={(e) => setSelectedSlotTime(e.target.value)}
|
||||
className="w-full h-9 px-3 bg-white border border-slate-200 rounded-lg text-xs font-medium text-slate-900 focus:outline-none focus:ring-2 focus:ring-black/10 focus:border-black transition-colors cursor-pointer"
|
||||
className="w-full h-10 px-3 bg-white border border-slate-200 rounded-xl text-xs font-medium text-slate-900 focus:outline-none focus:ring-2 focus:ring-black/10 focus:border-black transition-colors cursor-pointer"
|
||||
>
|
||||
{buildDefaultPickupSlots().map((slot) => (
|
||||
<option key={slot.time} value={slot.time}>
|
||||
@@ -799,7 +775,7 @@ export default function CreateOrder() {
|
||||
<select
|
||||
value={serviceOption}
|
||||
onChange={(e) => setServiceOption(e.target.value)}
|
||||
className="w-full h-9 px-3 bg-white border border-slate-200 rounded-lg text-xs font-medium text-slate-900 focus:outline-none focus:ring-2 focus:ring-black/10 focus:border-black transition-colors cursor-pointer"
|
||||
className="w-full h-10 px-3 bg-white border border-slate-200 rounded-xl text-xs font-medium text-slate-900 focus:outline-none focus:ring-2 focus:ring-black/10 focus:border-black transition-colors cursor-pointer"
|
||||
>
|
||||
{SERVICE_OPTIONS.map((opt) => (
|
||||
<option key={opt} value={opt}>
|
||||
@@ -813,28 +789,41 @@ export default function CreateOrder() {
|
||||
{/* Parcel Items */}
|
||||
<div className="pt-2 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-xs font-bold text-slate-900">Parcel Items</label>
|
||||
<div>
|
||||
<label className="text-xs font-bold text-slate-900">Parcel Items</label>
|
||||
<p className="text-[11px] text-slate-500">Configure item categories, count, and weight brackets</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddPackage}
|
||||
className="text-xs font-semibold text-blue-600 hover:text-blue-800 flex items-center gap-1 cursor-pointer"
|
||||
className="h-8 px-3 border border-slate-200 bg-white hover:bg-slate-50 text-blue-600 text-xs font-semibold rounded-lg flex items-center gap-1.5 transition-colors cursor-pointer shadow-2xs"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
<span>Add Item</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Column Headers */}
|
||||
<div className="hidden sm:grid grid-cols-12 gap-3 px-3 text-[11px] font-bold text-slate-500 uppercase tracking-wider">
|
||||
<div className="col-span-4">Category</div>
|
||||
<div className="col-span-3">Quantity (Count)</div>
|
||||
<div className="col-span-4">Weight Bracket</div>
|
||||
<div className="col-span-1 text-right">Action</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2.5">
|
||||
{packages.map((pkg, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="p-3 bg-slate-50 border border-slate-200/80 rounded-lg grid grid-cols-12 gap-2.5 items-center"
|
||||
className="p-3 bg-slate-50/80 border border-slate-200 rounded-xl grid grid-cols-12 gap-3 items-center"
|
||||
>
|
||||
<div className="col-span-12 sm:col-span-3">
|
||||
{/* Category */}
|
||||
<div className="col-span-12 sm:col-span-4">
|
||||
<label className="block sm:hidden text-[10px] font-bold text-slate-500 mb-1 uppercase">Category</label>
|
||||
<select
|
||||
value={pkg.category}
|
||||
onChange={(e) => handleUpdatePackage(idx, 'category', e.target.value)}
|
||||
className="w-full h-8 px-2 bg-white border border-slate-200 rounded text-xs font-medium text-slate-800 focus:outline-none focus:border-black cursor-pointer"
|
||||
className="w-full h-9 px-2.5 bg-white border border-slate-200 rounded-lg text-xs font-medium text-slate-800 focus:outline-none focus:border-black cursor-pointer shadow-2xs"
|
||||
>
|
||||
{ITEM_CATEGORIES.map((c) => (
|
||||
<option key={c} value={c}>
|
||||
@@ -844,44 +833,116 @@ export default function CreateOrder() {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 sm:col-span-5">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Item description (e.g. Lunch Box, Electronics)"
|
||||
value={pkg.description}
|
||||
onChange={(e) => handleUpdatePackage(idx, 'description', e.target.value)}
|
||||
className="w-full h-8 px-2.5 bg-white border border-slate-200 rounded text-xs font-medium text-slate-800 focus:outline-none focus:border-black"
|
||||
/>
|
||||
{/* Quantity / Count */}
|
||||
<div className="col-span-6 sm:col-span-3">
|
||||
<label className="block sm:hidden text-[10px] font-bold text-slate-500 mb-1 uppercase">Quantity</label>
|
||||
{pkg.isCustomCount ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="999"
|
||||
placeholder="Qty"
|
||||
value={pkg.count}
|
||||
onChange={(e) => handleUpdatePackage(idx, 'count', Math.max(1, Number(e.target.value) || 1))}
|
||||
className="w-full h-9 px-2.5 bg-white border border-slate-200 rounded-lg text-xs font-semibold text-slate-900 focus:outline-none focus:border-black text-center shadow-2xs"
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
handleUpdatePackage(idx, 'isCustomCount', false);
|
||||
handleUpdatePackage(idx, 'count', 1);
|
||||
}}
|
||||
className="text-[10px] text-blue-600 hover:underline shrink-0 px-1"
|
||||
title="Switch to list"
|
||||
>
|
||||
List
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<select
|
||||
value={COUNT_OPTIONS.includes(Number(pkg.count)) ? Number(pkg.count) : 'custom'}
|
||||
onChange={(e) => {
|
||||
if (e.target.value === 'custom') {
|
||||
handleUpdatePackage(idx, 'isCustomCount', true);
|
||||
handleUpdatePackage(idx, 'count', 11);
|
||||
} else {
|
||||
handleUpdatePackage(idx, 'count', Number(e.target.value));
|
||||
}
|
||||
}}
|
||||
className="w-full h-9 px-2.5 bg-white border border-slate-200 rounded-lg text-xs font-semibold text-slate-800 focus:outline-none focus:border-black cursor-pointer shadow-2xs"
|
||||
>
|
||||
{COUNT_OPTIONS.map((num) => (
|
||||
<option key={num} value={num}>
|
||||
{num} {num === 1 ? 'item' : 'items'}
|
||||
</option>
|
||||
))}
|
||||
<option value="custom">More than 10 (Custom…)</option>
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-span-6 sm:col-span-2">
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="Qty"
|
||||
value={pkg.count}
|
||||
onChange={(e) => handleUpdatePackage(idx, 'count', Number(e.target.value) || 1)}
|
||||
className="w-full h-8 px-2 bg-white border border-slate-200 rounded text-xs font-medium text-slate-800 focus:outline-none focus:border-black text-center font-mono"
|
||||
/>
|
||||
{/* Weight Bracket */}
|
||||
<div className="col-span-6 sm:col-span-4">
|
||||
<label className="block sm:hidden text-[10px] font-bold text-slate-500 mb-1 uppercase">Weight</label>
|
||||
{pkg.isCustomWeight ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g. 15 kg"
|
||||
value={pkg.weight}
|
||||
onChange={(e) => handleUpdatePackage(idx, 'weight', e.target.value)}
|
||||
className="w-full h-9 px-2.5 bg-white border border-slate-200 rounded-lg text-xs font-semibold text-slate-900 focus:outline-none focus:border-black shadow-2xs"
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
handleUpdatePackage(idx, 'isCustomWeight', false);
|
||||
handleUpdatePackage(idx, 'weight', '1 - 10 kg');
|
||||
}}
|
||||
className="text-[10px] text-blue-600 hover:underline shrink-0 px-1"
|
||||
title="Switch to brackets"
|
||||
>
|
||||
Brackets
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<select
|
||||
value={WEIGHT_OPTIONS.some((w) => w.value === pkg.weight) ? pkg.weight : 'custom'}
|
||||
onChange={(e) => {
|
||||
if (e.target.value === 'custom') {
|
||||
handleUpdatePackage(idx, 'isCustomWeight', true);
|
||||
handleUpdatePackage(idx, 'weight', '');
|
||||
} else {
|
||||
handleUpdatePackage(idx, 'weight', e.target.value);
|
||||
}
|
||||
}}
|
||||
className="w-full h-9 px-2.5 bg-white border border-slate-200 rounded-lg text-xs font-semibold text-slate-800 focus:outline-none focus:border-black cursor-pointer shadow-2xs"
|
||||
>
|
||||
{WEIGHT_OPTIONS.map((w) => (
|
||||
<option key={w.value} value={w.value}>
|
||||
{w.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-span-6 sm:col-span-2 flex items-center justify-end gap-1.5">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Weight (kg)"
|
||||
value={pkg.weight}
|
||||
onChange={(e) => handleUpdatePackage(idx, 'weight', e.target.value)}
|
||||
className="w-16 h-8 px-2 bg-white border border-slate-200 rounded text-xs font-medium text-slate-800 focus:outline-none focus:border-black text-right"
|
||||
/>
|
||||
{packages.length > 1 && (
|
||||
{/* Action */}
|
||||
<div className="col-span-12 sm:col-span-1 flex items-center justify-end">
|
||||
{packages.length > 1 ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemovePackage(idx)}
|
||||
className="p-1 text-slate-400 hover:text-red-600 rounded transition-colors"
|
||||
title="Remove parcel"
|
||||
className="p-2 text-slate-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors cursor-pointer"
|
||||
title="Remove item"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
) : (
|
||||
<div className="w-8" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -890,13 +951,13 @@ export default function CreateOrder() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-800 mb-1">Dispatch Notes / Gate Instructions</label>
|
||||
<label className="block text-xs font-semibold text-slate-800 mb-1.5">Dispatch Notes / Gate Instructions</label>
|
||||
<textarea
|
||||
rows={2}
|
||||
placeholder="Gate code, delivery landmarks, call upon arrival instructions…"
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
className="w-full p-2.5 bg-white border border-slate-200 rounded-lg text-xs font-medium text-slate-900 focus:outline-none focus:ring-2 focus:ring-black/10 focus:border-black transition-colors resize-none"
|
||||
className="w-full p-3 bg-white border border-slate-200 rounded-xl text-xs font-medium text-slate-900 focus:outline-none focus:ring-2 focus:ring-black/10 focus:border-black transition-colors resize-none shadow-2xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ import Papa from 'papaparse';
|
||||
import * as XLSX from 'xlsx';
|
||||
|
||||
import AddressAutocomplete, { geocodeAddress } from '@/components/doormile/AddressAutocomplete';
|
||||
import { HubLocationSelect } from '@/components/doormile/HubLocationSelect';
|
||||
import {
|
||||
createExpressBookingBulk,
|
||||
getAdminCustomers,
|
||||
@@ -612,32 +613,14 @@ export default function MultipleOrders() {
|
||||
<label className="block text-xs font-semibold text-slate-800 mb-1.5">
|
||||
Business Hub <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
value={selectedLocationId}
|
||||
onChange={(e) => handleSelectLocation(e.target.value)}
|
||||
<HubLocationSelect
|
||||
variant="field"
|
||||
locations={tenantLocations}
|
||||
selectedLocationId={selectedLocationId}
|
||||
onSelectLocation={handleSelectLocation}
|
||||
disabled={!selectedTenantId || isLocationsLoading}
|
||||
className="w-full h-9 px-3 bg-white border border-slate-200 rounded-lg text-xs font-medium text-slate-900 focus:outline-none focus:ring-2 focus:ring-black/10 focus:border-black transition-colors cursor-pointer disabled:bg-slate-50 disabled:opacity-60"
|
||||
>
|
||||
<option value="">
|
||||
{!selectedTenantId
|
||||
? 'Choose a client first'
|
||||
: isLocationsLoading
|
||||
? 'Loading locations…'
|
||||
: sortedLocations.length === 0
|
||||
? 'No saved locations for this client'
|
||||
: 'Select pickup location'}
|
||||
</option>
|
||||
{sortedLocations.map((loc) => {
|
||||
const id = String(loc.tenantlocationid || loc.locationid);
|
||||
return (
|
||||
<option key={id} value={id}>
|
||||
{locationDisplayName(loc)}
|
||||
{loc.isprimary ? ' · PRIMARY' : ''}
|
||||
{loc.address ? ` — ${loc.address}` : ''}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
placeholder={!selectedTenantId ? 'Choose a client first' : 'Select pickup location'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
|
||||
Reference in New Issue
Block a user