// ================================================================ // frontend/src/components/dashboard/StockReceiptModal.tsx // ================================================================ /** * Stock Receipt Modal - Lot-Level Tracking * * Complete workflow for receiving deliveries with lot-level expiration tracking. * Critical for food safety compliance. * * Features: * - Multi-line item support (one per ingredient) * - Lot splitting (e.g., 50kg → 2×25kg lots with different expiration dates) * - Mandatory expiration dates * - Quantity validation (lot quantities must sum to actual quantity) * - Discrepancy tracking (expected vs actual) * - Draft save functionality * - Warehouse location tracking */ import React, { useState, useEffect } from 'react'; import { X, Plus, Trash2, Save, CheckCircle, AlertTriangle, Package, Calendar, MapPin, FileText, Truck, } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { Button } from '../ui/Button'; // ============================================================ // Types // ============================================================ export interface StockLot { id?: string; lot_number?: string; supplier_lot_number?: string; quantity: number; unit_of_measure: string; expiration_date: string; // ISO date string (YYYY-MM-DD) best_before_date?: string; warehouse_location?: string; storage_zone?: string; quality_notes?: string; } export interface StockReceiptLineItem { id?: string; ingredient_id: string; ingredient_name: string; po_line_id?: string; expected_quantity: number; actual_quantity: number; unit_of_measure: string; has_discrepancy: boolean; discrepancy_reason?: string; unit_cost?: number; total_cost?: number; lots: StockLot[]; } export interface StockReceipt { id?: string; tenant_id: string; po_id: string; po_number?: string; received_at?: string; received_by_user_id: string; status?: 'draft' | 'confirmed' | 'cancelled'; supplier_id?: string; supplier_name?: string; notes?: string; has_discrepancies?: boolean; line_items: StockReceiptLineItem[]; } interface StockReceiptModalProps { isOpen: boolean; onClose: () => void; receipt: Partial; mode?: 'create' | 'edit'; onSaveDraft?: (receipt: StockReceipt) => Promise; onConfirm?: (receipt: StockReceipt) => Promise; } // ============================================================ // Helper Functions // ============================================================ function calculateLotQuantitySum(lots: StockLot[]): number { return lots.reduce((sum, lot) => sum + (lot.quantity || 0), 0); } function hasDiscrepancy(expected: number, actual: number): boolean { return Math.abs(expected - actual) > 0.01; } // ============================================================ // Sub-Components // ============================================================ interface LotInputProps { lot: StockLot; lineItemUoM: string; onChange: (updatedLot: StockLot) => void; onRemove: () => void; canRemove: boolean; } function LotInput({ lot, lineItemUoM, onChange, onRemove, canRemove }: LotInputProps) { const { t } = useTranslation('inventory'); return (
{/* Lot Header */}
{t('lot_details')}
{canRemove && ( )}
{/* Quantity (Required) */}
onChange({ ...lot, quantity: parseFloat(e.target.value) || 0 })} className="w-full px-3 py-2 rounded-lg border" style={{ backgroundColor: 'var(--bg-primary)', borderColor: 'var(--border-primary)', color: 'var(--text-primary)', }} required />
{/* Expiration Date (Required) */}
onChange({ ...lot, expiration_date: e.target.value })} className="w-full px-3 py-2 rounded-lg border" style={{ backgroundColor: 'var(--bg-primary)', borderColor: 'var(--border-primary)', color: 'var(--text-primary)', }} required />
{/* Lot Number (Optional) */}
onChange({ ...lot, lot_number: e.target.value })} placeholder="LOT-2024-001" className="w-full px-3 py-2 rounded-lg border" style={{ backgroundColor: 'var(--bg-primary)', borderColor: 'var(--border-primary)', color: 'var(--text-primary)', }} />
{/* Supplier Lot Number (Optional) */}
onChange({ ...lot, supplier_lot_number: e.target.value })} placeholder="SUPP-LOT-123" className="w-full px-3 py-2 rounded-lg border" style={{ backgroundColor: 'var(--bg-primary)', borderColor: 'var(--border-primary)', color: 'var(--text-primary)', }} />
{/* Warehouse Location (Optional) */}
onChange({ ...lot, warehouse_location: e.target.value })} placeholder="A-01-03" className="w-full px-3 py-2 rounded-lg border" style={{ backgroundColor: 'var(--bg-primary)', borderColor: 'var(--border-primary)', color: 'var(--text-primary)', }} />
{/* Storage Zone (Optional) */}
onChange({ ...lot, storage_zone: e.target.value })} placeholder="Cold Storage" className="w-full px-3 py-2 rounded-lg border" style={{ backgroundColor: 'var(--bg-primary)', borderColor: 'var(--border-primary)', color: 'var(--text-primary)', }} />
{/* Quality Notes (Optional, full width) */}