Files
bakery-ia/frontend/src/components/domain/orders/OrderFormModal.tsx
2025-09-26 07:46:25 +02:00

684 lines
23 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 } from 'react';
import { X, Plus, Minus, User, ShoppingCart, FileText, Calculator, Package } from 'lucide-react';
import {
Button,
Input,
Select,
Card,
Badge
} from '../../ui';
import { EditViewModal } from '../../ui/EditViewModal';
import type { StatusModalSection } from '../../ui/EditViewModal';
import {
OrderCreate,
OrderItemCreate,
CustomerResponse,
OrderType,
PriorityLevel,
DeliveryMethod,
PaymentMethod,
PaymentTerms,
OrderSource,
SalesChannel,
CustomerCreate,
CustomerType,
CustomerSegment
} from '../../../api/types/orders';
import { useCustomers, useCreateCustomer } from '../../../api/hooks/orders';
import { useIngredients } from '../../../api/hooks/inventory';
import { ProductType, ProductCategory } from '../../../api/types/inventory';
import { useCurrentTenant } from '../../../stores/tenant.store';
import { useAuthUser } from '../../../stores/auth.store';
import { useTranslation } from 'react-i18next';
interface OrderFormModalProps {
isOpen: boolean;
onClose: () => void;
onSave: (orderData: OrderCreate) => Promise<void>;
}
export const OrderFormModal: React.FC<OrderFormModalProps> = ({
isOpen,
onClose,
onSave
}) => {
const currentTenant = useCurrentTenant();
const user = useAuthUser();
const tenantId = currentTenant?.id || user?.tenant_id || '';
const { t } = useTranslation(['orders', 'common']);
// Create enum options using direct i18n
const orderTypeOptions = Object.values(OrderType).map(value => ({
value,
label: t(`orders:order_types.${value}`)
}));
const priorityLevelOptions = Object.values(PriorityLevel).map(value => ({
value,
label: t(`orders:priority_levels.${value}`)
}));
const deliveryMethodOptions = Object.values(DeliveryMethod).map(value => ({
value,
label: t(`orders:delivery_methods.${value}`)
}));
const customerTypeOptions = Object.values(CustomerType).map(value => ({
value,
label: t(`orders:customer_types.${value}`)
}));
// Form state
const [selectedCustomer, setSelectedCustomer] = useState<CustomerResponse | null>(null);
const [orderItems, setOrderItems] = useState<OrderItemCreate[]>([]);
const [orderData, setOrderData] = useState<Partial<OrderCreate>>({
order_type: OrderType.STANDARD,
priority: PriorityLevel.NORMAL,
delivery_method: DeliveryMethod.PICKUP,
discount_percentage: 0,
delivery_fee: 0,
payment_terms: PaymentTerms.IMMEDIATE,
order_source: OrderSource.MANUAL,
sales_channel: SalesChannel.DIRECT
});
// Customer modals
const [showCustomerForm, setShowCustomerForm] = useState(false);
const [showCustomerSelector, setShowCustomerSelector] = useState(false);
const [newCustomerData, setNewCustomerData] = useState<Partial<CustomerCreate>>({
customer_type: CustomerType.INDIVIDUAL,
country: 'España',
is_active: true,
preferred_delivery_method: DeliveryMethod.PICKUP,
payment_terms: PaymentTerms.IMMEDIATE,
discount_percentage: 0,
customer_segment: CustomerSegment.REGULAR,
priority_level: PriorityLevel.NORMAL
});
// Simple product selection
const [selectedProductId, setSelectedProductId] = useState('');
const [selectedQuantity, setSelectedQuantity] = useState(1);
// API hooks
const { data: customers = [] } = useCustomers({
tenant_id: tenantId,
active_only: true,
limit: 100
});
// Fetch finished products from inventory
const { data: finishedProducts = [], isLoading: productsLoading } = useIngredients(
tenantId,
{
product_type: ProductType.FINISHED_PRODUCT,
is_active: true
}
);
const createCustomerMutation = useCreateCustomer();
// Calculate totals
const subtotal = orderItems.reduce((sum, item) => sum + (item.quantity * item.unit_price), 0);
const discountAmount = subtotal * (orderData.discount_percentage || 0) / 100;
const taxAmount = (subtotal - discountAmount) * 0.21; // 21% VAT
const total = subtotal - discountAmount + taxAmount + (orderData.delivery_fee || 0);
useEffect(() => {
if (!isOpen) {
// Reset form when modal closes
setSelectedCustomer(null);
setOrderItems([]);
setSelectedProductId('');
setSelectedQuantity(1);
setOrderData({
order_type: OrderType.STANDARD,
priority: PriorityLevel.NORMAL,
delivery_method: DeliveryMethod.PICKUP,
discount_percentage: 0,
delivery_fee: 0,
payment_terms: PaymentTerms.IMMEDIATE,
order_source: OrderSource.MANUAL,
sales_channel: SalesChannel.DIRECT
});
}
}, [isOpen]);
const handleAddSelectedProduct = () => {
if (!selectedProductId || selectedQuantity <= 0) return;
const product = finishedProducts.find(p => p.id === selectedProductId);
if (!product) return;
const existingItem = orderItems.find(item => item.product_id === selectedProductId);
if (existingItem) {
setOrderItems(items => items.map(item =>
item.product_id === selectedProductId
? { ...item, quantity: item.quantity + selectedQuantity }
: item
));
} else {
const newItem: OrderItemCreate = {
product_id: product.id,
product_name: product.name,
product_sku: product.sku || undefined,
product_category: product.category || undefined,
quantity: selectedQuantity,
unit_of_measure: product.unit_of_measure || 'unidad',
unit_price: product.average_cost || product.standard_cost || 0,
line_discount: 0
};
setOrderItems(items => [...items, newItem]);
}
// Reset selection
setSelectedProductId('');
setSelectedQuantity(1);
};
const handleUpdateItemQuantity = (productId: string, quantity: number) => {
if (quantity <= 0) {
setOrderItems(items => items.filter(item => item.product_id !== productId));
} else {
setOrderItems(items => items.map(item =>
item.product_id === productId
? { ...item, quantity }
: item
));
}
};
const handleCreateCustomer = async () => {
if (!newCustomerData.name || !tenantId) return;
try {
const customerData: CustomerCreate = {
...newCustomerData,
tenant_id: tenantId,
customer_code: `CUST-${Date.now()}` // Generate simple code
} as CustomerCreate;
const newCustomer = await createCustomerMutation.mutateAsync(customerData);
setSelectedCustomer(newCustomer);
setShowCustomerForm(false);
setNewCustomerData({
customer_type: CustomerType.INDIVIDUAL,
country: 'España',
is_active: true,
preferred_delivery_method: DeliveryMethod.PICKUP,
payment_terms: PaymentTerms.IMMEDIATE,
discount_percentage: 0,
customer_segment: CustomerSegment.REGULAR,
priority_level: PriorityLevel.NORMAL
});
} catch (error) {
console.error('Error creating customer:', error);
}
};
const handleSaveOrder = async () => {
if (!selectedCustomer || orderItems.length === 0 || !tenantId) return;
const finalOrderData: OrderCreate = {
tenant_id: tenantId,
customer_id: selectedCustomer.id,
order_type: orderData.order_type || OrderType.STANDARD,
priority: orderData.priority || PriorityLevel.NORMAL,
requested_delivery_date: orderData.requested_delivery_date || new Date().toISOString(),
delivery_method: orderData.delivery_method || DeliveryMethod.PICKUP,
delivery_fee: orderData.delivery_fee || 0,
discount_percentage: orderData.discount_percentage || 0,
payment_terms: orderData.payment_terms || PaymentTerms.IMMEDIATE,
order_source: orderData.order_source || OrderSource.MANUAL,
sales_channel: orderData.sales_channel || SalesChannel.DIRECT,
items: orderItems,
special_instructions: orderData.special_instructions
};
await onSave(finalOrderData);
onClose();
};
// Convert form data to StatusModal sections
const modalSections: StatusModalSection[] = [
{
title: 'Cliente',
icon: User,
fields: [
{
label: 'Cliente Seleccionado',
value: selectedCustomer ? selectedCustomer.id : '',
type: 'select',
editable: true,
placeholder: 'Seleccionar cliente...',
options: customers.map(customer => ({
value: customer.id,
label: `${customer.name} (${customer.customer_code})`
})),
span: 1
},
{
label: 'Nuevo Cliente',
value: (
<Button
variant="outline"
size="sm"
onClick={() => setShowCustomerForm(true)}
className="w-full"
>
<Plus className="w-4 h-4 mr-2" />
Agregar Cliente
</Button>
),
span: 1
}
]
},
{
title: 'Detalles del Pedido',
icon: FileText,
fields: [
{
label: 'Tipo de Pedido',
value: orderData.order_type || OrderType.STANDARD,
type: 'select',
editable: true,
options: orderTypeOptions
},
{
label: 'Prioridad',
value: orderData.priority || PriorityLevel.NORMAL,
type: 'select',
editable: true,
options: priorityLevelOptions
},
{
label: 'Método de Entrega',
value: orderData.delivery_method || DeliveryMethod.PICKUP,
type: 'select',
editable: true,
options: deliveryMethodOptions
},
{
label: 'Fecha de Entrega',
value: orderData.requested_delivery_date?.split('T')[0] || '',
type: 'date',
editable: true
}
]
},
{
title: 'Productos',
icon: Package,
fields: [
{
label: 'Agregar Producto',
value: (
<div className="flex gap-3 items-end">
<div className="flex-1">
<select
value={selectedProductId}
onChange={(e) => setSelectedProductId(e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-transparent bg-[var(--bg-primary)]"
>
<option value="">Seleccionar producto...</option>
{finishedProducts.map(product => (
<option key={product.id} value={product.id}>
{product.name} - {(product.average_cost || product.standard_cost || 0).toFixed(2)}
</option>
))}
</select>
</div>
<div className="w-24">
<Input
type="number"
min="1"
value={selectedQuantity}
onChange={(e) => setSelectedQuantity(Number(e.target.value))}
placeholder="Cant."
/>
</div>
<Button
onClick={handleAddSelectedProduct}
disabled={!selectedProductId || selectedQuantity <= 0}
className="whitespace-nowrap"
>
<Plus className="w-4 h-4 mr-1" />
Agregar
</Button>
</div>
),
span: 2
},
...(orderItems.length > 0 ? [{
label: 'Productos en el Pedido',
value: (
<div className="space-y-2">
{orderItems.map((item, index) => (
<div key={`${item.product_id}-${index}`} className="flex items-center justify-between p-3 bg-[var(--bg-secondary)] rounded-lg">
<div className="flex-1">
<h4 className="font-medium text-[var(--text-primary)]">{item.product_name}</h4>
<p className="text-sm text-[var(--text-secondary)]">
{item.unit_price.toFixed(2)} × {item.quantity} = {(item.unit_price * item.quantity).toFixed(2)}
</p>
</div>
<div className="flex items-center gap-2">
<Button
size="sm"
variant="outline"
onClick={() => handleUpdateItemQuantity(item.product_id, item.quantity - 1)}
disabled={item.quantity <= 1}
>
<Minus className="w-4 h-4" />
</Button>
<span className="min-w-[2rem] text-center font-medium">{item.quantity}</span>
<Button
size="sm"
variant="outline"
onClick={() => handleUpdateItemQuantity(item.product_id, item.quantity + 1)}
>
<Plus className="w-4 h-4" />
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleUpdateItemQuantity(item.product_id, 0)}
className="text-red-600 hover:text-red-700 ml-2"
>
<X className="w-4 h-4" />
</Button>
</div>
</div>
))}
</div>
),
span: 2
}] : [])
]
},
{
title: 'Resumen Financiero',
icon: Calculator,
fields: [
{
label: 'Subtotal',
value: subtotal,
type: 'currency',
highlight: true
},
{
label: 'Descuento',
value: -discountAmount,
type: 'currency'
},
{
label: 'IVA (21%)',
value: taxAmount,
type: 'currency'
},
{
label: 'Gastos de Envío',
value: orderData.delivery_fee || 0,
type: 'currency',
editable: true
},
{
label: 'Total',
value: total,
type: 'currency',
highlight: true,
span: 2
}
]
}
];
const handleFieldChange = (sectionIndex: number, fieldIndex: number, value: string | number) => {
const section = modalSections[sectionIndex];
const field = section.fields[fieldIndex];
// Update order data based on field changes
if (section.title === 'Cliente') {
if (field.label === 'Cliente Seleccionado') {
const customer = customers.find(c => c.id === value);
setSelectedCustomer(customer || null);
}
} else if (section.title === 'Detalles del Pedido') {
if (field.label === 'Tipo de Pedido') {
setOrderData(prev => ({ ...prev, order_type: value as OrderType }));
} else if (field.label === 'Prioridad') {
setOrderData(prev => ({ ...prev, priority: value as PriorityLevel }));
} else if (field.label === 'Método de Entrega') {
setOrderData(prev => ({ ...prev, delivery_method: value as DeliveryMethod }));
} else if (field.label === 'Fecha de Entrega') {
setOrderData(prev => ({ ...prev, requested_delivery_date: value ? `${value}T12:00:00Z` : undefined }));
}
} else if (section.title === 'Resumen Financiero' && field.label === 'Gastos de Envío') {
setOrderData(prev => ({ ...prev, delivery_fee: Number(value) }));
}
};
return (
<>
<EditViewModal
isOpen={isOpen}
onClose={onClose}
mode="edit"
title="Nuevo Pedido"
subtitle="Complete los detalles del pedido"
sections={modalSections}
size="2xl"
showDefaultActions={false}
actions={[
{
label: 'Cancelar',
variant: 'outline',
onClick: onClose
},
{
label: 'Crear Pedido',
variant: 'primary',
onClick: handleSaveOrder,
disabled: !selectedCustomer || orderItems.length === 0
}
]}
onFieldChange={handleFieldChange}
/>
{/* Legacy content for sections that need custom UI */}
<div className="hidden">
</div>
{/* New Customer Modal - Using StatusModal for consistency */}
<EditViewModal
isOpen={showCustomerForm}
onClose={() => setShowCustomerForm(false)}
mode="edit"
title="Nuevo Cliente"
subtitle="Complete la información del cliente"
size="lg"
showDefaultActions={false}
sections={[
{
title: 'Información Personal',
icon: User,
fields: [
{
label: 'Nombre *',
value: newCustomerData.name || '',
type: 'text',
editable: true,
required: true,
placeholder: 'Nombre del cliente'
},
{
label: 'Teléfono',
value: newCustomerData.phone || '',
type: 'tel',
editable: true,
placeholder: 'Número de teléfono'
},
{
label: 'Email',
value: newCustomerData.email || '',
type: 'email',
editable: true,
placeholder: 'Correo electrónico',
span: 2
},
{
label: 'Tipo de Cliente',
value: newCustomerData.customer_type || CustomerType.INDIVIDUAL,
type: 'select',
editable: true,
options: customerTypeOptions
},
{
label: 'Método de Entrega Preferido',
value: newCustomerData.preferred_delivery_method || DeliveryMethod.PICKUP,
type: 'select',
editable: true,
options: deliveryMethodOptions
}
]
}
]}
actions={[
{
label: 'Cancelar',
variant: 'outline',
onClick: () => setShowCustomerForm(false)
},
{
label: 'Crear Cliente',
variant: 'primary',
onClick: handleCreateCustomer,
disabled: !newCustomerData.name
}
]}
onFieldChange={(sectionIndex, fieldIndex, value) => {
// Get the customer modal sections instead of the order modal sections
const customerModalSections = [
{
title: 'Información Personal',
icon: User,
fields: [
{ label: 'Nombre *' },
{ label: 'Teléfono' },
{ label: 'Email' },
{ label: 'Tipo de Cliente' },
{ label: 'Método de Entrega Preferido' }
]
}
];
const field = customerModalSections[sectionIndex]?.fields[fieldIndex];
if (!field) return;
if (field.label === 'Nombre *') {
setNewCustomerData(prev => ({ ...prev, name: String(value) }));
} else if (field.label === 'Teléfono') {
setNewCustomerData(prev => ({ ...prev, phone: String(value) }));
} else if (field.label === 'Email') {
setNewCustomerData(prev => ({ ...prev, email: String(value) }));
} else if (field.label === 'Tipo de Cliente') {
setNewCustomerData(prev => ({ ...prev, customer_type: value as CustomerType }));
} else if (field.label === 'Método de Entrega Preferido') {
setNewCustomerData(prev => ({ ...prev, preferred_delivery_method: value as DeliveryMethod }));
}
}}
/>
{/* Customer Selector Modal */}
<EditViewModal
isOpen={showCustomerSelector}
onClose={() => setShowCustomerSelector(false)}
mode="view"
title="Seleccionar Cliente"
subtitle="Elija un cliente existente o cree uno nuevo"
size="lg"
showDefaultActions={false}
sections={[
{
title: 'Clientes Disponibles',
icon: User,
fields: [
{
label: 'Lista de clientes',
value: (
<div className="space-y-3 max-h-96 overflow-y-auto">
{customers.length === 0 ? (
<div className="text-center py-8 text-[var(--text-secondary)]">
<p>No hay clientes disponibles</p>
<Button
size="sm"
onClick={() => {
setShowCustomerSelector(false);
setShowCustomerForm(true);
}}
className="mt-2"
>
Crear Primer Cliente
</Button>
</div>
) : (
customers.map(customer => (
<div key={customer.id} className="border border-[var(--border-primary)] rounded-lg p-4 hover:bg-[var(--bg-secondary)]">
<div className="flex items-start justify-between">
<div className="flex-1">
<h4 className="font-medium text-[var(--text-primary)]">{customer.name}</h4>
<p className="text-sm text-[var(--text-secondary)]">
Código: {customer.customer_code}
</p>
<div className="text-sm text-[var(--text-secondary)] space-y-1 mt-2">
{customer.email && <div>📧 {customer.email}</div>}
{customer.phone && <div>📞 {customer.phone}</div>}
</div>
</div>
<Button
size="sm"
onClick={() => {
setSelectedCustomer(customer);
setShowCustomerSelector(false);
}}
variant={selectedCustomer?.id === customer.id ? 'primary' : 'outline'}
>
{selectedCustomer?.id === customer.id ? 'Seleccionado' : 'Seleccionar'}
</Button>
</div>
</div>
))
)}
</div>
),
span: 2
}
]
}
]}
actions={[
{
label: 'Nuevo Cliente',
variant: 'outline',
onClick: () => {
setShowCustomerSelector(false);
setShowCustomerForm(true);
}
},
{
label: 'Cerrar',
variant: 'primary',
onClick: () => setShowCustomerSelector(false)
}
]}
/>
</>
);
};
export default OrderFormModal;