// ================================================================ // frontend/src/components/dashboard/PurchaseOrderDetailsModal.tsx // ================================================================ /** * Purchase Order Details Modal * Unified view/edit modal for PO details from the Action Queue * Now using EditViewModal with proper API response structure */ import React, { useState, useMemo } from 'react'; import { Package, Building2, Calendar, Euro, FileText, CheckCircle, Edit, } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { usePurchaseOrder, useUpdatePurchaseOrder } from '../../api/hooks/purchase-orders'; import { useUserById } from '../../api/hooks/user'; import { EditViewModal, EditViewModalSection } from '../ui/EditViewModal/EditViewModal'; import type { PurchaseOrderItem } from '../../api/services/purchase_orders'; interface PurchaseOrderDetailsModalProps { poId: string; tenantId: string; isOpen: boolean; onClose: () => void; onApprove?: (poId: string) => void; initialMode?: 'view' | 'edit'; } export const PurchaseOrderDetailsModal: React.FC = ({ poId, tenantId, isOpen, onClose, onApprove, initialMode = 'view', }) => { const { t, i18n } = useTranslation(['purchase_orders', 'common']); const { data: po, isLoading, refetch } = usePurchaseOrder(tenantId, poId); const [mode, setMode] = useState<'view' | 'edit'>(initialMode); const updatePurchaseOrderMutation = useUpdatePurchaseOrder(); // Component to display user name with data fetching const UserName: React.FC<{ userId: string | undefined | null }> = ({ userId }) => { if (!userId) return <>{t('common:not_available')}; if (userId === '00000000-0000-0000-0000-000000000001' || userId === '00000000-0000-0000-0000-000000000000') { return <>{t('common:system')}; } const { data: user, isLoading } = useUserById(userId, { retry: 1, staleTime: 10 * 60 * 1000, }); if (isLoading) return <>{t('common:loading')}; if (!user) return <>{t('common:unknown_user')}; return <>{user.full_name || user.email || t('common:user')}; }; // Component to display PO items const PurchaseOrderItemsTable: React.FC<{ items: PurchaseOrderItem[] }> = ({ items }) => { if (!items || items.length === 0) { return (

{t('no_items')}

); } const totalAmount = items.reduce((sum, item) => { const price = parseFloat(item.unit_price) || 0; const quantity = item.ordered_quantity || 0; return sum + (price * quantity); }, 0); return (
{items.map((item: PurchaseOrderItem, index: number) => { const unitPrice = parseFloat(item.unit_price) || 0; const quantity = item.ordered_quantity || 0; const itemTotal = unitPrice * quantity; const productName = item.product_name || `${t('product')} ${index + 1}`; return (

{productName}

{item.product_code && (

{t('sku')}: {item.product_code}

)}

€{itemTotal.toFixed(2)}

{t('quantity')}

{quantity} {item.unit_of_measure}

{t('unit_price')}

€{unitPrice.toFixed(2)}

{item.quality_requirements && (

{t('quality_requirements')}

{item.quality_requirements}

)} {item.item_notes && (

{t('common:notes')}

{item.item_notes}

)}
); })}
{t('total')} €{totalAmount.toFixed(2)}
); }; // Priority and unit options for edit mode const priorityOptions = [ { value: 'urgent', label: t('priority_urgent') }, { value: 'high', label: t('priority_high') }, { value: 'normal', label: t('priority_normal') }, { value: 'low', label: t('priority_low') } ]; const unitOptions = [ { value: 'kg', label: t('unit_kg') }, { value: 'g', label: t('unit_g') }, { value: 'l', label: t('unit_l') }, { value: 'ml', label: t('unit_ml') }, { value: 'units', label: t('unit_units') }, { value: 'boxes', label: t('unit_boxes') }, { value: 'bags', label: t('unit_bags') } ]; // Build sections for EditViewModal const buildViewSections = (): EditViewModalSection[] => { if (!po) return []; const formatCurrency = (value: any) => { const num = Number(value); return isNaN(num) ? '0.00' : num.toFixed(2); }; const sections: EditViewModalSection[] = [ { title: t('general_information'), icon: FileText, fields: [ { label: t('po_number'), value: po.po_number, type: 'text' as const }, { label: t('status_label'), value: t(`status.${po.status}`), type: 'status' as const }, { label: t('priority'), value: t(`priority_${po.priority}` as any) || po.priority, type: 'text' as const }, { label: t('created'), value: new Date(po.created_at).toLocaleDateString(i18n.language, { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' }), type: 'text' as const } ] }, { title: t('supplier_info'), icon: Building2, fields: [ { label: t('supplier_name'), value: po.supplier?.name || t('common:unknown'), type: 'text' as const } ] }, { title: t('financial_summary'), icon: Euro, fields: [ { label: t('total_amount'), value: `€${formatCurrency(po.total_amount)}`, type: 'text' as const, highlight: true } ] }, { title: t('items'), icon: Package, fields: [ { label: '', value: , type: 'component' as const, span: 2 } ] }, { title: t('dates'), icon: Calendar, fields: [ { label: t('order_date'), value: new Date(po.order_date).toLocaleDateString(i18n.language, { year: 'numeric', month: 'short', day: 'numeric' }), type: 'text' as const }, ...(po.required_delivery_date ? [{ label: t('required_delivery_date'), value: new Date(po.required_delivery_date).toLocaleDateString(i18n.language, { year: 'numeric', month: 'short', day: 'numeric' }), type: 'text' as const }] : []), ...(po.estimated_delivery_date ? [{ label: t('expected_delivery'), value: new Date(po.estimated_delivery_date).toLocaleDateString(i18n.language, { year: 'numeric', month: 'short', day: 'numeric' }), type: 'text' as const }] : []) ] } ]; // Add notes section if present if (po.notes) { sections.push({ title: t('notes'), icon: FileText, fields: [ { label: t('order_notes'), value: po.notes, type: 'text' as const } ] }); } return sections; }; // Build sections for edit mode const buildEditSections = (): EditViewModalSection[] => { if (!po) return []; return [ { title: t('supplier_info'), icon: Building2, fields: [ { label: t('supplier'), value: po.supplier?.name || t('common:unknown'), type: 'text' as const, editable: false, helpText: t('supplier_cannot_modify') } ] }, { title: t('order_details'), icon: Calendar, fields: [ { label: t('priority'), value: po.priority, type: 'select' as const, editable: true, options: priorityOptions, helpText: t('adjust_priority') }, { label: t('required_delivery_date'), value: po.required_delivery_date || '', type: 'date' as const, editable: true, helpText: t('delivery_deadline') }, { label: t('notes'), value: po.notes || '', type: 'textarea' as const, editable: true, placeholder: t('special_instructions'), span: 2, helpText: t('additional_info') } ] }, { title: t('products'), icon: Package, fields: [ { label: t('products'), value: (po.items || []).map((item: PurchaseOrderItem) => ({ id: item.id, inventory_product_id: item.inventory_product_id, product_code: item.product_code || '', product_name: item.product_name || '', ordered_quantity: item.ordered_quantity, unit_of_measure: item.unit_of_measure, unit_price: parseFloat(item.unit_price), })), type: 'list' as const, editable: true, span: 2, helpText: t('modify_quantities') } ] } ]; }; // Save handler for edit mode const handleSave = async (formData: Record) => { try { const items = formData.items || []; if (items.length === 0) { throw new Error(t('at_least_one_product')); } // Validate quantities const invalidQuantities = items.some((item: any) => item.ordered_quantity <= 0); if (invalidQuantities) { throw new Error(t('quantities_greater_zero')); } // Validate required fields const invalidProducts = items.some((item: any) => !item.product_name); if (invalidProducts) { throw new Error(t('products_need_names')); } // Prepare the update data const updateData: any = { notes: formData.notes || undefined, priority: formData.priority || undefined, }; // Add delivery date if changed if (formData.required_delivery_date) { updateData.required_delivery_date = formData.required_delivery_date; } // Update purchase order await updatePurchaseOrderMutation.mutateAsync({ tenantId, poId, data: updateData }); // Refetch data and switch back to view mode await refetch(); setMode('view'); } catch (error) { console.error('Error modifying purchase order:', error); throw error; } }; // Build actions for modal footer - only Approve button for pending approval POs const buildActions = () => { if (!po) return undefined; // Only show Approve button in view mode for pending approval POs if (mode === 'view' && po.status === 'pending_approval') { return [ { label: t('actions.approve'), icon: CheckCircle, onClick: () => { onApprove?.(poId); onClose(); }, variant: 'primary' as const } ]; } return undefined; }; const sections = mode === 'view' ? buildViewSections() : buildEditSections(); return ( { setMode('view'); onClose(); }} mode={mode} onModeChange={setMode} title={po?.po_number || t('purchase_order')} subtitle={po ? new Date(po.created_at).toLocaleDateString(i18n.language, { year: 'numeric', month: 'long', day: 'numeric' }) : undefined} sections={sections} actions={buildActions()} isLoading={isLoading} size="lg" // Enable edit mode via standard Edit button (only for pending approval) onEdit={po?.status === 'pending_approval' ? () => setMode('edit') : undefined} onSave={mode === 'edit' ? handleSave : undefined} onCancel={mode === 'edit' ? () => setMode('view') : undefined} saveLabel={t('actions.save')} cancelLabel={t('actions.cancel')} /> ); };