2025-09-19 11:44:38 +02:00
|
|
|
|
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';
|
2025-09-26 07:46:25 +02:00
|
|
|
|
import { EditViewModal } from '../../ui/EditViewModal';
|
|
|
|
|
|
import type { StatusModalSection } from '../../ui/EditViewModal';
|
2025-09-19 11:44:38 +02:00
|
|
|
|
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';
|
2025-09-26 07:46:25 +02:00
|
|
|
|
import { useTranslation } from 'react-i18next';
|
2025-09-19 11:44:38 +02:00
|
|
|
|
|
|
|
|
|
|
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 || '';
|
2025-09-26 07:46:25 +02:00
|
|
|
|
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}`)
|
|
|
|
|
|
}));
|
2025-09-19 11:44:38 +02:00
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-19 12:06:26 +02:00
|
|
|
|
// Simple product selection
|
|
|
|
|
|
const [selectedProductId, setSelectedProductId] = useState('');
|
|
|
|
|
|
const [selectedQuantity, setSelectedQuantity] = useState(1);
|
2025-09-19 11:44:38 +02:00
|
|
|
|
|
|
|
|
|
|
// 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([]);
|
2025-09-19 12:06:26 +02:00
|
|
|
|
setSelectedProductId('');
|
|
|
|
|
|
setSelectedQuantity(1);
|
2025-09-19 11:44:38 +02:00
|
|
|
|
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]);
|
|
|
|
|
|
|
2025-09-19 12:06:26 +02:00
|
|
|
|
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);
|
2025-09-19 11:44:38 +02:00
|
|
|
|
|
|
|
|
|
|
if (existingItem) {
|
|
|
|
|
|
setOrderItems(items => items.map(item =>
|
2025-09-19 12:06:26 +02:00
|
|
|
|
item.product_id === selectedProductId
|
|
|
|
|
|
? { ...item, quantity: item.quantity + selectedQuantity }
|
2025-09-19 11:44:38 +02:00
|
|
|
|
: item
|
|
|
|
|
|
));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const newItem: OrderItemCreate = {
|
|
|
|
|
|
product_id: product.id,
|
|
|
|
|
|
product_name: product.name,
|
|
|
|
|
|
product_sku: product.sku || undefined,
|
|
|
|
|
|
product_category: product.category || undefined,
|
2025-09-19 12:06:26 +02:00
|
|
|
|
quantity: selectedQuantity,
|
2025-09-19 11:44:38 +02:00
|
|
|
|
unit_of_measure: product.unit_of_measure || 'unidad',
|
|
|
|
|
|
unit_price: product.average_cost || product.standard_cost || 0,
|
|
|
|
|
|
line_discount: 0
|
|
|
|
|
|
};
|
|
|
|
|
|
setOrderItems(items => [...items, newItem]);
|
|
|
|
|
|
}
|
2025-09-19 12:06:26 +02:00
|
|
|
|
|
|
|
|
|
|
// Reset selection
|
|
|
|
|
|
setSelectedProductId('');
|
|
|
|
|
|
setSelectedQuantity(1);
|
2025-09-19 11:44:38 +02:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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',
|
2025-09-19 12:06:26 +02:00
|
|
|
|
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
|
2025-09-19 11:44:38 +02:00
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'Detalles del Pedido',
|
|
|
|
|
|
icon: FileText,
|
|
|
|
|
|
fields: [
|
|
|
|
|
|
{
|
|
|
|
|
|
label: 'Tipo de Pedido',
|
|
|
|
|
|
value: orderData.order_type || OrderType.STANDARD,
|
|
|
|
|
|
type: 'select',
|
|
|
|
|
|
editable: true,
|
2025-09-26 07:46:25 +02:00
|
|
|
|
options: orderTypeOptions
|
2025-09-19 11:44:38 +02:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: 'Prioridad',
|
|
|
|
|
|
value: orderData.priority || PriorityLevel.NORMAL,
|
|
|
|
|
|
type: 'select',
|
|
|
|
|
|
editable: true,
|
2025-09-26 07:46:25 +02:00
|
|
|
|
options: priorityLevelOptions
|
2025-09-19 11:44:38 +02:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: 'Método de Entrega',
|
|
|
|
|
|
value: orderData.delivery_method || DeliveryMethod.PICKUP,
|
|
|
|
|
|
type: 'select',
|
|
|
|
|
|
editable: true,
|
2025-09-26 07:46:25 +02:00
|
|
|
|
options: deliveryMethodOptions
|
2025-09-19 11:44:38 +02:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: 'Fecha de Entrega',
|
|
|
|
|
|
value: orderData.requested_delivery_date?.split('T')[0] || '',
|
|
|
|
|
|
type: 'date',
|
|
|
|
|
|
editable: true
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'Productos',
|
|
|
|
|
|
icon: Package,
|
|
|
|
|
|
fields: [
|
|
|
|
|
|
{
|
2025-09-19 12:06:26 +02:00
|
|
|
|
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
|
2025-09-19 11:44:38 +02:00
|
|
|
|
},
|
2025-09-19 12:06:26 +02:00
|
|
|
|
...(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>
|
|
|
|
|
|
),
|
2025-09-19 11:44:38 +02:00
|
|
|
|
span: 2
|
2025-09-19 12:06:26 +02:00
|
|
|
|
}] : [])
|
2025-09-19 11:44:38 +02:00
|
|
|
|
]
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
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
|
2025-09-19 12:06:26 +02:00
|
|
|
|
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') {
|
2025-09-19 11:44:38 +02:00
|
|
|
|
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 (
|
|
|
|
|
|
<>
|
2025-09-26 07:46:25 +02:00
|
|
|
|
<EditViewModal
|
2025-09-19 11:44:38 +02:00
|
|
|
|
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 */}
|
2025-09-26 07:46:25 +02:00
|
|
|
|
<EditViewModal
|
2025-09-19 11:44:38 +02:00
|
|
|
|
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,
|
2025-09-26 07:46:25 +02:00
|
|
|
|
options: customerTypeOptions
|
2025-09-19 11:44:38 +02:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: 'Método de Entrega Preferido',
|
|
|
|
|
|
value: newCustomerData.preferred_delivery_method || DeliveryMethod.PICKUP,
|
|
|
|
|
|
type: 'select',
|
|
|
|
|
|
editable: true,
|
2025-09-26 07:46:25 +02:00
|
|
|
|
options: deliveryMethodOptions
|
2025-09-19 11:44:38 +02:00
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
]}
|
|
|
|
|
|
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 */}
|
2025-09-26 07:46:25 +02:00
|
|
|
|
<EditViewModal
|
2025-09-19 11:44:38 +02:00
|
|
|
|
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;
|