feat: add PO/Notes popovers, supplier color sidebar, fix popover clipping
- Add ItemDetailModal showing PO document (view/download/share/email) + buyer notes - Add FileText (PO) and MessageSquare (notes) buttons to compact item list with popovers - Fix photo upload: append to order_items.order_item_attachements (PB file field) - Add colored left sidebar stripe per supplier group for visual distinction - Fix popover z-index so they appear above following cards (overflow-visible + z-50) - Extend PurchaseOrder type with documents, email_payload, supplier - Add order_item_attachements and expand.purchase_order to OrderItem - onShowDetails prop wired through SupplierGroup and AreaGroup Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
2a7720cd1f
commit
85c63b336c
10 changed files with 875 additions and 213 deletions
35
app/page.tsx
35
app/page.tsx
|
|
@ -10,6 +10,7 @@ import SupplierGroup from '@/components/SupplierGroup';
|
|||
import AreaGroup from '@/components/AreaGroup';
|
||||
import CollectModal from '@/components/CollectModal';
|
||||
import ProblemModal from '@/components/ProblemModal';
|
||||
import ItemDetailModal from '@/components/ItemDetailModal';
|
||||
import {
|
||||
getMyAssignedItems,
|
||||
getPoolItems,
|
||||
|
|
@ -40,16 +41,16 @@ import {
|
|||
// tile (not a saturated left-border stripe). Each group gets a
|
||||
// distinct hue but all read as part of the same family.
|
||||
const SUPPLIER_COLORS = [
|
||||
{ dot: 'bg-[#f54e00]', light: 'bg-[#ebeae5]', text: 'text-[#26251e]', ring: 'ring-[#f54e00]/30' }, // orange (signature)
|
||||
{ dot: 'bg-[#18714f]', light: 'bg-[#e6f1ec]', text: 'text-[#125a3e]', ring: 'ring-[#18714f]/30' }, // emerald
|
||||
{ dot: 'bg-[#6c5a99]', light: 'bg-[#ece8f3]', text: 'text-[#4a3d6b]', ring: 'ring-[#6c5a99]/30' }, // violet
|
||||
{ dot: 'bg-[#cf2d56]', light: 'bg-[#fbeaef]', text: 'text-[#8a1b3a]', ring: 'ring-[#cf2d56]/30' }, // rose
|
||||
{ dot: 'bg-[#8a5a02]', light: 'bg-[#fcf3d8]', text: 'text-[#8a5a02]', ring: 'ring-[#d49103]/30' }, // amber
|
||||
{ dot: 'bg-[#26251e]', light: 'bg-[#ebeae5]', text: 'text-[#26251e]', ring: 'ring-[#26251e]/30' }, // ink
|
||||
{ dot: 'bg-[#6b6760]', light: 'bg-[#ebeae5]', text: 'text-[#4a4742]', ring: 'ring-[#6b6760]/30' }, // warm gray
|
||||
{ dot: 'bg-[#0d9488]', light: 'bg-[#e6f5f3]', text: 'text-[#0a7a70]', ring: 'ring-[#0d9488]/30' }, // teal
|
||||
{ dot: 'bg-[#8a1b3a]', light: 'bg-[#fbeaef]', text: 'text-[#8a1b3a]', ring: 'ring-[#cf2d56]/30' }, // crimson
|
||||
{ dot: 'bg-[#4a3d6b]', light: 'bg-[#ece8f3]', text: 'text-[#4a3d6b]', ring: 'ring-[#6c5a99]/30' }, // indigo
|
||||
{ dot: 'bg-[#f54e00]', border: 'border-l-[#f54e00]', light: 'bg-[#ebeae5]', text: 'text-[#26251e]', ring: 'ring-[#f54e00]/30' }, // orange (signature)
|
||||
{ dot: 'bg-[#18714f]', border: 'border-l-[#18714f]', light: 'bg-[#e6f1ec]', text: 'text-[#125a3e]', ring: 'ring-[#18714f]/30' }, // emerald
|
||||
{ dot: 'bg-[#6c5a99]', border: 'border-l-[#6c5a99]', light: 'bg-[#ece8f3]', text: 'text-[#4a3d6b]', ring: 'ring-[#6c5a99]/30' }, // violet
|
||||
{ dot: 'bg-[#cf2d56]', border: 'border-l-[#cf2d56]', light: 'bg-[#fbeaef]', text: 'text-[#8a1b3a]', ring: 'ring-[#cf2d56]/30' }, // rose
|
||||
{ dot: 'bg-[#8a5a02]', border: 'border-l-[#8a5a02]', light: 'bg-[#fcf3d8]', text: 'text-[#8a5a02]', ring: 'ring-[#d49103]/30' }, // amber
|
||||
{ dot: 'bg-[#26251e]', border: 'border-l-[#26251e]', light: 'bg-[#ebeae5]', text: 'text-[#26251e]', ring: 'ring-[#26251e]/30' }, // ink
|
||||
{ dot: 'bg-[#6b6760]', border: 'border-l-[#6b6760]', light: 'bg-[#ebeae5]', text: 'text-[#4a4742]', ring: 'ring-[#6b6760]/30' }, // warm gray
|
||||
{ dot: 'bg-[#0d9488]', border: 'border-l-[#0d9488]', light: 'bg-[#e6f5f3]', text: 'text-[#0a7a70]', ring: 'ring-[#0d9488]/30' }, // teal
|
||||
{ dot: 'bg-[#8a1b3a]', border: 'border-l-[#8a1b3a]', light: 'bg-[#fbeaef]', text: 'text-[#8a1b3a]', ring: 'ring-[#cf2d56]/30' }, // crimson
|
||||
{ dot: 'bg-[#4a3d6b]', border: 'border-l-[#4a3d6b]', light: 'bg-[#ece8f3]', text: 'text-[#4a3d6b]', ring: 'ring-[#6c5a99]/30' }, // indigo
|
||||
];
|
||||
|
||||
export default function CollectionsPage() {
|
||||
|
|
@ -75,6 +76,7 @@ export default function CollectionsPage() {
|
|||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedItem, setSelectedItem] = useState<OrderItem | null>(null);
|
||||
const [showCollectModal, setShowCollectModal] = useState(false);
|
||||
const [showDetailModal, setShowDetailModal] = useState(false);
|
||||
const [showProblemModal, setShowProblemModal] = useState(false);
|
||||
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'warning' } | null>(null);
|
||||
const [viewMode, setViewMode] = useState<'compact' | 'detailed'>('compact');
|
||||
|
|
@ -279,6 +281,11 @@ const loadItems = useCallback(async (showLoading = true) => {
|
|||
window.open(url, '_blank');
|
||||
};
|
||||
|
||||
const handleShowDetails = (item: OrderItem) => {
|
||||
setSelectedItem(item);
|
||||
setShowDetailModal(true);
|
||||
};
|
||||
|
||||
const toggleSupplier = (supplierId: string) => {
|
||||
setExpandedSuppliers(prev => {
|
||||
const next = new Set(prev);
|
||||
|
|
@ -600,6 +607,7 @@ const loadItems = useCallback(async (showLoading = true) => {
|
|||
onCollect={(item) => { setSelectedItem(item); setShowCollectModal(true); }}
|
||||
onProblem={(item) => { setSelectedItem(item); setShowProblemModal(true); }}
|
||||
onNavigate={handleNavigate}
|
||||
onShowDetails={handleShowDetails}
|
||||
/>
|
||||
);
|
||||
})
|
||||
|
|
@ -622,6 +630,7 @@ const loadItems = useCallback(async (showLoading = true) => {
|
|||
onCollect={(item) => { setSelectedItem(item); setShowCollectModal(true); }}
|
||||
onProblem={(item) => { setSelectedItem(item); setShowProblemModal(true); }}
|
||||
onNavigate={handleNavigate}
|
||||
onShowDetails={handleShowDetails}
|
||||
/>
|
||||
);
|
||||
})
|
||||
|
|
@ -643,6 +652,12 @@ const loadItems = useCallback(async (showLoading = true) => {
|
|||
onClose={() => { setShowProblemModal(false); setSelectedItem(null); }}
|
||||
onConfirm={handleProblem}
|
||||
/>
|
||||
<ItemDetailModal
|
||||
item={selectedItem!}
|
||||
purchaseOrder={selectedItem?.expand?.purchase_order ?? null}
|
||||
isOpen={showDetailModal}
|
||||
onClose={() => { setShowDetailModal(false); setSelectedItem(null); }}
|
||||
/>
|
||||
|
||||
{/* Toast */}
|
||||
{toast && (
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ interface AreaGroupProps {
|
|||
onCollect: (item: OrderItem) => void;
|
||||
onProblem: (item: OrderItem) => void;
|
||||
onNavigate: (lat: number, lng: number, name: string) => void;
|
||||
onShowDetails: (item: OrderItem) => void;
|
||||
}
|
||||
|
||||
function getItemIcon(item: OrderItem) {
|
||||
|
|
@ -45,7 +46,6 @@ function getItemIcon(item: OrderItem) {
|
|||
if (item.is_stock_item) return <Package size={12} className="text-violet-500" />;
|
||||
return <ShoppingCart size={12} className="text-gray-400" />;
|
||||
}
|
||||
|
||||
export default function AreaGroup({
|
||||
clusterId,
|
||||
label,
|
||||
|
|
@ -58,6 +58,7 @@ export default function AreaGroup({
|
|||
onCollect,
|
||||
onProblem,
|
||||
onNavigate,
|
||||
onShowDetails,
|
||||
}: AreaGroupProps) {
|
||||
const totalQty = items.reduce((sum, i) => sum + (i.order_qty || i.quantity || 0), 0);
|
||||
|
||||
|
|
@ -216,6 +217,7 @@ export default function AreaGroup({
|
|||
onCollect={onCollect}
|
||||
onProblem={onProblem}
|
||||
onNavigate={onNavigate}
|
||||
onShowDetails={onShowDetails}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { OrderItem } from '@/lib/types';
|
||||
import { cn, formatDate, formatCurrency, parseCoordinates } from '@/lib/utils';
|
||||
import { getItemDisplayDescription, getItemDisplayQty } from '@/lib/pocketbase';
|
||||
|
|
@ -17,6 +17,9 @@ import {
|
|||
AlertCircle,
|
||||
Clock,
|
||||
MapPin,
|
||||
FileText,
|
||||
MessageSquare,
|
||||
ExternalLink,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface CollectionItemProps {
|
||||
|
|
@ -26,6 +29,8 @@ interface CollectionItemProps {
|
|||
onCollect: (item: OrderItem) => void;
|
||||
onProblem: (item: OrderItem) => void;
|
||||
onNavigate: (lat: number, lng: number, name: string) => void;
|
||||
// Opens the ItemDetailModal showing PO document + buyer notes
|
||||
onShowDetails: (item: OrderItem) => void;
|
||||
}
|
||||
|
||||
function getItemType(item: OrderItem): {
|
||||
|
|
@ -46,18 +51,19 @@ function getItemType(item: OrderItem): {
|
|||
}
|
||||
|
||||
const orderNumber = item.expand?.order?.order_number || '';
|
||||
const poNumber = item.expand?.purchase_order?.po_number || '';
|
||||
|
||||
if (orderNumber.toLowerCase().includes('breakdown')) {
|
||||
if (item.is_stock_item) {
|
||||
return {
|
||||
type: 'breakdown',
|
||||
icon: <Hammer size={16} />,
|
||||
color: 'text-[#8a5a02]',
|
||||
bgColor: 'bg-[#fcf3d8]',
|
||||
label: 'Breakdown',
|
||||
type: 'stock',
|
||||
icon: <Package size={16} />,
|
||||
color: 'text-[#4a3d6b]',
|
||||
bgColor: 'bg-[#d4cae3]',
|
||||
label: 'Stock',
|
||||
};
|
||||
}
|
||||
|
||||
if (orderNumber.toLowerCase().includes('repair')) {
|
||||
if (orderNumber.startsWith('RPR') || poNumber.startsWith('RPR')) {
|
||||
return {
|
||||
type: 'repair',
|
||||
icon: <Wrench size={16} />,
|
||||
|
|
@ -67,13 +73,13 @@ function getItemType(item: OrderItem): {
|
|||
};
|
||||
}
|
||||
|
||||
if (item.is_stock_item) {
|
||||
if (orderNumber.startsWith('BRK') || poNumber.startsWith('BRK')) {
|
||||
return {
|
||||
type: 'stock',
|
||||
icon: <Package size={16} />,
|
||||
color: 'text-[#4a3d6b]',
|
||||
bgColor: 'bg-[#ece8f3]',
|
||||
label: 'Stock',
|
||||
type: 'breakdown',
|
||||
icon: <Hammer size={16} />,
|
||||
color: 'text-[#8a5a02]',
|
||||
bgColor: 'bg-[#f6e3a4]',
|
||||
label: 'Breakdown',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -86,6 +92,19 @@ function getItemType(item: OrderItem): {
|
|||
};
|
||||
}
|
||||
|
||||
// Closes tooltip when clicking outside
|
||||
function useClickOutside(ref: React.RefObject<HTMLElement | null>, onClose: () => void) {
|
||||
useEffect(() => {
|
||||
function handleClick(e: MouseEvent) {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClick);
|
||||
return () => document.removeEventListener('mousedown', handleClick);
|
||||
}, [ref, onClose]);
|
||||
}
|
||||
|
||||
export default function CollectionItemCard({
|
||||
item,
|
||||
supplierColor,
|
||||
|
|
@ -93,19 +112,31 @@ export default function CollectionItemCard({
|
|||
onCollect,
|
||||
onProblem,
|
||||
onNavigate,
|
||||
onShowDetails,
|
||||
}: CollectionItemProps) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [showNotesTooltip, setShowNotesTooltip] = useState(false);
|
||||
const [showPoTooltip, setShowPoTooltip] = useState(false);
|
||||
const notesTooltipRef = useRef<HTMLDivElement>(null);
|
||||
const poTooltipRef = useRef<HTMLDivElement>(null);
|
||||
useClickOutside(notesTooltipRef, () => setShowNotesTooltip(false));
|
||||
useClickOutside(poTooltipRef, () => setShowPoTooltip(false));
|
||||
|
||||
const supplier = item.expand?.supplier;
|
||||
const itemType = getItemType(item);
|
||||
const coords = parseCoordinates(supplier?.coordinates);
|
||||
const hasCoords = coords !== null;
|
||||
// Per the project brief: prefer the supplier-facing description on the
|
||||
// pickup screen; fall back to the customer-facing description.
|
||||
const description = getItemDisplayDescription(item);
|
||||
// Per the brief: prefer order_qty; falls back to quantity.
|
||||
const expectedQty = getItemDisplayQty(item);
|
||||
|
||||
const po = item.expand?.purchase_order;
|
||||
const order = item.expand?.order;
|
||||
const poNumber = po?.po_number;
|
||||
const orderNumber = order?.order_number;
|
||||
const hasNotes = Boolean(item.notes?.trim());
|
||||
const hasProblemNotes = Boolean(item.problem_notes?.trim());
|
||||
const hasAnyNotes = hasNotes || hasProblemNotes;
|
||||
|
||||
return (
|
||||
<article
|
||||
className={cn(
|
||||
|
|
@ -131,13 +162,33 @@ export default function CollectionItemCard({
|
|||
<span className={itemType.color}>{itemType.icon}</span>
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-0.5">
|
||||
<h3 className="font-semibold text-[15px] text-[#26251e] truncate leading-snug">
|
||||
<h3 className="font-semibold text-[15px] text-[#26251e] truncate leading-snug flex-1">
|
||||
{description}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* PO and Order numbers — always visible in compact mode */}
|
||||
{compact && (poNumber || orderNumber) && (
|
||||
<div className="flex items-center gap-2 mb-0.5 -mt-0.5">
|
||||
{poNumber && (
|
||||
<span className="text-[10px] text-[rgba(38,37,30,0.5)] font-mono">
|
||||
PO: {poNumber}
|
||||
</span>
|
||||
)}
|
||||
{poNumber && orderNumber && (
|
||||
<span className="text-[10px] text-[rgba(38,37,30,0.3)]" aria-hidden="true">·</span>
|
||||
)}
|
||||
{orderNumber && (
|
||||
<span className="text-[10px] text-[rgba(38,37,30,0.5)]">
|
||||
Order: {orderNumber}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* If the customer-facing description differs from the
|
||||
supplier-facing one we just rendered, surface the
|
||||
customer one as a smaller sub-line so the driver can
|
||||
|
|
@ -171,7 +222,7 @@ export default function CollectionItemCard({
|
|||
)}
|
||||
</div>
|
||||
|
||||
{supplier?.suppliername && (
|
||||
{supplier?.suppliername && !compact && (
|
||||
<div className="flex items-center gap-1 mt-1.5">
|
||||
<MapPin size={12} className="text-[rgba(38,37,30,0.4)] flex-shrink-0" aria-hidden="true" />
|
||||
<span className="text-xs text-[rgba(38,37,30,0.55)] truncate">{supplier.suppliername}</span>
|
||||
|
|
@ -179,18 +230,167 @@ export default function CollectionItemCard({
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
{item.selling_price && expectedQty > 0 && (
|
||||
<span className="text-xs font-semibold text-[#125a3e]">
|
||||
{formatCurrency(item.selling_price * expectedQty)}
|
||||
</span>
|
||||
)}
|
||||
{expanded ? (
|
||||
<ChevronUp size={18} className="text-[rgba(38,37,30,0.55)]" aria-hidden="true" />
|
||||
) : (
|
||||
<ChevronDown size={18} className="text-[rgba(38,37,30,0.55)]" aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
{/* Right side — tooltip buttons always visible in compact mode */}
|
||||
{compact ? (
|
||||
<div className="flex flex-col items-center gap-1.5 flex-shrink-0">
|
||||
{/* Price + chevron row */}
|
||||
<div className="flex items-center gap-2">
|
||||
{item.selling_price && expectedQty > 0 && (
|
||||
<span className="text-xs font-semibold text-[#125a3e]">
|
||||
{formatCurrency(item.selling_price * expectedQty)}
|
||||
</span>
|
||||
)}
|
||||
{expanded ? (
|
||||
<ChevronUp size={18} className="text-[rgba(38,37,30,0.55)]" aria-hidden="true" />
|
||||
) : (
|
||||
<ChevronDown size={18} className="text-[rgba(38,37,30,0.55)]" aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
{/* Always-visible tooltip buttons — shown as a row of 2 */}
|
||||
<div className="flex items-center gap-1">
|
||||
{/* PO tooltip button */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setShowPoTooltip(v => !v); setShowNotesTooltip(false); }}
|
||||
aria-label="Show PO details"
|
||||
aria-expanded={showPoTooltip}
|
||||
className="w-7 h-7 rounded-lg flex items-center justify-center bg-[#6c5a99] text-white active:bg-[#4a3d6b] transition-colors"
|
||||
>
|
||||
<FileText size={13} aria-hidden="true" />
|
||||
</button>
|
||||
{/* PO tooltip popover */}
|
||||
{showPoTooltip && (
|
||||
<div
|
||||
ref={poTooltipRef}
|
||||
className="absolute right-0 z-50 mt-1 w-64 bg-white border border-[rgba(38,37,30,0.15)] rounded-xl shadow-lg p-3"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
{po ? (
|
||||
<>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<p className="text-[11px] font-semibold text-[#26251e] uppercase tracking-wide">Purchase Order</p>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setShowPoTooltip(false); }}
|
||||
aria-label="Close"
|
||||
className="w-5 h-5 rounded flex items-center justify-center hover:bg-[rgba(38,37,30,0.08)]"
|
||||
>
|
||||
<span className="text-[rgba(38,37,30,0.4)] text-sm">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<dl className="space-y-1.5 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-[rgba(38,37,30,0.55)]">PO Number</dt>
|
||||
<dd className="text-[#26251e] font-medium font-mono">{po.po_number}</dd>
|
||||
</div>
|
||||
{po.status && (
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-[rgba(38,37,30,0.55)]">Status</dt>
|
||||
<dd className="text-[#26251e] font-medium capitalize">{po.status}</dd>
|
||||
</div>
|
||||
)}
|
||||
{po.total_amount != null && (
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-[rgba(38,37,30,0.55)]">Total</dt>
|
||||
<dd className="text-[#26251e] font-medium">R{(po.total_amount / 100).toFixed(2)}</dd>
|
||||
</div>
|
||||
)}
|
||||
{orderNumber && (
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-[rgba(38,37,30,0.55)]">Order</dt>
|
||||
<dd className="text-[#26251e] font-medium">{orderNumber}</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setShowPoTooltip(false); onShowDetails(item); }}
|
||||
className="mt-3 w-full flex items-center justify-center gap-2 py-2 rounded-lg bg-[#6c5a99] text-white text-[12px] font-semibold active:bg-[#4a3d6b] transition-colors"
|
||||
>
|
||||
<ExternalLink size={13} aria-hidden="true" />
|
||||
View PO & Document
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-2">
|
||||
<p className="text-xs text-[rgba(38,37,30,0.55)]">No PO attached</p>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setShowPoTooltip(false); onShowDetails(item); }}
|
||||
className="mt-2 text-[11px] text-[#6c5a99] font-medium hover:underline"
|
||||
>
|
||||
View details
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Notes tooltip button */}
|
||||
{hasAnyNotes && (
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setShowNotesTooltip(v => !v); setShowPoTooltip(false); }}
|
||||
aria-label="Show notes"
|
||||
aria-expanded={showNotesTooltip}
|
||||
className="w-7 h-7 rounded-lg flex items-center justify-center bg-[#8a5a02] text-white active:bg-[#6b4501] transition-colors"
|
||||
>
|
||||
<MessageSquare size={13} aria-hidden="true" />
|
||||
</button>
|
||||
{/* Notes tooltip popover */}
|
||||
{showNotesTooltip && (
|
||||
<div
|
||||
ref={notesTooltipRef}
|
||||
className="absolute right-0 z-50 mt-1 w-64 bg-white border border-[rgba(38,37,30,0.15)] rounded-xl shadow-lg p-3"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<p className="text-[11px] font-semibold text-[#26251e] uppercase tracking-wide">Buyer Notes</p>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setShowNotesTooltip(false); }}
|
||||
aria-label="Close"
|
||||
className="w-5 h-5 rounded flex items-center justify-center hover:bg-[rgba(38,37,30,0.08)]"
|
||||
>
|
||||
<span className="text-[rgba(38,37,30,0.4)] text-sm">×</span>
|
||||
</button>
|
||||
</div>
|
||||
{hasNotes ? (
|
||||
<>
|
||||
<p className="text-[11px] text-[#8a5a02] bg-[#fcf3d8] border border-[#f6e3a4] rounded-lg p-2 selectable leading-snug">
|
||||
{item.notes}
|
||||
</p>
|
||||
{hasProblemNotes && (
|
||||
<p className="text-[11px] text-[#8a1b3a] bg-[#fbeaef] border border-[#f3c4d2] rounded-lg p-2 mt-2 selectable leading-snug">
|
||||
<span className="font-semibold">Problem: </span>{item.problem_notes}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
) : hasProblemNotes ? (
|
||||
<p className="text-[11px] text-[#8a1b3a] bg-[#fbeaef] border border-[#f3c4d2] rounded-lg p-2 selectable leading-snug">
|
||||
{item.problem_notes}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-xs text-[rgba(38,37,30,0.55)] text-center py-1">No notes</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* Full/non-compact right side */
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
{item.selling_price && expectedQty > 0 && (
|
||||
<span className="text-xs font-semibold text-[#125a3e]">
|
||||
{formatCurrency(item.selling_price * expectedQty)}
|
||||
</span>
|
||||
)}
|
||||
{expanded ? (
|
||||
<ChevronUp size={18} className="text-[rgba(38,37,30,0.55)]" aria-hidden="true" />
|
||||
) : (
|
||||
<ChevronDown size={18} className="text-[rgba(38,37,30,0.55)]" aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
|
|
@ -222,16 +422,16 @@ export default function CollectionItemCard({
|
|||
|
||||
{/* Details grid */}
|
||||
<dl className="grid grid-cols-2 gap-2 text-xs">
|
||||
{item.expand?.order?.order_number && (
|
||||
{orderNumber && (
|
||||
<div className="bg-[#ebeae5] rounded-lg p-2">
|
||||
<dt className="text-[rgba(38,37,30,0.55)]">Order</dt>
|
||||
<dd className="text-[#26251e] font-medium">{item.expand.order.order_number}</dd>
|
||||
<dd className="text-[#26251e] font-medium">{orderNumber}</dd>
|
||||
</div>
|
||||
)}
|
||||
{item.expand?.purchase_order?.po_number && (
|
||||
{poNumber && (
|
||||
<div className="bg-[#ebeae5] rounded-lg p-2">
|
||||
<dt className="text-[rgba(38,37,30,0.55)]">PO</dt>
|
||||
<dd className="text-[#26251e] font-medium">{item.expand.purchase_order.po_number}</dd>
|
||||
<dd className="text-[#26251e] font-medium">{poNumber}</dd>
|
||||
</div>
|
||||
)}
|
||||
{supplier?.supplieraddress && (
|
||||
|
|
@ -241,46 +441,69 @@ export default function CollectionItemCard({
|
|||
</div>
|
||||
)}
|
||||
</dl>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="grid grid-cols-3 gap-2 pt-1">
|
||||
<button
|
||||
onClick={() => onCollect(item)}
|
||||
aria-label={`Mark ${description} as collected`}
|
||||
className="flex flex-col items-center justify-center gap-1 py-3 rounded-xl bg-[#1f8a65] text-white active:bg-[#125a3e] transition-colors touch-manipulation press-feedback-sm min-h-[56px]"
|
||||
>
|
||||
<Check size={18} strokeWidth={2.5} aria-hidden="true" />
|
||||
<span className="text-xs font-semibold">Collect</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => onProblem(item)}
|
||||
aria-label={`Report problem with ${description}`}
|
||||
className="flex flex-col items-center justify-center gap-1 py-3 rounded-xl bg-[#cf2d56] text-white active:bg-[#8a1b3a] transition-colors touch-manipulation press-feedback-sm min-h-[56px]"
|
||||
>
|
||||
<AlertTriangle size={18} strokeWidth={2.5} aria-hidden="true" />
|
||||
<span className="text-xs font-semibold">Problem</span>
|
||||
</button>
|
||||
|
||||
{hasCoords && coords && supplier ? (
|
||||
<div className="space-y-2 pt-1">
|
||||
{/* Row 1 — core workflow */}
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<button
|
||||
onClick={() => onNavigate(coords.latitude, coords.longitude, supplier.suppliername)}
|
||||
aria-label={`Navigate to ${supplier.suppliername}`}
|
||||
className="flex flex-col items-center justify-center gap-1 py-3 rounded-xl bg-[#f54e00] text-white active:bg-[#d44200] transition-colors touch-manipulation press-feedback-sm min-h-[56px]"
|
||||
onClick={() => onCollect(item)}
|
||||
aria-label={`Mark ${description} as collected`}
|
||||
className="flex flex-col items-center justify-center gap-1 py-3 rounded-xl bg-[#1f8a65] text-white active:bg-[#125a3e] transition-colors touch-manipulation press-feedback-sm min-h-[56px]"
|
||||
>
|
||||
<Navigation size={18} strokeWidth={2.5} aria-hidden="true" />
|
||||
<span className="text-xs font-semibold">Navigate</span>
|
||||
<Check size={18} strokeWidth={2.5} aria-hidden="true" />
|
||||
<span className="text-xs font-semibold">Collect</span>
|
||||
</button>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center gap-1 py-3 rounded-xl bg-[rgba(38,37,30,0.06)] text-[rgba(38,37,30,0.4)] min-h-[56px]" aria-label="No GPS coordinates available">
|
||||
<Navigation size={18} aria-hidden="true" />
|
||||
<span className="text-xs font-medium">No GPS</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => onProblem(item)}
|
||||
aria-label={`Report problem with ${description}`}
|
||||
className="flex flex-col items-center justify-center gap-1 py-3 rounded-xl bg-[#cf2d56] text-white active:bg-[#8a1b3a] transition-colors touch-manipulation press-feedback-sm min-h-[56px]"
|
||||
>
|
||||
<AlertTriangle size={18} strokeWidth={2.5} aria-hidden="true" />
|
||||
<span className="text-xs font-semibold">Problem</span>
|
||||
</button>
|
||||
|
||||
{hasCoords && coords && supplier ? (
|
||||
<button
|
||||
onClick={() => onNavigate(coords.latitude, coords.longitude, supplier.suppliername)}
|
||||
aria-label={`Navigate to ${supplier.suppliername}`}
|
||||
className="flex flex-col items-center justify-center gap-1 py-3 rounded-xl bg-[#f54e00] text-white active:bg-[#d44200] transition-colors touch-manipulation press-feedback-sm min-h-[56px]"
|
||||
>
|
||||
<Navigation size={18} strokeWidth={2.5} aria-hidden="true" />
|
||||
<span className="text-xs font-semibold">Navigate</span>
|
||||
</button>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center gap-1 py-3 rounded-xl bg-[rgba(38,37,30,0.06)] text-[rgba(38,37,30,0.4)] min-h-[56px]" aria-label="No GPS coordinates available">
|
||||
<Navigation size={18} aria-hidden="true" />
|
||||
<span className="text-xs font-medium">No GPS</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Row 2 — PO document and buyer notes */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
onClick={() => onShowDetails(item)}
|
||||
aria-label={`View PO document for ${description}`}
|
||||
className="flex flex-col items-center justify-center gap-1 py-3 rounded-xl bg-[#6c5a99] text-white active:bg-[#4a3d6b] transition-colors touch-manipulation press-feedback-sm min-h-[56px]"
|
||||
>
|
||||
<FileText size={18} strokeWidth={2.5} aria-hidden="true" />
|
||||
<span className="text-xs font-semibold">View PO</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => onShowDetails(item)}
|
||||
aria-label={`View buyer notes for ${description}`}
|
||||
className="flex flex-col items-center justify-center gap-1 py-3 rounded-xl bg-[#8a5a02] text-white active:bg-[#6b4501] transition-colors touch-manipulation press-feedback-sm min-h-[56px]"
|
||||
>
|
||||
<MessageSquare size={18} strokeWidth={2.5} aria-hidden="true" />
|
||||
<span className="text-xs font-semibold">Buyer Notes</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
308
components/ItemDetailModal.tsx
Normal file
308
components/ItemDetailModal.tsx
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
'use client';
|
||||
|
||||
import { OrderItem, PurchaseOrder } from '@/lib/types';
|
||||
import { pb } from '@/lib/pocketbase';
|
||||
import {
|
||||
X,
|
||||
FileText,
|
||||
Share2,
|
||||
Mail,
|
||||
Check,
|
||||
AlertCircle,
|
||||
Copy,
|
||||
ExternalLink,
|
||||
} from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface ItemDetailModalProps {
|
||||
item: OrderItem;
|
||||
purchaseOrder: PurchaseOrder | null;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
// PocketBase's pb.files.getUrl expects a record object with id and
|
||||
// collectionId. We satisfy the contract with a typed inline object rather
|
||||
// than a broader `any` cast.
|
||||
function buildPoFileUrl(po: PurchaseOrder): string | null {
|
||||
if (!po.documents?.length) return null;
|
||||
const record = {
|
||||
id: po.id,
|
||||
collectionId: (po as unknown as { collectionId: string }).collectionId ?? '',
|
||||
};
|
||||
return pb.files.getUrl(record, po.documents[0]);
|
||||
}
|
||||
|
||||
function statusColor(status: string): string {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'sent':
|
||||
case 'approved':
|
||||
return 'bg-[#e6f1ec] text-[#125a3e]';
|
||||
case 'pending':
|
||||
return 'bg-[#fcf3d8] text-[#8a5a02]';
|
||||
case 'cancelled':
|
||||
return 'bg-[#fbeaef] text-[#8a1b3a]';
|
||||
default:
|
||||
return 'bg-[#ebeae5] text-[#26251e]';
|
||||
}
|
||||
}
|
||||
|
||||
export default function ItemDetailModal({
|
||||
item,
|
||||
purchaseOrder,
|
||||
isOpen,
|
||||
onClose,
|
||||
}: ItemDetailModalProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [sharing, setSharing] = useState(false);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const poUrl = purchaseOrder ? buildPoFileUrl(purchaseOrder) : null;
|
||||
const supplier = item.expand?.supplier;
|
||||
const hasNotes = Boolean(item.notes?.trim());
|
||||
|
||||
const handleShare = async () => {
|
||||
if (!poUrl || !purchaseOrder) return;
|
||||
setSharing(true);
|
||||
try {
|
||||
if (navigator.share) {
|
||||
await navigator.share({
|
||||
title: `Purchase Order ${purchaseOrder.po_number}`,
|
||||
text: `Purchase Order ${purchaseOrder.po_number} for ${supplier?.suppliername ?? 'your order'}`,
|
||||
url: poUrl,
|
||||
});
|
||||
} else {
|
||||
await navigator.clipboard.writeText(poUrl);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2500);
|
||||
}
|
||||
} catch (err) {
|
||||
if ((err as Error).name !== 'AbortError') {
|
||||
console.error('Share failed:', err);
|
||||
}
|
||||
} finally {
|
||||
setSharing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEmail = () => {
|
||||
if (!purchaseOrder?.email_payload) return;
|
||||
const { subject, bodyText } = purchaseOrder.email_payload;
|
||||
const bodyWithLink = poUrl ? `${bodyText}\n\nPDF Download: ${poUrl}` : bodyText;
|
||||
const mailto = `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(bodyWithLink)}`;
|
||||
window.location.href = mailto;
|
||||
};
|
||||
|
||||
const handleCopyLink = async () => {
|
||||
if (!poUrl) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(poUrl);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2500);
|
||||
} catch {
|
||||
// clipboard may be blocked
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-[80] flex items-end sm:items-center justify-center"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Item details"
|
||||
>
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/30 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Sheet */}
|
||||
<div className="relative w-full max-w-lg bg-[#ebeae5] rounded-t-2xl sm:rounded-2xl shadow-xl flex flex-col max-h-[90dvh] overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-5 py-4 border-b border-[rgba(38,37,30,0.1)]">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-xl bg-[#1f8a65] flex items-center justify-center">
|
||||
<FileText size={18} className="text-white" aria-hidden="true" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-[15px] font-semibold text-[#26251e] leading-tight">
|
||||
{purchaseOrder?.po_number ?? 'No PO'}
|
||||
</h2>
|
||||
{supplier && (
|
||||
<p className="text-[11px] text-[rgba(38,37,30,0.55)] leading-tight mt-0.5">
|
||||
{supplier.suppliername}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
aria-label="Close details"
|
||||
className="w-9 h-9 rounded-xl flex items-center justify-center active:bg-[rgba(38,37,30,0.08)] transition-colors"
|
||||
>
|
||||
<X size={20} className="text-[#26251e]" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Scrollable body */}
|
||||
<div className="overflow-y-auto flex-1 p-5 space-y-5">
|
||||
{/* PO summary grid */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{purchaseOrder?.status && (
|
||||
<div className="bg-[#f2f1ed] rounded-xl p-3">
|
||||
<p className="text-[10px] text-[rgba(38,37,30,0.55)] uppercase tracking-wide mb-1">
|
||||
Status
|
||||
</p>
|
||||
<span
|
||||
className={`inline-block text-xs font-semibold px-2 py-0.5 rounded-lg ${statusColor(
|
||||
purchaseOrder.status
|
||||
)}`}
|
||||
>
|
||||
{purchaseOrder.status}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{purchaseOrder?.total_amount != null && (
|
||||
<div className="bg-[#f2f1ed] rounded-xl p-3">
|
||||
<p className="text-[10px] text-[rgba(38,37,30,0.55)] uppercase tracking-wide mb-1">
|
||||
PO Total
|
||||
</p>
|
||||
<p className="text-[14px] font-semibold text-[#26251e]">
|
||||
R{(purchaseOrder.total_amount / 100).toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{item.expand?.order?.order_number && (
|
||||
<div className="bg-[#f2f1ed] rounded-xl p-3">
|
||||
<p className="text-[10px] text-[rgba(38,37,30,0.55)] uppercase tracking-wide mb-1">
|
||||
Order Ref
|
||||
</p>
|
||||
<p className="text-[13px] font-medium text-[#26251e] leading-tight">
|
||||
{item.expand.order.order_number}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{supplier?.supplieraddress && (
|
||||
<div className="bg-[#f2f1ed] rounded-xl p-3 col-span-2">
|
||||
<p className="text-[10px] text-[rgba(38,37,30,0.55)] uppercase tracking-wide mb-1">
|
||||
Supplier Address
|
||||
</p>
|
||||
<p className="text-[13px] font-medium text-[#26251e] leading-snug selectable">
|
||||
{supplier.supplieraddress}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* PO Document actions */}
|
||||
{poUrl ? (
|
||||
<div className="space-y-3">
|
||||
<p className="text-[10px] text-[rgba(38,37,30,0.55)] uppercase tracking-wide">
|
||||
Attached Document
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<a
|
||||
href={poUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-[#26251e] text-white active:bg-[#3a3832] transition-colors touch-manipulation press-feedback-sm text-[13px] font-semibold"
|
||||
>
|
||||
<ExternalLink size={15} aria-hidden="true" />
|
||||
View / Download PDF
|
||||
</a>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
onClick={handleShare}
|
||||
disabled={sharing}
|
||||
className="flex items-center justify-center gap-2 py-3 rounded-xl bg-[#1f8a65] text-white active:bg-[#125a3e] transition-colors touch-manipulation press-feedback-sm text-[13px] font-semibold disabled:opacity-60"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<Check size={15} aria-hidden="true" />
|
||||
Copied!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Share2 size={15} aria-hidden="true" />
|
||||
Share PDF
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{purchaseOrder?.email_payload ? (
|
||||
<button
|
||||
onClick={handleEmail}
|
||||
className="flex items-center justify-center gap-2 py-3 rounded-xl bg-[#6c5a99] text-white active:bg-[#4a3d6b] transition-colors touch-manipulation press-feedback-sm text-[13px] font-semibold"
|
||||
>
|
||||
<Mail size={15} aria-hidden="true" />
|
||||
Email PO
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleCopyLink}
|
||||
className="flex items-center justify-center gap-2 py-3 rounded-xl bg-[#6c5a99] text-white active:bg-[#4a3d6b] transition-colors touch-manipulation press-feedback-sm text-[13px] font-semibold"
|
||||
>
|
||||
<Copy size={15} aria-hidden="true" />
|
||||
Copy Link
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{copied && (
|
||||
<p className="text-[11px] text-center text-[#125a3e] font-medium">
|
||||
Link copied to clipboard
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 py-3 px-4 rounded-xl bg-[#fcf3d8] border border-[#f6e3a4]">
|
||||
<AlertCircle size={15} className="text-[#8a5a02] flex-shrink-0" />
|
||||
<p className="text-[12px] text-[#8a5a02]">No document attached to this PO</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Buyer Notes */}
|
||||
{hasNotes && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-[10px] text-[rgba(38,37,30,0.55)] uppercase tracking-wide">
|
||||
Buyer Notes
|
||||
</p>
|
||||
<div className="bg-[#fcf3d8] border border-[#f6e3a4] rounded-xl p-4">
|
||||
<p className="text-[13px] text-[#8a5a02] leading-relaxed selectable">
|
||||
{item.notes}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Collection / problem notes */}
|
||||
{item.problem_notes && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-[10px] text-[rgba(38,37,30,0.55)] uppercase tracking-wide">
|
||||
Collection Notes
|
||||
</p>
|
||||
<div className="bg-[#fbeaef] border border-[#f3c4d2] rounded-xl p-4">
|
||||
<p className="text-[13px] text-[#8a1b3a] leading-relaxed selectable">
|
||||
{item.problem_notes}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-5 pt-3">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-full py-3.5 rounded-xl bg-[#26251e] text-white active:bg-[#3a3832] transition-colors touch-manipulation press-feedback-sm text-[14px] font-semibold"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { OrderItem, Supplier } from '@/lib/types';
|
||||
import { cn, parseCoordinates } from '@/lib/utils';
|
||||
import CollectionItemCard from '@/components/CollectionItemCard';
|
||||
|
|
@ -13,10 +14,14 @@ import {
|
|||
Hammer,
|
||||
Wrench,
|
||||
AlertTriangle,
|
||||
FileText,
|
||||
MessageSquare,
|
||||
ExternalLink,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface SupplierColor {
|
||||
dot: string;
|
||||
border: string;
|
||||
light: string;
|
||||
text: string;
|
||||
ring: string;
|
||||
|
|
@ -32,6 +37,7 @@ interface SupplierGroupProps {
|
|||
onCollect: (item: OrderItem) => void;
|
||||
onProblem: (item: OrderItem) => void;
|
||||
onNavigate: (lat: number, lng: number, name: string) => void;
|
||||
onShowDetails: (item: OrderItem) => void;
|
||||
}
|
||||
|
||||
function getItemIcon(item: OrderItem) {
|
||||
|
|
@ -53,17 +59,36 @@ export default function SupplierGroup({
|
|||
onCollect,
|
||||
onProblem,
|
||||
onNavigate,
|
||||
onShowDetails,
|
||||
}: SupplierGroupProps) {
|
||||
const coords = parseCoordinates(supplier.coordinates);
|
||||
const hasCoords = coords !== null;
|
||||
const totalQty = items.reduce((sum, i) => sum + (i.order_qty || i.quantity || 0), 0);
|
||||
|
||||
// Active tooltip: { itemId, type }
|
||||
const [activeTooltip, setActiveTooltip] = useState<{ itemId: string; type: 'po' | 'notes' } | null>(null);
|
||||
const listRef = useRef<HTMLUListElement>(null);
|
||||
|
||||
// Close tooltip when clicking outside the list
|
||||
useEffect(() => {
|
||||
if (!activeTooltip) return;
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (listRef.current && !listRef.current.contains(e.target as Node)) {
|
||||
setActiveTooltip(null);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handler);
|
||||
return () => document.removeEventListener('mousedown', handler);
|
||||
}, [activeTooltip]);
|
||||
return (
|
||||
<section
|
||||
className="bg-[#e6e5e0] rounded-2xl overflow-hidden transition-all duration-200 border border-[rgba(38,37,30,0.1)]"
|
||||
className={cn(
|
||||
'relative z-10 bg-[#e6e5e0] rounded-2xl overflow-visible transition-all duration-200 border border-[rgba(38,37,30,0.1)]',
|
||||
'border-l-[6px]',
|
||||
color.border
|
||||
)}
|
||||
aria-label={`${supplier.suppliername} collection group`}
|
||||
>
|
||||
{/* Supplier Header — always visible, tap to expand/collapse */}
|
||||
<button
|
||||
onClick={onToggle}
|
||||
aria-expanded={isExpanded}
|
||||
|
|
@ -128,15 +153,25 @@ export default function SupplierGroup({
|
|||
className="border-t border-[rgba(38,37,30,0.08)] animate-fade-in"
|
||||
>
|
||||
{viewMode === 'compact' ? (
|
||||
/* COMPACT MODE: simple list with item name + qty */
|
||||
<ul className="divide-y divide-[rgba(38,37,30,0.05)]" role="list">
|
||||
/* COMPACT MODE: simple list with FileText + MessageSquare buttons */
|
||||
<ul ref={listRef} className="divide-y divide-[rgba(38,37,30,0.05)]" role="list">
|
||||
{items.map((item) => {
|
||||
const expectedQty = item.order_qty || item.quantity || 0;
|
||||
const po = item.expand?.purchase_order;
|
||||
const poNumber = po?.po_number;
|
||||
const orderNumber = item.expand?.order?.order_number;
|
||||
const hasNotes = Boolean(item.notes);
|
||||
const hasProblemNotes = Boolean(item.problem_notes);
|
||||
const hasAnyNotes = hasNotes || hasProblemNotes;
|
||||
const isPoOpen = activeTooltip?.itemId === item.id && activeTooltip?.type === 'po';
|
||||
const isNotesOpen = activeTooltip?.itemId === item.id && activeTooltip?.type === 'notes';
|
||||
|
||||
return (
|
||||
<li
|
||||
key={item.id}
|
||||
className="flex items-center justify-between px-4 py-3 hover:bg-[#ebeae5] transition-colors"
|
||||
className="relative flex items-center justify-between px-4 py-3 hover:bg-[#ebeae5] transition-colors"
|
||||
>
|
||||
{/* Left: icon + description */}
|
||||
<div className="flex items-center gap-2.5 min-w-0 flex-1">
|
||||
{getItemIcon(item)}
|
||||
<span className="text-sm text-[#26251e] truncate">
|
||||
|
|
@ -148,8 +183,143 @@ export default function SupplierGroup({
|
|||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3 flex-shrink-0 ml-2">
|
||||
|
||||
{/* Right: qty + FileText + Collect/Problem */}
|
||||
<div className="flex items-center gap-2 flex-shrink-0 ml-2">
|
||||
<span className="text-sm font-semibold text-[rgba(38,37,30,0.7)]">{expectedQty}×</span>
|
||||
|
||||
{/* PO FileText button */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setActiveTooltip(isPoOpen ? null : { itemId: item.id, type: 'po' });
|
||||
}}
|
||||
aria-label="Show PO details"
|
||||
aria-expanded={isPoOpen}
|
||||
className="w-8 h-8 rounded-lg flex items-center justify-center bg-[#6c5a99] text-white active:bg-[#4a3d6b] transition-colors"
|
||||
>
|
||||
<FileText size={13} aria-hidden="true" />
|
||||
</button>
|
||||
{/* PO tooltip popover */}
|
||||
{isPoOpen && (
|
||||
<div
|
||||
className="absolute right-0 z-50 mt-1 w-64 bg-white border border-[rgba(38,37,30,0.15)] rounded-xl shadow-lg p-3"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<p className="text-[11px] font-semibold text-[#26251e] uppercase tracking-wide">Purchase Order</p>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setActiveTooltip(null); }}
|
||||
aria-label="Close"
|
||||
className="w-5 h-5 rounded flex items-center justify-center hover:bg-[rgba(38,37,30,0.08)]"
|
||||
>
|
||||
<span className="text-[rgba(38,37,30,0.4)] text-sm">×</span>
|
||||
</button>
|
||||
</div>
|
||||
{po ? (
|
||||
<>
|
||||
<dl className="space-y-1.5 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-[rgba(38,37,30,0.55)]">PO Number</dt>
|
||||
<dd className="text-[#26251e] font-medium font-mono">{po.po_number}</dd>
|
||||
</div>
|
||||
{po.status && (
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-[rgba(38,37,30,0.55)]">Status</dt>
|
||||
<dd className="text-[#26251e] font-medium capitalize">{po.status}</dd>
|
||||
</div>
|
||||
)}
|
||||
{po.total_amount != null && (
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-[rgba(38,37,30,0.55)]">Total</dt>
|
||||
<dd className="text-[#26251e] font-medium">R{(po.total_amount / 100).toFixed(2)}</dd>
|
||||
</div>
|
||||
)}
|
||||
{orderNumber && (
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-[rgba(38,37,30,0.55)]">Order</dt>
|
||||
<dd className="text-[#26251e] font-medium">{orderNumber}</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setActiveTooltip(null); onShowDetails(item); }}
|
||||
className="mt-3 w-full flex items-center justify-center gap-2 py-2 rounded-lg bg-[#6c5a99] text-white text-[12px] font-semibold active:bg-[#4a3d6b] transition-colors"
|
||||
>
|
||||
<ExternalLink size={13} aria-hidden="true" />
|
||||
View PO & Document
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-2">
|
||||
<p className="text-xs text-[rgba(38,37,30,0.55)]">No PO attached</p>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setActiveTooltip(null); onShowDetails(item); }}
|
||||
className="mt-2 text-[11px] text-[#6c5a99] font-medium hover:underline"
|
||||
>
|
||||
View details
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Notes MessageSquare button — only when notes exist */}
|
||||
{hasAnyNotes && (
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setActiveTooltip(isNotesOpen ? null : { itemId: item.id, type: 'notes' });
|
||||
}}
|
||||
aria-label="Show notes"
|
||||
aria-expanded={isNotesOpen}
|
||||
className="w-8 h-8 rounded-lg flex items-center justify-center bg-[#8a5a02] text-white active:bg-[#6b4501] transition-colors"
|
||||
>
|
||||
<MessageSquare size={13} aria-hidden="true" />
|
||||
</button>
|
||||
{/* Notes tooltip popover */}
|
||||
{isNotesOpen && (
|
||||
<div
|
||||
className="absolute right-0 z-50 mt-1 w-64 bg-white border border-[rgba(38,37,30,0.15)] rounded-xl shadow-lg p-3"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<p className="text-[11px] font-semibold text-[#26251e] uppercase tracking-wide">Buyer Notes</p>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setActiveTooltip(null); }}
|
||||
aria-label="Close"
|
||||
className="w-5 h-5 rounded flex items-center justify-center hover:bg-[rgba(38,37,30,0.08)]"
|
||||
>
|
||||
<span className="text-[rgba(38,37,30,0.4)] text-sm">×</span>
|
||||
</button>
|
||||
</div>
|
||||
{hasNotes ? (
|
||||
<>
|
||||
<p className="text-[11px] text-[#8a5a02] bg-[#fcf3d8] border border-[#f6e3a4] rounded-lg p-2 selectable leading-snug">
|
||||
{item.notes}
|
||||
</p>
|
||||
{hasProblemNotes && (
|
||||
<p className="text-[11px] text-[#8a1b3a] bg-[#fbeaef] border border-[#f3c4d2] rounded-lg p-2 mt-2 selectable leading-snug">
|
||||
<span className="font-semibold">Problem: </span>{item.problem_notes}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
) : hasProblemNotes ? (
|
||||
<p className="text-[11px] text-[#8a1b3a] bg-[#fbeaef] border border-[#f3c4d2] rounded-lg p-2 selectable leading-snug">
|
||||
{item.problem_notes}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-xs text-[rgba(38,37,30,0.55)] text-center py-1">No notes</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Collect + Problem buttons */}
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => onCollect(item)}
|
||||
|
|
@ -161,7 +331,7 @@ export default function SupplierGroup({
|
|||
<button
|
||||
onClick={() => onProblem(item)}
|
||||
aria-label={`Report problem for ${item.description || 'item'}`}
|
||||
className="w-10 h-10 rounded-lg bg-[#cf2d56] text-white flex items-center justify-center active:bg-[#8a1b3a] transition-colors touch-manipulation min-h-[40px] min-w-[40px]"
|
||||
className="w-10 h-10 rounded-lg bg-[#cf2d56] text-white flex items-center justify-center active:bg-[#8a1a3a] transition-colors touch-manipulation min-h-[40px] min-w-[40px]"
|
||||
>
|
||||
<AlertTriangle size={16} aria-hidden="true" />
|
||||
</button>
|
||||
|
|
@ -183,6 +353,7 @@ export default function SupplierGroup({
|
|||
onCollect={onCollect}
|
||||
onProblem={onProblem}
|
||||
onNavigate={onNavigate}
|
||||
onShowDetails={onShowDetails}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -238,49 +238,60 @@ export const updateCollectionStatus = async (
|
|||
};
|
||||
|
||||
// ---------- Attachments ----------
|
||||
// Upload a file (typically a pickup photo) as an OrderItemAttachment
|
||||
// row in PB's `order_item_attachments` collection. Mirrors the
|
||||
// quote_item_attachments flow: parent via the `order_item` relation,
|
||||
// file carried in the `file` field, plus content_type / size_bytes
|
||||
// for the operator side to surface in their list view.
|
||||
// Upload a pickup photo by appending it to the order_item's
|
||||
// `order_item_attachements` file field (PB file field, max multi).
|
||||
// Uses the `fieldName+` append syntax so each upload is preserved
|
||||
// alongside any previous files rather than replacing them.
|
||||
//
|
||||
// Returns the new attachment record. Throws on PB errors so the
|
||||
// caller can roll back the status change (otherwise the driver
|
||||
// sees a "Collected" toast while the photo is lost).
|
||||
// The source/notes params are accepted for interface compatibility
|
||||
// but cannot be stored per-file without a separate attachment collection.
|
||||
// Both are safe to drop here since the driver always uses
|
||||
// source=driver_proof and notes go into order_items.notes separately.
|
||||
//
|
||||
// Throws on PB errors so the caller can roll back the status change.
|
||||
export const uploadOrderItemAttachment = async (params: {
|
||||
orderItemId: string;
|
||||
file: File;
|
||||
source?: AttachmentSource;
|
||||
notes?: string;
|
||||
}): Promise<OrderItemAttachment> => {
|
||||
const { orderItemId, file, source = 'driver_proof', notes } = params;
|
||||
const { orderItemId, file } = params;
|
||||
|
||||
// Build a FormData payload. PB's create() accepts a plain object
|
||||
// with the file property, which it then serializes as multipart.
|
||||
const formData = new FormData();
|
||||
formData.append('order_item', orderItemId);
|
||||
formData.append('filename', file.name);
|
||||
formData.append('file', file);
|
||||
formData.append('content_type', file.type || '');
|
||||
if (typeof file.size === 'number') {
|
||||
formData.append('size_bytes', String(file.size));
|
||||
}
|
||||
formData.append('source', source);
|
||||
if (notes) formData.append('notes', notes);
|
||||
// fieldName+ = append (never overwrite) existing files on the row
|
||||
formData.append('order_item_attachements+', file);
|
||||
|
||||
const created = await pb.collection('order_item_attachments').create(formData);
|
||||
return created as unknown as OrderItemAttachment;
|
||||
const updated = await pb.collection('order_items').update(orderItemId, formData);
|
||||
// PB returns the updated order_item; extract the last filename as the
|
||||
// "id" for the returned OrderItemAttachment shape.
|
||||
const filenames: string[] = (updated as unknown as { order_item_attachements?: string[] }).order_item_attachements ?? [];
|
||||
const filename = filenames[filenames.length - 1] ?? file.name;
|
||||
|
||||
return {
|
||||
id: filename, // no separate attachment id; filename is the key
|
||||
order_item: orderItemId,
|
||||
filename,
|
||||
file: filename,
|
||||
source: 'driver_proof',
|
||||
uploaded_at: new Date().toISOString(),
|
||||
} as unknown as OrderItemAttachment;
|
||||
};
|
||||
|
||||
// List attachments for a single order item. Newest first.
|
||||
// List attachment filenames for an order item by fetching the file list
|
||||
// from the order_item_attachements file field on the order_items row.
|
||||
export const listOrderItemAttachments = async (
|
||||
orderItemId: string
|
||||
): Promise<OrderItemAttachment[]> => {
|
||||
const records = await pb.collection('order_item_attachments').getFullList({
|
||||
filter: `order_item = "${orderItemId}"`,
|
||||
sort: '-uploaded_at',
|
||||
});
|
||||
return records as unknown as OrderItemAttachment[];
|
||||
const item = await pb.collection('order_items').getOne(orderItemId);
|
||||
const filenames: string[] = (item as unknown as { order_item_attachements?: string[] }).order_item_attachements ?? [];
|
||||
return filenames.map((filename) => ({
|
||||
id: filename,
|
||||
order_item: orderItemId,
|
||||
filename,
|
||||
file: filename,
|
||||
source: 'driver_proof' as const,
|
||||
uploaded_at: '',
|
||||
})) as unknown as OrderItemAttachment[];
|
||||
};
|
||||
|
||||
// ---------- Display helpers ----------
|
||||
|
|
|
|||
37
lib/types.ts
37
lib/types.ts
|
|
@ -20,6 +20,28 @@ export interface PurchaseOrder {
|
|||
id: string;
|
||||
po_number: string;
|
||||
status: string;
|
||||
// Attached PO PDF documents (file field on the purchase_orders PB collection)
|
||||
documents?: string[];
|
||||
// Pre-composed email payload (subject, body, to) — driver can resend
|
||||
// this to the supplier via the "Email PO" button when the supplier
|
||||
// hasn't received it.
|
||||
email_payload?: {
|
||||
to: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
bodyText: string;
|
||||
cc?: string;
|
||||
bcc?: string;
|
||||
};
|
||||
// Linked supplier (expanded)
|
||||
expand?: {
|
||||
supplier?: Supplier;
|
||||
};
|
||||
supplier?: string;
|
||||
total_amount?: number;
|
||||
order?: string;
|
||||
order_item?: string;
|
||||
updated?: string;
|
||||
}
|
||||
|
||||
export interface OrderItem {
|
||||
|
|
@ -81,7 +103,14 @@ export interface OrderItem {
|
|||
// uploads go through the order_item_attachments collection so we
|
||||
// can carry multiple files per pickup (camera, library, signed
|
||||
// delivery note scan) without overwriting history.
|
||||
collection_photo?: string;
|
||||
// Multiple pickup proof files (photos, signed slips, etc.) appended
|
||||
// directly to the order_item row via PB's file field. Each upload
|
||||
expand?: {
|
||||
supplier?: Supplier; // PB expand key matches relation field name
|
||||
order?: Order;
|
||||
purchase_order?: PurchaseOrder;
|
||||
};
|
||||
order_item_attachements?: string[];
|
||||
// Per-row quantities used for proving/dashboard math.
|
||||
qty_processed?: number;
|
||||
qty_assigned?: number;
|
||||
|
|
@ -97,12 +126,6 @@ export interface OrderItem {
|
|||
is_stock_item?: boolean;
|
||||
collected_date?: string;
|
||||
collection_date?: string;
|
||||
// Expanded fields
|
||||
expand?: {
|
||||
supplier?: Supplier; // PB expand key matches relation field name
|
||||
order?: Order;
|
||||
purchase_order?: PurchaseOrder;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DriverLocation {
|
||||
|
|
|
|||
90
package-lock.json
generated
90
package-lock.json
generated
|
|
@ -278,9 +278,6 @@
|
|||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -298,9 +295,6 @@
|
|||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -318,9 +312,6 @@
|
|||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -338,9 +329,6 @@
|
|||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -358,9 +346,6 @@
|
|||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -378,9 +363,6 @@
|
|||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -398,9 +380,6 @@
|
|||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -418,9 +397,6 @@
|
|||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -438,9 +414,6 @@
|
|||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -464,9 +437,6 @@
|
|||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -490,9 +460,6 @@
|
|||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -516,9 +483,6 @@
|
|||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -542,9 +506,6 @@
|
|||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -568,9 +529,6 @@
|
|||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -594,9 +552,6 @@
|
|||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -620,9 +575,6 @@
|
|||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -878,9 +830,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -897,9 +846,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -916,9 +862,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -935,9 +878,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1563,9 +1503,6 @@
|
|||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1580,9 +1517,6 @@
|
|||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1597,9 +1531,6 @@
|
|||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1614,9 +1545,6 @@
|
|||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1631,9 +1559,6 @@
|
|||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1648,9 +1573,6 @@
|
|||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1665,9 +1587,6 @@
|
|||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1682,9 +1601,6 @@
|
|||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1699,9 +1615,6 @@
|
|||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1716,9 +1629,6 @@
|
|||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 4001",
|
||||
"dev": "next dev -p 4002",
|
||||
"build": "next build",
|
||||
"export": "NEXT_EXPORT=true next build",
|
||||
"start": "next start",
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ const FILTERS = [
|
|||
{ collection: 'order_items', filter: 'drv_id = "placeholder" && item_status = "assigned"' },
|
||||
{ collection: 'order_items', filter: 'item_status = "collection_pool"' },
|
||||
{ collection: 'order_items', filter: 'problem_flag = true || item_status = "problem"' },
|
||||
{ collection: 'order_item_attachments', filter: 'order_item = "placeholder"' },
|
||||
{ collection: 'driver_locations', filter: 'relation = "placeholder" && timestamp >= "2026-01-01T00:00:00Z"' },
|
||||
];
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue