WFDASHDRIVER-APP/components/CollectModal.tsx
SJCoetz db10eb95c7 Add PWA support and UI redesign
Add full PWA support (icons, manifest, service worker, pwa lib and bridge) and introduce install/offline UI. Generate app icons via a new scripts/generate-icons.js and include multiple icon sizes in public/icons; update manifest and layout head meta/preconnect. Revamp global styles (globals.css) with new utilities, animations, spacing for bottom nav, card shadows, skeletons and accessibility tweaks. Polish major screens (login, collections, map, settings) with improved headers, loading states, touch/press feedback, and layout adjustments; add new components (InstallPrompt, OfflineIndicator, PWABridge) and update various components to match new visual/UX patterns. Update Dockerfile to generate icons and use Next.js static export output (/out) for nginx, and bump package setup to support icon generation.
2026-05-27 00:34:50 +02:00

179 lines
No EOL
6.6 KiB
TypeScript

'use client';
import { useState } from 'react';
import { OrderItem } from '@/lib/types';
import { cn, formatCurrency } from '@/lib/utils';
import { X, Minus, Plus, Check } from 'lucide-react';
interface CollectModalProps {
item: OrderItem | null;
isOpen: boolean;
onClose: () => void;
onConfirm: (item: OrderItem, receivedQty: number, notes: string) => void;
}
export default function CollectModal({
item,
isOpen,
onClose,
onConfirm,
}: CollectModalProps) {
const [receivedQty, setReceivedQty] = useState('');
const [notes, setNotes] = useState('');
const [error, setError] = useState('');
if (!isOpen || !item) return null;
const expectedQty = item.order_qty || item.quantity || 0;
const parsedQty = parseFloat(receivedQty) || 0;
const isPartial = parsedQty > 0 && parsedQty < expectedQty;
const isOverQty = parsedQty > expectedQty;
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setError('');
const qty = parseFloat(receivedQty);
if (isNaN(qty) || qty <= 0) {
setError('Please enter a valid quantity');
return;
}
onConfirm(item, qty, notes);
setReceivedQty('');
setNotes('');
setError('');
};
const adjustQty = (delta: number) => {
const current = parseFloat(receivedQty) || 0;
const newQty = Math.max(0, current + delta);
setReceivedQty(newQty.toString());
setError('');
};
return (
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center">
<div className="absolute inset-0 bg-black/50 modal-overlay" onClick={onClose} />
<div className="relative bg-white bottom-sheet w-full max-w-md p-6 animate-slide-up">
{/* Drag indicator */}
<div className="sm:hidden flex justify-center mb-4">
<div className="w-10 h-1 bg-gray-300 rounded-full" />
</div>
<div className="flex items-center justify-between mb-5">
<div className="flex items-center gap-2.5">
<div className="w-8 h-8 bg-green-50 rounded-xl flex items-center justify-center">
<Check size={18} className="text-green-600" />
</div>
<h2 className="text-lg font-bold text-gray-900">Collect Item</h2>
</div>
<button
onClick={onClose}
className="p-2 rounded-xl hover:bg-gray-100 active:bg-gray-200 transition-colors"
>
<X size={18} className="text-gray-400" />
</button>
</div>
{/* Item info */}
<div className="mb-5 p-4 bg-gray-50 rounded-xl">
<p className="text-sm font-semibold text-gray-900 truncate">{item.description}</p>
<div className="flex items-center gap-3 mt-1.5 text-xs text-gray-500">
<span className="font-medium text-gray-700">
{item.expand?.supplier?.suppliername || 'Unknown'}
</span>
<span>Expected: {expectedQty} units</span>
</div>
{item.selling_price && (
<p className="text-xs text-green-600 font-medium mt-1">
Unit price: {formatCurrency(item.selling_price)}
</p>
)}
</div>
<form onSubmit={handleSubmit} className="space-y-5">
{/* Quantity */}
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">
Received Quantity *
</label>
<div className="flex items-center gap-3">
<button
type="button"
onClick={() => adjustQty(-1)}
className="w-12 h-12 flex items-center justify-center rounded-xl bg-gray-100 active:bg-gray-200 transition-colors touch-manipulation press-feedback-sm"
>
<Minus size={18} className="text-gray-600" />
</button>
<input
type="number"
step="0.01"
inputMode="decimal"
value={receivedQty}
onChange={(e) => { setReceivedQty(e.target.value); setError(''); }}
className="w-20 text-center text-2xl font-bold py-3 border-2 border-gray-200 rounded-xl focus:ring-2 focus:ring-green-500/30 focus:border-green-500 transition-all"
placeholder="0"
autoFocus
/>
<button
type="button"
onClick={() => adjustQty(1)}
className="w-12 h-12 flex items-center justify-center rounded-xl bg-gray-100 active:bg-gray-200 transition-colors touch-manipulation press-feedback-sm"
>
<Plus size={18} className="text-gray-600" />
</button>
{expectedQty > 0 && (
<button
type="button"
onClick={() => { setReceivedQty(expectedQty.toString()); setError(''); }}
className="px-3 py-2 text-xs font-medium text-green-700 bg-green-50 rounded-lg active:bg-green-100 transition-colors touch-manipulation"
>
Full ({expectedQty})
</button>
)}
</div>
{isPartial && (
<p className="text-xs text-amber-600 mt-2 font-medium">
Partial collection: {parsedQty} of {expectedQty} units
</p>
)}
{isOverQty && (
<p className="text-xs text-blue-600 mt-2 font-medium">
Over collection: {parsedQty} exceeds expected {expectedQty}
</p>
)}
{error && <p className="text-xs text-red-500 mt-2">{error}</p>}
</div>
{/* Notes */}
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">
Notes (optional)
</label>
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
className="w-full p-3 text-sm bg-gray-50 border border-gray-200 rounded-xl focus:ring-2 focus:ring-green-500/30 focus:border-green-500 focus:bg-white transition-all resize-none selectable"
rows={2}
placeholder="Add any notes about this collection..."
/>
</div>
{/* Submit */}
<button
type="submit"
className={cn(
'w-full py-4 px-4 rounded-xl text-white font-semibold text-[15px]',
'bg-green-500 active:bg-green-600 transition-all shadow-lg shadow-green-500/25',
'touch-manipulation press-feedback',
(!parsedQty || parsedQty < 0) && 'opacity-50'
)}
>
Confirm Collection
</button>
</form>
</div>
</div>
);
}