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

@@ -14,7 +14,7 @@
import React, { useMemo, useState, useRef, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { ShoppingBag, Clock, CheckCircle2, XCircle, Calendar, ChevronLeft, ChevronRight, Package, MapPin, Phone, X, Loader2, Download, UserCheck, ClipboardList, ArrowLeft } from 'lucide-react';
import { useFiestaOrderSummary, useFiestaOrders, useFiestaOrderDetails, useFiestaRiders, useFiestaAssignRider } from '../services/fiestaQueries';
import { useFiestaOrderSummary, useFiestaOrders, useFiestaOrderDetails, useFiestaRiders, useFiestaAssignRider, useFiestaUsers } from '../services/fiestaQueries';
import { FIESTA_TENANT_ID, num as fnum, str as fstr, ymd, type Row } from '../services/fiestaApi';
import { shortTime } from '../services/fiestaMappers';
import {
@@ -105,6 +105,8 @@ export default function OrdersView({ searchQuery = '', locationid, tenantId = FI
setShowSelected(false);
}, [fromdate, todate, status, branch, pageno, locationid]);
const [riderSource, setRiderSource] = useState<'own' | 'partner'>('own');
// Scope to the user's store when a locationid is supplied (server-side per the
// backend's getordersummary/getorders locationid param); tenant-wide otherwise.
const summaryQ = useFiestaOrderSummary(tenantId, fromdate, todate, locationid);
@@ -118,20 +120,45 @@ export default function OrdersView({ searchQuery = '', locationid, tenantId = FI
// simply won't appear — the intended guard.
const orderPartnerId = useMemo(() => fnum(rawRows.find((r) => fnum(r.partnerid))?.partnerid), [rawRows]);
const orderApplocationId = useMemo(() => fnum(rawRows.find((r) => fnum(r.applocationid))?.applocationid), [rawRows]);
const ridersQ = useFiestaRiders({
tenantid: tenantId,
applocationid: orderApplocationId || undefined,
partnerid: orderPartnerId || undefined,
// We omit partnerid here to fetch all partner riders for the location at once.
});
const internalRidersQ = useFiestaUsers({
tenantid: tenantId,
roleid: 5, // 5 = Rider role
pagesize: 500
});
const riderOptions = useMemo(
() =>
(ridersQ.data ?? [])
() => {
const externalRiders = ridersQ.data ?? [];
const internalRiders = internalRidersQ.data ?? [];
const allRiders = [...externalRiders, ...internalRiders];
const filtered = allRiders.filter((r) => {
const pId = fnum(r.partnerid);
if (riderSource === 'own') {
// Store fleet riders are internal users (they have no partner id)
return !pId || pId === 0;
} else {
// Partner riders belong to a 3rd party (partnerid > 0)
// If the order already has a specific partnerid, we only show riders from that partner.
return pId > 0 && (!orderPartnerId || pId === orderPartnerId);
}
});
return filtered
.map((r) => ({
id: fnum(r.userid),
label: `${fstr(r.firstname)} ${fstr(r.lastname)}`.trim() + (fstr(r.contactno) ? ` · ${fstr(r.contactno)}` : ''),
}))
.filter((o) => o.id > 0 && o.label),
[ridersQ.data],
.filter((o) => o.id > 0 && o.label);
},
[ridersQ.data, internalRidersQ.data, riderSource, orderPartnerId],
);
// Branches (app-locations) present in the data — drives the branch filter so the
@@ -342,6 +369,22 @@ export default function OrdersView({ searchQuery = '', locationid, tenantId = FI
<span className="inline-flex items-center gap-1.5 font-extrabold text-xs" style={{ color: BRAND }}>
<UserCheck size={15} /> {selected.size} selected
</span>
<div className="flex bg-white rounded-full p-0.5 ml-2 border" style={{ borderColor: edge(BRAND) }}>
<button
onClick={() => setRiderSource('own')}
className={`px-3 py-1 text-[11px] font-bold rounded-full transition-colors ${riderSource === 'own' ? '' : 'text-slate-500 hover:bg-slate-50'}`}
style={riderSource === 'own' ? { background: tint(BRAND), color: BRAND } : undefined}
>
Store Fleet
</button>
<button
onClick={() => setRiderSource('partner')}
className={`px-3 py-1 text-[11px] font-bold rounded-full transition-colors ${riderSource === 'partner' ? '' : 'text-slate-500 hover:bg-slate-50'}`}
style={riderSource === 'partner' ? { background: tint(BRAND), color: BRAND } : undefined}
>
Partners
</button>
</div>
<select
value={assignRiderId}
onChange={(e) => setAssignRiderId(Number(e.target.value))}
@@ -350,7 +393,7 @@ export default function OrdersView({ searchQuery = '', locationid, tenantId = FI
className="rounded-full font-bold text-xs outline-none cursor-pointer disabled:opacity-50"
style={{ padding: '7px 12px', border: `1.5px solid ${edge(BRAND)}`, background: '#fff', color: BRAND, maxWidth: 260 }}
>
<option value={0}>{ridersQ.isLoading ? 'Loading riders…' : riderOptions.length ? 'Select rider…' : 'No riders available'}</option>
<option value={0}>{ridersQ.isLoading ? 'Loading riders…' : riderOptions.length ? 'Select rider…' : `No ${riderSource === 'own' ? 'store' : 'partner'} riders available`}</option>
{riderOptions.map((o) => <option key={o.id} value={o.id}>{o.label}</option>)}
</select>
<button
@@ -371,7 +414,7 @@ export default function OrdersView({ searchQuery = '', locationid, tenantId = FI
)}
{/* Table */}
<div className="bg-white border rounded-2xl overflow-hidden" style={{ borderColor: BORDER }}>
<div className="bg-white border overflow-hidden" style={{ borderColor: BORDER }}>
<div className="overflow-x-auto">
<table className="w-full" style={{ minWidth: 960 }}>
<thead>
@@ -554,7 +597,7 @@ function SelectedOrdersPage({
No orders selected. <button onClick={onClose} className="font-bold underline cursor-pointer" style={{ color: BRAND }}>Go back</button>
</div>
) : (
<div className="bg-white border rounded-2xl overflow-hidden" style={{ borderColor: BORDER }}>
<div className="bg-white border overflow-hidden" style={{ borderColor: BORDER }}>
<div className="overflow-x-auto">
<table className="w-full" style={{ minWidth: 720 }}>
<thead><tr>{['#', 'Order', 'Pickup', 'Drop', 'Status', ''].map((h, i) => <th key={i} className="px-3 py-2.5 text-left" style={TH_STYLE}>{h}</th>)}</tr></thead>
@@ -632,7 +675,7 @@ function OrderDetailModal({ order, onClose }: { order: Row; onClose: () => void
return createPortal(
<div className="fixed inset-0 z-[200] flex items-center justify-center p-4" style={{ background: 'rgba(15,23,42,0.4)', backdropFilter: 'blur(4px)' }}
onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}>
<div className="bg-white max-h-[90vh] flex flex-col overflow-hidden rounded-2xl animate-in zoom-in-95 duration-200" style={{ width: 'min(32rem, 92vw)', border: `1px solid ${BORDER}`, boxShadow: '0 18px 50px rgba(15,23,42,0.18)' }}>
<div className="bg-white max-h-[90vh] flex flex-col overflow-hidden animate-in zoom-in-95 duration-200" style={{ width: 'min(32rem, 92vw)', border: `1px solid ${BORDER}`, boxShadow: '0 18px 50px rgba(15,23,42,0.18)' }}>
<div style={{ height: 4, background: `linear-gradient(90deg, ${BRAND} 0%, ${soft(BRAND)} 100%)` }} />
<div className="p-4 border-b flex justify-between items-center shrink-0" style={{ borderColor: BORDER, background: SURFACE_ALT }}>
<h4 className="font-extrabold flex items-center gap-2" style={{ color: TEXT }}><Package size={16} style={{ color: BRAND }} /> Order {fstr(order.orderid) || `#${fstr(order.orderheaderid)}`}</h4>