- Added package.json with dependencies for Next.js, React, and Tailwind CSS. - Created postcss.config.js for Tailwind CSS and autoprefixer integration. - Added SVG icon for the application in public/icons/icon.svg. - Created manifest.json for PWA support with app details and icon. - Configured Tailwind CSS in tailwind.config.js with custom colors and animations. - Set up TypeScript configuration in tsconfig.json for strict type checking and module resolution.
149 lines
5.1 KiB
TypeScript
149 lines
5.1 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { Collection } from '@/lib/types';
|
|
import { cn, formatCurrency } from '@/lib/utils';
|
|
import { X, Minus, Plus } from 'lucide-react';
|
|
|
|
interface CollectModalProps {
|
|
collection: Collection | null;
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onConfirm: (collection: Collection, receivedQty: number, notes: string) => void;
|
|
}
|
|
|
|
export default function CollectModal({
|
|
collection,
|
|
isOpen,
|
|
onClose,
|
|
onConfirm,
|
|
}: CollectModalProps) {
|
|
const [receivedQty, setReceivedQty] = useState<string>('');
|
|
const [notes, setNotes] = useState('');
|
|
const [error, setError] = useState('');
|
|
|
|
if (!isOpen || !collection) return null;
|
|
|
|
const expectedQty = collection.quantity || collection.expand?.order_item?.quantity || 0;
|
|
|
|
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(collection, qty, notes);
|
|
setReceivedQty('');
|
|
setNotes('');
|
|
setError('');
|
|
};
|
|
|
|
const adjustQty = (delta: number) => {
|
|
const current = parseFloat(receivedQty) || 0;
|
|
const newQty = Math.max(0, current + delta);
|
|
setReceivedQty(newQty.toString());
|
|
};
|
|
|
|
const isPartial = parseFloat(receivedQty) > 0 && parseFloat(receivedQty) < expectedQty;
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center">
|
|
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" onClick={onClose} />
|
|
<div className="relative bg-white rounded-t-2xl sm:rounded-2xl w-full max-w-md p-6 animate-slide-up">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-lg font-bold">Collect Item</h2>
|
|
<button
|
|
onClick={onClose}
|
|
className="p-1 rounded-full hover:bg-gray-100 active:bg-gray-200 transition-colors"
|
|
>
|
|
<X size={20} />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="mb-4 p-3 bg-gray-50 rounded-lg">
|
|
<p className="text-sm font-medium truncate">
|
|
{collection.is_manual
|
|
? collection.description
|
|
: collection.expand?.order_item?.description || 'Unknown item'}
|
|
</p>
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
Expected: {expectedQty} {collection.unit || collection.expand?.order_item?.unit || 'units'}
|
|
</p>
|
|
{collection.expand?.order_item?.selling_price && (
|
|
<p className="text-xs text-green-600 mt-1">
|
|
Unit price: {formatCurrency(collection.expand.order_item.selling_price)}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit}>
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Received Quantity *
|
|
</label>
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => adjustQty(-1)}
|
|
className="p-3 rounded-lg bg-gray-100 active:bg-gray-200 transition-colors"
|
|
>
|
|
<Minus size={18} />
|
|
</button>
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
inputMode="decimal"
|
|
value={receivedQty}
|
|
onChange={(e) => setReceivedQty(e.target.value)}
|
|
className="flex-1 p-3 text-center text-lg font-semibold border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-transparent"
|
|
placeholder="0.00"
|
|
autoFocus
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => adjustQty(1)}
|
|
className="p-3 rounded-lg bg-gray-100 active:bg-gray-200 transition-colors"
|
|
>
|
|
<Plus size={18} />
|
|
</button>
|
|
</div>
|
|
{isPartial && (
|
|
<p className="text-xs text-amber-600 mt-1">
|
|
Partial collection: {receivedQty} of {expectedQty} units
|
|
</p>
|
|
)}
|
|
{error && <p className="text-xs text-red-500 mt-1">{error}</p>}
|
|
</div>
|
|
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Notes (optional)
|
|
</label>
|
|
<textarea
|
|
value={notes}
|
|
onChange={(e) => setNotes(e.target.value)}
|
|
className="w-full p-3 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-transparent resize-none"
|
|
rows={2}
|
|
placeholder="Add any notes..."
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
className={cn(
|
|
'w-full py-3.5 px-4 rounded-xl text-white font-semibold text-base',
|
|
'bg-green-500 active:bg-green-600 transition-colors',
|
|
'touch-manipulation flex items-center justify-center gap-2'
|
|
)}
|
|
>
|
|
Confirm Collection
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|