Files
bakery-ia/frontend/src/components/domain/unified-wizard/wizards/PurchaseOrderWizard.tsx
Urtzi Alfaro c07df124fb Improve UI
2025-12-30 14:40:20 +01:00

735 lines
33 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection';
import {
Building2,
Package,
Calendar,
CheckCircle2,
Plus,
Trash2,
Search,
Loader2,
AlertCircle,
TrendingUp,
} from 'lucide-react';
import { useTenant } from '../../../../stores/tenant.store';
import { useSuppliers } from '../../../../api/hooks/suppliers';
import { useIngredients } from '../../../../api/hooks/inventory';
import { suppliersService } from '../../../../api/services/suppliers';
import { useCreatePurchaseOrder } from '../../../../api/hooks/purchase-orders';
import { useTenantCurrency } from '../../../../hooks/useTenantCurrency';
// Step 1: Supplier Selection
const SupplierSelectionStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
const data = dataRef?.current || {};
const { t } = useTranslation(['wizards', 'procurement']);
const { currentTenant } = useTenant();
const [searchQuery, setSearchQuery] = useState('');
const [selectedSupplier, setSelectedSupplier] = useState(data.supplier || null);
// Fetch suppliers
const { data: suppliersData, isLoading, isError } = useSuppliers(
currentTenant?.id || '',
{ limit: 100 },
{ enabled: !!currentTenant?.id }
);
const suppliers = (suppliersData || []).filter((s: any) => s.status === 'active');
const filteredSuppliers = suppliers.filter((supplier: any) =>
supplier.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
supplier.supplier_code?.toLowerCase().includes(searchQuery.toLowerCase())
);
const handleSelectSupplier = (supplier: any) => {
setSelectedSupplier(supplier);
onDataChange?.({
...data,
supplier,
supplier_id: supplier.id,
});
};
return (
<div className="space-y-6">
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
<Building2 className="w-12 h-12 mx-auto mb-3 text-[var(--color-primary)]" />
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
{t('purchaseOrder.supplierSelection.title')}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
{t('purchaseOrder.supplierSelection.description')}
</p>
</div>
{isError && (
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
{t('purchaseOrder.supplierSelection.errorLoading')}
</div>
)}
{isLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
<span className="ml-3 text-[var(--text-secondary)]">{t('purchaseOrder.supplierSelection.loading')}</span>
</div>
) : (
<>
{/* Search Bar */}
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--text-tertiary)]" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder={t('purchaseOrder.supplierSelection.searchPlaceholder')}
className="w-full pl-10 pr-4 py-3 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
/>
</div>
{/* Supplier List */}
<div className="space-y-3 max-h-96 overflow-y-auto pr-2">
{filteredSuppliers.length === 0 ? (
<div className="text-center py-8 border-2 border-dashed border-[var(--border-secondary)] rounded-lg">
<Building2 className="w-12 h-12 mx-auto mb-3 text-[var(--text-tertiary)]" />
<p className="text-[var(--text-secondary)] mb-1">{t('purchaseOrder.supplierSelection.noSuppliers')}</p>
<p className="text-sm text-[var(--text-tertiary)]">{t('purchaseOrder.supplierSelection.tryDifferentSearch')}</p>
</div>
) : (
filteredSuppliers.map((supplier: any) => (
<button
key={supplier.id}
onClick={() => handleSelectSupplier(supplier)}
className={`w-full p-4 rounded-xl border-2 transition-all text-left group hover:shadow-md ${
selectedSupplier?.id === supplier.id
? 'border-[var(--color-primary)] bg-gradient-to-r from-[var(--color-primary)]/10 to-[var(--color-primary)]/5 shadow-sm'
: 'border-[var(--border-secondary)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]/30'
}`}
>
<div className="flex items-start gap-3">
<div
className={`w-12 h-12 rounded-full flex items-center justify-center flex-shrink-0 transition-colors ${
selectedSupplier?.id === supplier.id
? 'bg-[var(--color-primary)] text-white'
: 'bg-[var(--bg-tertiary)] text-[var(--text-tertiary)] group-hover:bg-[var(--color-primary)]/20 group-hover:text-[var(--color-primary)]'
}`}
>
<Building2 className="w-6 h-6" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h4
className={`font-semibold truncate transition-colors ${
selectedSupplier?.id === supplier.id
? 'text-[var(--color-primary)]'
: 'text-[var(--text-primary)]'
}`}
>
{supplier.name}
</h4>
{selectedSupplier?.id === supplier.id && (
<CheckCircle2 className="w-5 h-5 text-[var(--color-primary)] flex-shrink-0" />
)}
</div>
<div className="flex flex-wrap items-center gap-2 text-sm text-[var(--text-secondary)]">
<span className="px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300">
{supplier.supplier_code}
</span>
{supplier.email && <span>📧 {supplier.email}</span>}
{supplier.phone && <span>📱 {supplier.phone}</span>}
</div>
</div>
</div>
</button>
))
)}
</div>
</>
)}
</div>
);
};
// Step 2: Add Items
const AddItemsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
const data = dataRef?.current || {};
const { t } = useTranslation(['wizards', 'procurement']);
const { currentTenant } = useTenant();
const { currencySymbol } = useTenantCurrency();
const [supplierProductIds, setSupplierProductIds] = useState<string[]>([]);
const [isLoadingSupplierProducts, setIsLoadingSupplierProducts] = useState(false);
// Fetch ALL ingredients
const { data: allIngredientsData = [], isLoading: isLoadingIngredients } = useIngredients(
currentTenant?.id || '',
{},
{ enabled: !!currentTenant?.id }
);
// Fetch supplier products when supplier is available
useEffect(() => {
const fetchSupplierProducts = async () => {
if (!data.supplier_id || !currentTenant?.id) {
setSupplierProductIds([]);
return;
}
setIsLoadingSupplierProducts(true);
try {
const products = await suppliersService.getSupplierProducts(currentTenant.id, data.supplier_id);
const productIds = products.map((p: any) => p.inventory_product_id);
setSupplierProductIds(productIds);
} catch (error) {
console.error('Error fetching supplier products:', error);
setSupplierProductIds([]);
} finally {
setIsLoadingSupplierProducts(false);
}
};
fetchSupplierProducts();
}, [data.supplier_id, currentTenant?.id]);
// Filter ingredients based on supplier products
const ingredientsData = useMemo(() => {
if (!data.supplier_id || supplierProductIds.length === 0) {
return [];
}
return allIngredientsData.filter((ing: any) => supplierProductIds.includes(ing.id));
}, [allIngredientsData, supplierProductIds, data.supplier_id]);
const handleAddItem = () => {
onDataChange?.({
...data,
items: [
...(data.items || []),
{
id: Date.now(),
inventory_product_id: '',
product_name: '',
ordered_quantity: 1,
unit_price: 0,
unit_of_measure: 'kg',
subtotal: 0,
},
],
});
};
const handleUpdateItem = (index: number, field: string, value: any) => {
const updated = (data.items || []).map((item: any, i: number) => {
if (i === index) {
const newItem = { ...item, [field]: value };
if (field === 'inventory_product_id') {
const product = ingredientsData.find((p: any) => p.id === value);
if (product) {
newItem.product_name = product.name;
newItem.unit_price = product.last_purchase_price || product.average_cost || 0;
newItem.unit_of_measure = product.unit_of_measure;
}
}
if (field === 'ordered_quantity' || field === 'unit_price' || field === 'inventory_product_id') {
newItem.subtotal = (newItem.ordered_quantity || 0) * (newItem.unit_price || 0);
}
return newItem;
}
return item;
});
onDataChange?.({ ...data, items: updated });
};
const handleRemoveItem = (index: number) => {
onDataChange?.({ ...data, items: (data.items || []).filter((_: any, i: number) => i !== index) });
};
const calculateTotal = () => {
return (data.items || []).reduce((sum: number, item: any) => sum + (item.subtotal || 0), 0);
};
const unitOptions = [
{ value: 'kg', label: t('purchaseOrder.orderItems.units.kg') },
{ value: 'g', label: t('purchaseOrder.orderItems.units.g') },
{ value: 'l', label: t('purchaseOrder.orderItems.units.l') },
{ value: 'ml', label: t('purchaseOrder.orderItems.units.ml') },
{ value: 'units', label: t('purchaseOrder.orderItems.units.units') },
{ value: 'boxes', label: t('purchaseOrder.orderItems.units.boxes') },
{ value: 'bags', label: t('purchaseOrder.orderItems.units.bags') },
];
return (
<div className="space-y-6">
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
<Package className="w-12 h-12 mx-auto mb-3 text-[var(--color-primary)]" />
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">{t('purchaseOrder.orderItems.titleHeader')}</h3>
<p className="text-sm text-[var(--text-secondary)]">
{t('purchaseOrder.orderItems.supplier')}: <span className="font-semibold">{data.supplier?.name || 'N/A'}</span>
</p>
</div>
{isLoadingIngredients || isLoadingSupplierProducts ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
<span className="ml-3 text-[var(--text-secondary)]">{t('purchaseOrder.orderItems.loadingProducts')}</span>
</div>
) : (
<>
<div className="space-y-3">
<div className="flex items-center justify-between">
<label className="block text-sm font-medium text-[var(--text-secondary)]">
{t('purchaseOrder.orderItems.productsInOrder')}
</label>
<button
onClick={handleAddItem}
disabled={ingredientsData.length === 0}
className="px-3 py-1.5 text-sm bg-[var(--color-primary)] text-white rounded-md hover:bg-[var(--color-primary)]/90 transition-colors flex items-center gap-1 disabled:opacity-50 disabled:cursor-not-allowed"
>
<Plus className="w-4 h-4" />
{t('purchaseOrder.orderItems.addProduct')}
</button>
</div>
{ingredientsData.length === 0 && (
<div className="p-4 bg-amber-50 border border-amber-200 rounded-lg text-amber-700 text-sm flex items-center gap-2">
<AlertCircle className="w-5 h-5 flex-shrink-0" />
<span>
{t('purchaseOrder.orderItems.noIngredientsForSupplier')}
</span>
</div>
)}
{(data.items || []).length === 0 ? (
<div className="text-center py-12 border-2 border-dashed border-[var(--border-secondary)] rounded-lg text-[var(--text-tertiary)]">
<Package className="w-12 h-12 mx-auto mb-3 opacity-50" />
<p className="mb-2">{t('purchaseOrder.orderItems.noProducts')}</p>
<p className="text-sm">{t('purchaseOrder.orderItems.clickToAdd')}</p>
</div>
) : (
<div className="space-y-3">
{(data.items || []).map((item: any, index: number) => (
<div
key={item.id}
className="p-4 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-secondary)]/30 space-y-3"
>
<div className="flex items-center justify-between">
<span className="text-sm font-semibold text-[var(--text-primary)]">
{t('purchaseOrder.orderItems.productNumber', { number: index + 1 })}
</span>
<button
onClick={() => handleRemoveItem(index)}
className="p-1 text-red-500 hover:text-red-700 transition-colors"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div className="md:col-span-2">
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
{t('purchaseOrder.orderItems.ingredient')} *
</label>
<select
value={item.inventory_product_id}
onChange={(e) => handleUpdateItem(index, 'inventory_product_id', e.target.value)}
className="w-full px-3 py-2 text-sm border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
>
<option value="">{t('purchaseOrder.orderItems.selectIngredient')}</option>
{ingredientsData.map((product: any) => (
<option key={product.id} value={product.id}>
{product.name} - {currencySymbol}{(product.last_purchase_price || product.average_cost || 0).toFixed(2)} /{' '}
{product.unit_of_measure}
</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
{t('purchaseOrder.orderItems.quantity')} *
</label>
<input
type="number"
value={item.ordered_quantity}
onChange={(e) =>
handleUpdateItem(index, 'ordered_quantity', parseFloat(e.target.value) || 0)
}
className="w-full px-3 py-2 text-sm border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
min="0"
step="0.01"
/>
</div>
<div>
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">{t('purchaseOrder.orderItems.unit')} *</label>
<select
value={item.unit_of_measure}
onChange={(e) => handleUpdateItem(index, 'unit_of_measure', e.target.value)}
className="w-full px-3 py-2 text-sm border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
>
{unitOptions.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
<div className="md:col-span-2">
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
{t('purchaseOrder.orderItems.unitPrice')} *
</label>
<input
type="number"
value={item.unit_price}
onChange={(e) => handleUpdateItem(index, 'unit_price', parseFloat(e.target.value) || 0)}
className="w-full px-3 py-2 text-sm border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
min="0"
step="0.01"
/>
</div>
</div>
<div className="pt-2 border-t border-[var(--border-primary)] text-sm">
<span className="font-semibold text-[var(--text-primary)]">
{t('purchaseOrder.orderItems.subtotal')}: {currencySymbol}{item.subtotal.toFixed(2)}
</span>
</div>
</div>
))}
</div>
)}
{(data.items || []).length > 0 && (
<div className="p-4 bg-gradient-to-r from-[var(--color-primary)]/5 to-[var(--color-primary)]/10 rounded-lg border-2 border-[var(--color-primary)]/20">
<div className="flex justify-between items-center">
<span className="text-lg font-semibold text-[var(--text-primary)]">{t('purchaseOrder.orderItems.total')}:</span>
<span className="text-2xl font-bold text-[var(--color-primary)]">{currencySymbol}{calculateTotal().toFixed(2)}</span>
</div>
</div>
)}
</div>
</>
)}
</div>
);
};
// Step 3: Order Details
const OrderDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
const data = dataRef?.current || {};
const { t } = useTranslation(['wizards', 'procurement']);
const getValue = (field: string, defaultValue: any = '') => {
return data[field] ?? defaultValue;
};
const handleFieldChange = (updates: Record<string, any>) => {
onDataChange?.({ ...data, ...updates });
};
const priorityOptions = [
{ value: 'low', label: t('purchaseOrder.orderDetails.priorityOptions.low') },
{ value: 'normal', label: t('purchaseOrder.orderDetails.priorityOptions.normal') },
{ value: 'high', label: t('purchaseOrder.orderDetails.priorityOptions.high') },
{ value: 'critical', label: t('purchaseOrder.orderDetails.priorityOptions.critical') },
];
return (
<div className="space-y-6">
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
<Calendar className="w-12 h-12 mx-auto mb-3 text-[var(--color-primary)]" />
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">{t('purchaseOrder.orderDetails.title')}</h3>
<p className="text-sm text-[var(--text-secondary)]">{t('purchaseOrder.orderDetails.description')}</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
{t('purchaseOrder.orderDetails.requiredDeliveryDate')} *
</label>
<input
type="date"
value={getValue('required_delivery_date')}
onChange={(e) => handleFieldChange({ required_delivery_date: e.target.value })}
min={new Date().toISOString().split('T')[0]}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">{t('purchaseOrder.orderDetails.priority')} *</label>
<select
value={getValue('priority', 'normal')}
onChange={(e) => handleFieldChange({ priority: e.target.value })}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
>
{priorityOptions.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
{t('purchaseOrder.orderDetails.notes')}
</label>
<textarea
value={getValue('notes')}
onChange={(e) => handleFieldChange({ notes: e.target.value })}
placeholder={t('purchaseOrder.orderDetails.notesPlaceholder')}
rows={4}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
/>
</div>
</div>
<AdvancedOptionsSection
title={t('purchaseOrder.advancedOptions.title')}
description={t('purchaseOrder.advancedOptions.description')}
>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">{t('purchaseOrder.advancedOptions.tax')}</label>
<input
type="number"
value={getValue('tax_amount', 0)}
onChange={(e) => handleFieldChange({ tax_amount: parseFloat(e.target.value) || 0 })}
min="0"
step="0.01"
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">{t('purchaseOrder.advancedOptions.shippingCost')}</label>
<input
type="number"
value={getValue('shipping_cost', 0)}
onChange={(e) => handleFieldChange({ shipping_cost: parseFloat(e.target.value) || 0 })}
min="0"
step="0.01"
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">{t('purchaseOrder.advancedOptions.discount')}</label>
<input
type="number"
value={getValue('discount_amount', 0)}
onChange={(e) => handleFieldChange({ discount_amount: parseFloat(e.target.value) || 0 })}
min="0"
step="0.01"
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
/>
</div>
</div>
</AdvancedOptionsSection>
</div>
);
};
// Step 4: Review & Submit
const ReviewSubmitStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
const data = dataRef?.current || {};
const { t } = useTranslation(['wizards', 'procurement']);
const { currencySymbol } = useTenantCurrency();
const calculateSubtotal = () => {
return (data.items || []).reduce((sum: number, item: any) => sum + (item.subtotal || 0), 0);
};
const calculateTotal = () => {
const subtotal = calculateSubtotal();
const tax = data.tax_amount || 0;
const shipping = data.shipping_cost || 0;
const discount = data.discount_amount || 0;
return subtotal + tax + shipping - discount;
};
return (
<div className="space-y-6">
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
<CheckCircle2 className="w-12 h-12 mx-auto mb-3 text-[var(--color-primary)]" />
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">{t('purchaseOrder.review.title')}</h3>
<p className="text-sm text-[var(--text-secondary)]">{t('purchaseOrder.review.description')}</p>
</div>
{/* Supplier Info */}
<div className="p-4 bg-[var(--bg-secondary)]/50 rounded-lg border border-[var(--border-secondary)]">
<h4 className="font-semibold text-[var(--text-primary)] mb-3 flex items-center gap-2">
<Building2 className="w-5 h-5" />
{t('purchaseOrder.review.supplierInfo')}
</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">{t('purchaseOrder.review.supplier')}:</span>
<span className="font-medium">{data.supplier?.name || 'N/A'}</span>
</div>
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">{t('purchaseOrder.review.code')}:</span>
<span className="font-medium">{data.supplier?.supplier_code || 'N/A'}</span>
</div>
{data.supplier?.email && (
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">{t('purchaseOrder.review.email')}:</span>
<span className="font-medium">{data.supplier.email}</span>
</div>
)}
</div>
</div>
{/* Order Details */}
<div className="p-4 bg-[var(--bg-secondary)]/50 rounded-lg border border-[var(--border-secondary)]">
<h4 className="font-semibold text-[var(--text-primary)] mb-3 flex items-center gap-2">
<Calendar className="w-5 h-5" />
{t('purchaseOrder.review.orderDetails')}
</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">{t('purchaseOrder.review.deliveryDate')}:</span>
<span className="font-medium">
{data.required_delivery_date
? new Date(data.required_delivery_date).toLocaleDateString('es-ES')
: t('purchaseOrder.review.notSpecified')}
</span>
</div>
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">{t('purchaseOrder.review.priority')}:</span>
<span className="font-medium capitalize">{data.priority || 'normal'}</span>
</div>
{data.notes && (
<div className="pt-2 border-t border-[var(--border-primary)]">
<span className="text-[var(--text-secondary)] block mb-1">{t('purchaseOrder.review.notes')}:</span>
<span className="font-medium">{data.notes}</span>
</div>
)}
</div>
</div>
{/* Items */}
<div className="p-4 bg-[var(--bg-secondary)]/50 rounded-lg border border-[var(--border-secondary)]">
<h4 className="font-semibold text-[var(--text-primary)] mb-3 flex items-center gap-2">
<Package className="w-5 h-5" />
{t('purchaseOrder.review.products')} ({(data.items || []).length})
</h4>
<div className="space-y-2">
{(data.items || []).map((item: any, index: number) => (
<div
key={item.id}
className="flex justify-between items-start p-3 bg-[var(--bg-primary)] rounded-lg border border-[var(--border-secondary)]"
>
<div className="flex-1">
<p className="font-medium text-[var(--text-primary)]">{item.product_name || t('purchaseOrder.review.productNoName')}</p>
<p className="text-sm text-[var(--text-secondary)]">
{item.ordered_quantity} {item.unit_of_measure} × {currencySymbol}{item.unit_price.toFixed(2)}
</p>
</div>
<div className="text-right">
<p className="font-semibold text-[var(--text-primary)]">{currencySymbol}{item.subtotal.toFixed(2)}</p>
</div>
</div>
))}
</div>
</div>
{/* Financial Summary */}
<div className="p-4 bg-gradient-to-r from-[var(--color-primary)]/5 to-[var(--color-primary)]/10 rounded-lg border-2 border-[var(--color-primary)]/20">
<h4 className="font-semibold text-[var(--text-primary)] mb-3 flex items-center gap-2">
<TrendingUp className="w-5 h-5" />
{t('purchaseOrder.review.financialSummary')}
</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">{t('purchaseOrder.review.subtotal')}:</span>
<span className="font-medium">{currencySymbol}{calculateSubtotal().toFixed(2)}</span>
</div>
{(data.tax_amount || 0) > 0 && (
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">{t('purchaseOrder.review.taxes')}:</span>
<span className="font-medium">{currencySymbol}{(data.tax_amount || 0).toFixed(2)}</span>
</div>
)}
{(data.shipping_cost || 0) > 0 && (
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">{t('purchaseOrder.review.shipping')}:</span>
<span className="font-medium">{currencySymbol}{(data.shipping_cost || 0).toFixed(2)}</span>
</div>
)}
{(data.discount_amount || 0) > 0 && (
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">{t('purchaseOrder.review.discount')}:</span>
<span className="font-medium text-green-600">-{currencySymbol}{(data.discount_amount || 0).toFixed(2)}</span>
</div>
)}
<div className="pt-2 border-t-2 border-[var(--color-primary)]/30 flex justify-between">
<span className="text-lg font-semibold text-[var(--text-primary)]">{t('purchaseOrder.review.total')}:</span>
<span className="text-2xl font-bold text-[var(--color-primary)]">{currencySymbol}{calculateTotal().toFixed(2)}</span>
</div>
</div>
</div>
</div>
);
};
export const PurchaseOrderWizardSteps = (
dataRef: React.MutableRefObject<Record<string, any>>,
_setData: (data: Record<string, any>) => void
): WizardStep[] => {
return [
{
id: 'supplier-selection',
title: 'Seleccionar Proveedor', // Will be translated in UnifiedAddWizard
component: SupplierSelectionStep,
validate: () => {
const data = dataRef.current;
if (!data.supplier_id) {
throw new Error('Debes seleccionar un proveedor');
}
return true;
},
},
{
id: 'add-items',
title: 'Agregar Productos',
component: AddItemsStep,
validate: () => {
const data = dataRef.current;
if (!data.items || data.items.length === 0) {
throw new Error('Debes agregar al menos un producto');
}
const invalidItems = data.items.some(
(item: any) => !item.inventory_product_id || item.ordered_quantity < 0.01 || item.unit_price < 0.01
);
if (invalidItems) {
throw new Error('Todos los productos deben tener ingrediente, cantidad mayor a 0 y precio mayor a 0');
}
return true;
},
},
{
id: 'order-details',
title: 'Detalles de la Orden',
component: OrderDetailsStep,
validate: () => {
const data = dataRef.current;
if (!data.required_delivery_date) {
throw new Error('Debes especificar una fecha de entrega');
}
return true;
},
},
{
id: 'review-submit',
title: 'Revisar y Confirmar',
component: ReviewSubmitStep,
},
];
};