import product catalogue

This commit is contained in:
2026-07-16 17:05:06 +05:30
parent 8a1527cef8
commit 11baf732b7
35 changed files with 2722 additions and 1513 deletions

View File

@@ -131,6 +131,7 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID,
setFocusedId(null);
}, [viewMode]);
const [customerStoreFilter, setCustomerStoreFilter] = useState<string>('all');
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [tripSort, setTripSort] = useState<'planned' | 'time'>('planned');
const [animateNonce, setAnimateNonce] = useState(0);
@@ -167,6 +168,7 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID,
if (viewMode === 'stores' && locationsQ.data) {
for (const loc of locationsQ.data) {
if (locationid && fnum(loc.locationid) !== locationid) continue;
const id = String(fnum(loc.locationid)).toLowerCase();
const name = fstr(loc.locationname) || `Store ${id}`;
map.set(id, { id, name, color: colorFor(id), orders: [], delivered: 0, totalKm: 0, profit: 0, riders: new Set(), suburbs: new Map(), statusCounts: {} });
@@ -175,6 +177,10 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID,
if (viewMode === 'customers' && customersQ.data) {
for (const cust of customersQ.data) {
if (customerStoreFilter !== 'all') {
const locId = String(fnum(cust.locationid));
if (locId !== customerStoreFilter) continue;
}
const id = String(fnum(cust.customerid) || fstr(cust.contactno)).toLowerCase();
const name = fstr(cust.customername) || fstr(cust.name) || `Customer ${id}`;
map.set(id, { id, name, color: colorFor(id), orders: [], delivered: 0, totalKm: 0, profit: 0, riders: new Set(), suburbs: new Map(), statusCounts: {}, raw: cust });
@@ -221,6 +227,12 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID,
return Array.from(map.values()).sort((a, b) => b.orders.length - a.orders.length || a.name.localeCompare(b.name));
}, [rows, viewMode, locationsQ.data, customersQ.data]);
useEffect(() => {
if (viewMode === 'stores' && locationid && groups.length === 1 && !focusedId) {
setFocusedId(groups[0].id);
}
}, [viewMode, locationid, groups, focusedId]);
const focused = groups.find((g) => g.id === focusedId) ?? null;
const groupedByRider = viewMode !== 'riders';
@@ -361,7 +373,7 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID,
groupedByRider={groupedByRider}
tripSort={tripSort}
setTripSort={setTripSort}
onBack={() => setFocusedId(null)}
onBack={(locationid && viewMode === 'stores' && groups.length === 1) ? undefined : () => setFocusedId(null)}
fmtTime={fmtTime}
riderLogs={riderLogsQ.data}
riderLogsLoading={riderLogsQ.isLoading}
@@ -370,8 +382,31 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID,
<div className="ph">{viewMode === 'customers' ? 'No customers found' : 'No deliveries for this day'}</div>
) : (
<>
<div className="ph">
{viewMode === 'riders' ? 'Riders' : viewMode === 'customers' ? 'Customers' : viewMode === 'stores' ? 'Stores' : 'Zones'} ({groups.length})
<div className="ph relative flex items-center justify-between w-full h-8">
<span>{viewMode === 'riders' ? 'Riders' : viewMode === 'customers' ? 'Customers' : viewMode === 'stores' ? 'Stores' : 'Zones'} ({groups.length})</span>
{viewMode === 'customers' && !locationid && locationsQ.data && (
<div className="absolute right-0 flex items-center group/filter cursor-pointer">
<select
className="appearance-none w-36 text-[11px] font-extrabold tracking-wide rounded-md pl-3 pr-8 py-1.5 bg-white text-slate-800 shadow-sm ring-1 ring-slate-900/5 hover:bg-slate-50 outline-none focus:ring-2 focus:ring-[#662582]/30 transition-all duration-300 cursor-pointer relative z-0 truncate"
style={{ textOverflow: 'ellipsis' }}
value={customerStoreFilter}
onChange={(e) => {
setCustomerStoreFilter(e.target.value);
setFocusedId(null);
}}
>
<option value="all">All Stores</option>
{locationsQ.data.map(loc => (
<option key={fnum(loc.locationid)} value={String(fnum(loc.locationid))}>
{fstr(loc.locationname) || `Store ${fnum(loc.locationid)}`}
</option>
))}
</select>
<div className="absolute right-2 text-slate-400 pointer-events-none group-hover/filter:text-[#662582] transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="m6 9 6 6 6-6"/></svg>
</div>
</div>
)}
</div>
{groups.map((g) => {
const isSelected = focusedId === g.id;
@@ -580,16 +615,18 @@ function FocusedDetail({
groupedByRider: boolean;
tripSort: 'planned' | 'time';
setTripSort: (v: 'planned' | 'time') => void;
onBack: () => void;
onBack?: () => void;
fmtTime: (raw: unknown) => string;
riderLogs?: Row[];
riderLogsLoading?: boolean;
}) {
return (
<>
<button className="sbt" onClick={onBack} style={{ marginBottom: 12 }}>
<span className="sbt-icon"><ChevronLeft size={15} /></span> Back to list
</button>
{onBack && (
<button className="sbt" onClick={onBack} style={{ marginBottom: 12 }}>
<span className="sbt-icon"><ChevronLeft size={15} /></span> Back to list
</button>
)}
{riderLogs && riderLogs.length > 0 && (
<RiderTelemetryPanel
@@ -599,6 +636,16 @@ function FocusedDetail({
/>
)}
{tripBlocks.length === 0 && (
<div className="flex flex-col items-center justify-center p-8 text-center h-48 border-2 border-dashed border-slate-200 rounded-xl mt-4 bg-slate-50/50">
<div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center text-slate-400 mb-3">
<Package size={20} />
</div>
<p className="font-bold text-slate-700 text-sm">No orders to display</p>
<p className="text-xs text-slate-500 mt-1">There are no deliveries matching this selection for the current date.</p>
</div>
)}
{tripBlocks.map((blk, bi) => (
<div className="trip-block" key={bi}>
<div className="trip-header" style={{ background: `${blk.color}12`, borderColor: `${blk.color}40` }}>