Files
bakery-ia/frontend/src/components/domain/sales/OrderForm.tsx
2025-08-28 10:41:04 +02:00

1374 lines
51 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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, useCallback } from 'react';
import {
Card,
Button,
Input,
Select,
Modal,
Badge,
Tooltip
} from '../../ui';
import {
SalesChannel,
PaymentMethod
} from '../../../types/sales.types';
import { salesService } from '../../../services/api/sales.service';
// Order form interfaces
interface Product {
id: string;
name: string;
description?: string;
category: string;
price: number;
available_quantity: number;
image_url?: string;
allergens: string[];
nutritional_info?: NutritionalInfo;
preparation_time: number; // in minutes
is_available: boolean;
tags: string[];
}
interface NutritionalInfo {
calories_per_100g: number;
protein: number;
carbs: number;
fat: number;
fiber: number;
sugar: number;
}
interface Customer {
id: string;
name: string;
email?: string;
phone?: string;
address?: string;
city?: string;
postal_code?: string;
loyalty_points: number;
preferred_channel: SalesChannel;
notes?: string;
}
interface OrderItem {
product_id: string;
product_name: string;
quantity: number;
unit_price: number;
total_price: number;
special_instructions?: string;
customizations?: OrderCustomization[];
}
interface OrderCustomization {
name: string;
value: string;
price_adjustment: number;
}
interface DeliveryInfo {
type: DeliveryType;
address?: string;
city?: string;
postal_code?: string;
delivery_date?: string;
delivery_time_slot?: string;
delivery_notes?: string;
delivery_fee: number;
}
interface OrderFormData {
customer?: Customer;
items: OrderItem[];
sales_channel: SalesChannel;
payment_method?: PaymentMethod;
delivery_info?: DeliveryInfo;
special_instructions?: string;
discount_code?: string;
discount_amount: number;
tax_rate: number;
subtotal: number;
tax_amount: number;
delivery_fee: number;
total_amount: number;
scheduled_date?: string;
scheduled_time?: string;
loyalty_points_to_use: number;
notes?: string;
}
enum DeliveryType {
PICKUP = 'pickup',
HOME_DELIVERY = 'home_delivery',
SCHEDULED_PICKUP = 'scheduled_pickup'
}
enum PaymentMethodType {
CASH = 'cash',
CREDIT_CARD = 'credit_card',
DEBIT_CARD = 'debit_card',
DIGITAL_WALLET = 'digital_wallet',
BANK_TRANSFER = 'bank_transfer',
LOYALTY_POINTS = 'loyalty_points',
STORE_CREDIT = 'store_credit'
}
interface OrderFormProps {
orderId?: string; // For editing existing orders
initialCustomer?: Customer;
onOrderSave?: (orderData: OrderFormData) => Promise<boolean>;
onOrderCancel?: () => void;
allowCustomerCreation?: boolean;
showPricing?: boolean;
className?: string;
}
const ChannelLabels = {
[SalesChannel.STORE_FRONT]: 'Tienda Física',
[SalesChannel.ONLINE]: 'Tienda Online',
[SalesChannel.PHONE_ORDER]: 'Pedido Telefónico',
[SalesChannel.DELIVERY]: 'Servicio Delivery',
[SalesChannel.CATERING]: 'Catering',
[SalesChannel.WHOLESALE]: 'Venta Mayorista',
[SalesChannel.FARMERS_MARKET]: 'Mercado Local',
[SalesChannel.THIRD_PARTY]: 'Plataforma Terceros'
};
const PaymentMethodLabels = {
[PaymentMethodType.CASH]: 'Efectivo',
[PaymentMethodType.CREDIT_CARD]: 'Tarjeta de Crédito',
[PaymentMethodType.DEBIT_CARD]: 'Tarjeta de Débito',
[PaymentMethodType.DIGITAL_WALLET]: 'Wallet Digital',
[PaymentMethodType.BANK_TRANSFER]: 'Transferencia',
[PaymentMethodType.LOYALTY_POINTS]: 'Puntos de Fidelización',
[PaymentMethodType.STORE_CREDIT]: 'Crédito de Tienda'
};
const DeliveryTypeLabels = {
[DeliveryType.PICKUP]: 'Recogida en Tienda',
[DeliveryType.HOME_DELIVERY]: 'Entrega a Domicilio',
[DeliveryType.SCHEDULED_PICKUP]: 'Recogida Programada'
};
// Mock data for bakery products
const mockProducts: Product[] = [
{
id: 'pan-integral-001',
name: 'Pan Integral Artesano',
description: 'Pan elaborado con harina integral 100%, masa madre natural',
category: 'Panadería',
price: 2.50,
available_quantity: 25,
allergens: ['gluten'],
preparation_time: 180,
is_available: true,
tags: ['artesano', 'integral', 'masa-madre']
},
{
id: 'croissant-mantequilla-002',
name: 'Croissant de Mantequilla',
description: 'Croissant francés tradicional con mantequilla de Normandía',
category: 'Bollería',
price: 1.80,
available_quantity: 40,
allergens: ['gluten', 'lactosa', 'huevo'],
preparation_time: 240,
is_available: true,
tags: ['frances', 'mantequilla', 'tradicional']
},
{
id: 'tarta-santiago-003',
name: 'Tarta de Santiago',
description: 'Tarta tradicional gallega con almendra marcona',
category: 'Repostería',
price: 18.90,
available_quantity: 8,
allergens: ['almendra', 'huevo'],
preparation_time: 360,
is_available: true,
tags: ['tradicional', 'almendra', 'gallega']
},
{
id: 'magdalenas-limon-004',
name: 'Magdalenas de Limón',
description: 'Magdalenas esponjosas con ralladura de limón ecológico',
category: 'Bollería',
price: 0.90,
available_quantity: 60,
allergens: ['gluten', 'huevo', 'lactosa'],
preparation_time: 45,
is_available: true,
tags: ['limon', 'esponjosas', 'ecologico']
},
{
id: 'empanada-atun-005',
name: 'Empanada de Atún',
description: 'Empanada gallega rellena de atún, pimiento y huevo',
category: 'Salado',
price: 12.50,
available_quantity: 6,
allergens: ['gluten', 'pescado', 'huevo'],
preparation_time: 300,
is_available: true,
tags: ['gallega', 'atun', 'tradicional']
},
{
id: 'brownie-chocolate-006',
name: 'Brownie de Chocolate Negro',
description: 'Brownie intenso con chocolate negro 70% y nueces',
category: 'Repostería',
price: 3.20,
available_quantity: 20,
allergens: ['gluten', 'huevo', 'frutos-secos', 'lactosa'],
preparation_time: 60,
is_available: true,
tags: ['chocolate', 'nueces', 'intenso']
}
];
const mockCustomers: Customer[] = [
{
id: 'customer-001',
name: 'María García López',
email: 'maria.garcia@email.com',
phone: '+34 612 345 678',
address: 'Calle Mayor, 123',
city: 'Madrid',
postal_code: '28001',
loyalty_points: 2450,
preferred_channel: SalesChannel.ONLINE,
notes: 'Cliente VIP. Prefiere productos sin gluten.'
},
{
id: 'customer-002',
name: 'Carlos Rodríguez Martín',
email: 'carlos.rodriguez@email.com',
phone: '+34 687 654 321',
address: 'Avenida de la Constitución, 45',
city: 'Madrid',
postal_code: '28002',
loyalty_points: 1230,
preferred_channel: SalesChannel.STORE_FRONT
},
{
id: 'customer-003',
name: 'Ana Fernández Ruiz',
email: 'ana.fernandez@email.com',
phone: '+34 698 765 432',
address: 'Plaza de España, 12',
city: 'Madrid',
postal_code: '28008',
loyalty_points: 890,
preferred_channel: SalesChannel.DELIVERY
}
];
export const OrderForm: React.FC<OrderFormProps> = ({
orderId,
initialCustomer,
onOrderSave,
onOrderCancel,
allowCustomerCreation = true,
showPricing = true,
className = ''
}) => {
// Form data state
const [orderData, setOrderData] = useState<OrderFormData>({
customer: initialCustomer,
items: [],
sales_channel: SalesChannel.STORE_FRONT,
discount_amount: 0,
tax_rate: 0.21, // 21% IVA in Spain
subtotal: 0,
tax_amount: 0,
delivery_fee: 0,
total_amount: 0,
loyalty_points_to_use: 0
});
// UI state
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
// Product selection
const [products, setProducts] = useState<Product[]>(mockProducts);
const [filteredProducts, setFilteredProducts] = useState<Product[]>(mockProducts);
const [selectedCategory, setSelectedCategory] = useState<string>('');
const [productSearchTerm, setProductSearchTerm] = useState('');
const [showProductModal, setShowProductModal] = useState(false);
// Customer management
const [customers, setCustomers] = useState<Customer[]>(mockCustomers);
const [customerSearchTerm, setCustomerSearchTerm] = useState('');
const [showCustomerModal, setShowCustomerModal] = useState(false);
const [newCustomerForm, setNewCustomerForm] = useState<Partial<Customer>>({});
const [showNewCustomerForm, setShowNewCustomerForm] = useState(false);
// Validation
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
// Available categories
const categories = useMemo(() => {
const cats = [...new Set(products.map(p => p.category))];
return cats;
}, [products]);
// Available time slots for delivery/pickup
const timeSlots = [
'09:00 - 10:00', '10:00 - 11:00', '11:00 - 12:00',
'12:00 - 13:00', '13:00 - 14:00', '15:00 - 16:00',
'16:00 - 17:00', '17:00 - 18:00', '18:00 - 19:00'
];
// Filter products
useEffect(() => {
let filtered = products.filter(product => product.is_available);
if (selectedCategory) {
filtered = filtered.filter(product => product.category === selectedCategory);
}
if (productSearchTerm) {
const searchLower = productSearchTerm.toLowerCase();
filtered = filtered.filter(product =>
product.name.toLowerCase().includes(searchLower) ||
product.description?.toLowerCase().includes(searchLower) ||
product.tags.some(tag => tag.toLowerCase().includes(searchLower))
);
}
setFilteredProducts(filtered);
}, [products, selectedCategory, productSearchTerm]);
// Calculate totals
useEffect(() => {
const subtotal = orderData.items.reduce((sum, item) => sum + item.total_price, 0);
const discountAmount = orderData.discount_code ? calculateDiscount(subtotal, orderData.discount_code) : 0;
const subtotalAfterDiscount = subtotal - discountAmount;
const taxAmount = subtotalAfterDiscount * orderData.tax_rate;
const deliveryFee = calculateDeliveryFee();
const loyaltyPointsDiscount = orderData.loyalty_points_to_use * 0.01; // 1 point = 1 cent
const totalAmount = Math.max(0, subtotalAfterDiscount + taxAmount + deliveryFee - loyaltyPointsDiscount);
setOrderData(prev => ({
...prev,
subtotal,
discount_amount: discountAmount,
tax_amount: taxAmount,
delivery_fee: deliveryFee,
total_amount: totalAmount
}));
}, [orderData.items, orderData.discount_code, orderData.loyalty_points_to_use, orderData.delivery_info, orderData.tax_rate]);
// Load existing order for editing
useEffect(() => {
if (orderId) {
loadExistingOrder(orderId);
}
}, [orderId]);
// Functions
const loadExistingOrder = async (id: string) => {
setLoading(true);
try {
// In real app, load from API
// const response = await salesService.getOrder(id);
console.log('Loading order:', id);
} catch (err) {
setError('Error al cargar el pedido');
} finally {
setLoading(false);
}
};
const calculateDiscount = (subtotal: number, discountCode: string): number => {
// Mock discount calculation
const discountCodes = {
'BIENVENIDO10': 0.10, // 10%
'FIDELIDAD15': 0.15, // 15%
'CUMPLE20': 0.20 // 20%
};
const discountRate = discountCodes[discountCode as keyof typeof discountCodes] || 0;
return subtotal * discountRate;
};
const calculateDeliveryFee = (): number => {
if (!orderData.delivery_info || orderData.delivery_info.type === DeliveryType.PICKUP) {
return 0;
}
if (orderData.delivery_info.type === DeliveryType.SCHEDULED_PICKUP) {
return 0;
}
// Base delivery fee
let fee = 3.50;
// Free delivery for orders over €25
if (orderData.subtotal >= 25) {
fee = 0;
}
return fee;
};
const addProductToOrder = (product: Product, quantity: number = 1) => {
const existingItemIndex = orderData.items.findIndex(item => item.product_id === product.id);
if (existingItemIndex >= 0) {
// Update existing item
const updatedItems = [...orderData.items];
updatedItems[existingItemIndex].quantity += quantity;
updatedItems[existingItemIndex].total_price =
updatedItems[existingItemIndex].quantity * updatedItems[existingItemIndex].unit_price;
setOrderData(prev => ({ ...prev, items: updatedItems }));
} else {
// Add new item
const newItem: OrderItem = {
product_id: product.id,
product_name: product.name,
quantity,
unit_price: product.price,
total_price: product.price * quantity
};
setOrderData(prev => ({
...prev,
items: [...prev.items, newItem]
}));
}
setShowProductModal(false);
};
const updateOrderItem = (productId: string, updates: Partial<OrderItem>) => {
const updatedItems = orderData.items.map(item => {
if (item.product_id === productId) {
const updatedItem = { ...item, ...updates };
if (updates.quantity !== undefined) {
updatedItem.total_price = updatedItem.quantity * updatedItem.unit_price;
}
return updatedItem;
}
return item;
});
setOrderData(prev => ({ ...prev, items: updatedItems }));
};
const removeOrderItem = (productId: string) => {
const updatedItems = orderData.items.filter(item => item.product_id !== productId);
setOrderData(prev => ({ ...prev, items: updatedItems }));
};
const selectCustomer = (customer: Customer) => {
setOrderData(prev => ({ ...prev, customer }));
setShowCustomerModal(false);
setCustomerSearchTerm('');
};
const createNewCustomer = async () => {
if (!newCustomerForm.name || !newCustomerForm.phone) {
setError('Nombre y teléfono son obligatorios');
return;
}
const newCustomer: Customer = {
id: `customer-${Date.now()}`,
name: newCustomerForm.name,
email: newCustomerForm.email,
phone: newCustomerForm.phone,
address: newCustomerForm.address,
city: newCustomerForm.city || 'Madrid',
postal_code: newCustomerForm.postal_code,
loyalty_points: 0,
preferred_channel: orderData.sales_channel,
notes: newCustomerForm.notes
};
setCustomers(prev => [...prev, newCustomer]);
setOrderData(prev => ({ ...prev, customer: newCustomer }));
setNewCustomerForm({});
setShowNewCustomerForm(false);
setShowCustomerModal(false);
};
const validateForm = (): boolean => {
const errors: Record<string, string> = {};
if (orderData.items.length === 0) {
errors.items = 'Debe agregar al menos un producto al pedido';
}
if (!orderData.customer) {
errors.customer = 'Debe seleccionar un cliente';
}
if (orderData.delivery_info?.type === DeliveryType.HOME_DELIVERY) {
if (!orderData.delivery_info.address) {
errors.delivery_address = 'La dirección de entrega es obligatoria';
}
if (!orderData.delivery_info.delivery_date) {
errors.delivery_date = 'La fecha de entrega es obligatoria';
}
}
if (orderData.delivery_info?.type === DeliveryType.SCHEDULED_PICKUP) {
if (!orderData.scheduled_date) {
errors.scheduled_date = 'La fecha de recogida es obligatoria';
}
if (!orderData.scheduled_time) {
errors.scheduled_time = 'La hora de recogida es obligatoria';
}
}
setValidationErrors(errors);
return Object.keys(errors).length === 0;
};
const handleSaveOrder = async () => {
if (!validateForm()) {
return;
}
setLoading(true);
setError(null);
try {
const success = await onOrderSave?.(orderData);
if (success) {
setSuccess('Pedido guardado correctamente');
// Reset form or navigate away
}
} catch (err) {
setError('Error al guardar el pedido');
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className={`flex items-center justify-center p-8 ${className}`}>
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span className="ml-2 text-[var(--text-secondary)]">
{orderId ? 'Cargando pedido...' : 'Guardando pedido...'}
</span>
</div>
);
}
return (
<div className={`space-y-6 ${className}`}>
{/* Header */}
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-[var(--text-primary)]">
{orderId ? 'Editar Pedido' : 'Nuevo Pedido'}
</h2>
<p className="text-[var(--text-secondary)]">
{orderId ? `Pedido #${orderId.slice(-8)}` : 'Crear un nuevo pedido'}
</p>
</div>
<div className="flex items-center space-x-3">
<Button variant="outline" onClick={onOrderCancel}>
Cancelar
</Button>
<Button onClick={handleSaveOrder} disabled={orderData.items.length === 0}>
{orderId ? 'Actualizar Pedido' : 'Crear Pedido'}
</Button>
</div>
</div>
</Card>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Column - Order Details */}
<div className="lg:col-span-2 space-y-6">
{/* Customer Selection */}
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">Cliente</h3>
<Button
variant="outline"
size="sm"
onClick={() => setShowCustomerModal(true)}
>
{orderData.customer ? 'Cambiar Cliente' : 'Seleccionar Cliente'}
</Button>
</div>
{orderData.customer ? (
<div className="bg-[var(--color-info)]/5 border border-[var(--color-info)]/20 rounded-lg p-4">
<div className="flex items-start justify-between">
<div>
<h4 className="font-medium text-[var(--text-primary)]">{orderData.customer.name}</h4>
<div className="mt-1 text-sm text-[var(--text-secondary)] space-y-1">
{orderData.customer.email && <div>📧 {orderData.customer.email}</div>}
{orderData.customer.phone && <div>📞 {orderData.customer.phone}</div>}
{orderData.customer.address && (
<div>📍 {orderData.customer.address}, {orderData.customer.city}</div>
)}
</div>
<div className="mt-2 flex items-center space-x-4">
<Badge variant="soft" color="gold">
{orderData.customer.loyalty_points} puntos
</Badge>
<Badge variant="soft" color="blue">
{ChannelLabels[orderData.customer.preferred_channel]}
</Badge>
</div>
</div>
</div>
</div>
) : (
<div className="text-center py-6 text-[var(--text-tertiary)] border-2 border-dashed border-[var(--border-secondary)] rounded-lg">
<svg className="w-12 h-12 mx-auto mb-2 text-[var(--text-tertiary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<p>No hay cliente seleccionado</p>
<Button
size="sm"
onClick={() => setShowCustomerModal(true)}
className="mt-2"
>
Seleccionar Cliente
</Button>
</div>
)}
{validationErrors.customer && (
<p className="mt-2 text-sm text-[var(--color-error)]">{validationErrors.customer}</p>
)}
</Card>
{/* Product Selection */}
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">Productos</h3>
<Button
variant="outline"
size="sm"
onClick={() => setShowProductModal(true)}
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Agregar Producto
</Button>
</div>
<div className="space-y-4">
{orderData.items.length === 0 ? (
<div className="text-center py-8 text-[var(--text-tertiary)] border-2 border-dashed border-[var(--border-secondary)] rounded-lg">
<svg className="w-12 h-12 mx-auto mb-2 text-[var(--text-tertiary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" />
</svg>
<p>No hay productos en el pedido</p>
<Button
size="sm"
onClick={() => setShowProductModal(true)}
className="mt-2"
>
Agregar Primer Producto
</Button>
</div>
) : (
orderData.items.map((item) => (
<div key={item.product_id} className="flex items-center justify-between p-4 border border-[var(--border-primary)] 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.total_price.toFixed(2)}
</p>
{item.special_instructions && (
<p className="text-sm text-[var(--color-primary)] mt-1">
📝 {item.special_instructions}
</p>
)}
</div>
<div className="flex items-center space-x-2 ml-4">
<div className="flex items-center border border-[var(--border-secondary)] rounded">
<button
onClick={() => updateOrderItem(item.product_id, { quantity: Math.max(1, item.quantity - 1) })}
className="p-1 hover:bg-[var(--bg-tertiary)]"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 12H4" />
</svg>
</button>
<span className="px-3 py-1 text-sm">{item.quantity}</span>
<button
onClick={() => updateOrderItem(item.product_id, { quantity: item.quantity + 1 })}
className="p-1 hover:bg-[var(--bg-tertiary)]"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
</button>
</div>
<Tooltip content="Eliminar producto">
<Button
size="sm"
variant="outline"
onClick={() => removeOrderItem(item.product_id)}
className="text-[var(--color-error)] hover:text-[var(--color-error)]"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</Button>
</Tooltip>
</div>
</div>
))
)}
</div>
{validationErrors.items && (
<p className="mt-2 text-sm text-[var(--color-error)]">{validationErrors.items}</p>
)}
</Card>
{/* Delivery & Pickup Options */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Entrega y Recogida</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Tipo de Servicio</label>
<Select
value={orderData.delivery_info?.type || DeliveryType.PICKUP}
onChange={(value) => setOrderData(prev => ({
...prev,
delivery_info: {
...prev.delivery_info,
type: value as DeliveryType,
delivery_fee: 0
}
}))}
options={Object.values(DeliveryType).map(type => ({
value: type,
label: DeliveryTypeLabels[type]
}))}
/>
</div>
{orderData.delivery_info?.type === DeliveryType.HOME_DELIVERY && (
<div className="space-y-4 p-4 bg-[var(--color-info)]/5 rounded-lg">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
label="Dirección de entrega"
value={orderData.delivery_info.address || ''}
onChange={(e) => setOrderData(prev => ({
...prev,
delivery_info: {
...prev.delivery_info!,
address: e.target.value
}
}))}
error={validationErrors.delivery_address}
required
/>
<Input
label="Ciudad"
value={orderData.delivery_info.city || ''}
onChange={(e) => setOrderData(prev => ({
...prev,
delivery_info: {
...prev.delivery_info!,
city: e.target.value
}
}))}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
label="Fecha de entrega"
type="date"
value={orderData.delivery_info.delivery_date || ''}
onChange={(e) => setOrderData(prev => ({
...prev,
delivery_info: {
...prev.delivery_info!,
delivery_date: e.target.value
}
}))}
min={new Date().toISOString().split('T')[0]}
error={validationErrors.delivery_date}
required
/>
<Select
label="Franja horaria"
value={orderData.delivery_info.delivery_time_slot || ''}
onChange={(value) => setOrderData(prev => ({
...prev,
delivery_info: {
...prev.delivery_info!,
delivery_time_slot: value
}
}))}
options={[
{ value: '', label: 'Seleccionar franja' },
...timeSlots.map(slot => ({ value: slot, label: slot }))
]}
/>
</div>
<div>
<Input
label="Notas de entrega"
value={orderData.delivery_info.delivery_notes || ''}
onChange={(e) => setOrderData(prev => ({
...prev,
delivery_info: {
...prev.delivery_info!,
delivery_notes: e.target.value
}
}))}
placeholder="Instrucciones especiales para el repartidor..."
/>
</div>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
<p className="text-sm text-yellow-800">
💡 <strong>Envío gratuito</strong> en pedidos superiores a 25.
Tu pedido: {orderData.subtotal.toFixed(2)}
{orderData.subtotal < 25 && (
<span> - Faltan {(25 - orderData.subtotal).toFixed(2)} para envío gratuito</span>
)}
</p>
</div>
</div>
)}
{orderData.delivery_info?.type === DeliveryType.SCHEDULED_PICKUP && (
<div className="space-y-4 p-4 bg-green-50 rounded-lg">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
label="Fecha de recogida"
type="date"
value={orderData.scheduled_date || ''}
onChange={(e) => setOrderData(prev => ({
...prev,
scheduled_date: e.target.value
}))}
min={new Date().toISOString().split('T')[0]}
error={validationErrors.scheduled_date}
required
/>
<Select
label="Hora de recogida"
value={orderData.scheduled_time || ''}
onChange={(value) => setOrderData(prev => ({
...prev,
scheduled_time: value
}))}
options={[
{ value: '', label: 'Seleccionar hora' },
...timeSlots.map(slot => ({ value: slot, label: slot }))
]}
error={validationErrors.scheduled_time}
required
/>
</div>
</div>
)}
</div>
</Card>
{/* Payment & Channel */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Pago y Canal</h3>
<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">Canal de Venta</label>
<Select
value={orderData.sales_channel}
onChange={(value) => setOrderData(prev => ({
...prev,
sales_channel: value as SalesChannel
}))}
options={Object.values(SalesChannel).map(channel => ({
value: channel,
label: ChannelLabels[channel]
}))}
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Método de Pago</label>
<Select
value={orderData.payment_method || ''}
onChange={(value) => setOrderData(prev => ({
...prev,
payment_method: value as PaymentMethod
}))}
options={[
{ value: '', label: 'Seleccionar método' },
...Object.values(PaymentMethodType).map(method => ({
value: method,
label: PaymentMethodLabels[method]
}))
]}
/>
</div>
</div>
</Card>
{/* Special Instructions */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Instrucciones Especiales</h3>
<textarea
value={orderData.special_instructions || ''}
onChange={(e) => setOrderData(prev => ({
...prev,
special_instructions: e.target.value
}))}
rows={3}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
placeholder="Alergias, preferencias especiales, instrucciones de preparación..."
/>
</Card>
</div>
{/* Right Column - Order Summary */}
<div className="space-y-6">
<Card className="p-6 sticky top-4">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Resumen del Pedido</h3>
<div className="space-y-3">
<div className="flex justify-between text-sm">
<span className="text-[var(--text-secondary)]">Subtotal</span>
<span className="font-medium">{orderData.subtotal.toFixed(2)}</span>
</div>
{orderData.discount_amount > 0 && (
<div className="flex justify-between text-sm text-[var(--color-success)]">
<span>Descuento{orderData.discount_code && ` (${orderData.discount_code})`}</span>
<span>-{orderData.discount_amount.toFixed(2)}</span>
</div>
)}
{orderData.delivery_fee > 0 && (
<div className="flex justify-between text-sm">
<span className="text-[var(--text-secondary)]">Gastos de envío</span>
<span className="font-medium">{orderData.delivery_fee.toFixed(2)}</span>
</div>
)}
{orderData.loyalty_points_to_use > 0 && (
<div className="flex justify-between text-sm text-[var(--color-success)]">
<span>Puntos utilizados ({orderData.loyalty_points_to_use})</span>
<span>-{(orderData.loyalty_points_to_use * 0.01).toFixed(2)}</span>
</div>
)}
<div className="flex justify-between text-sm">
<span className="text-[var(--text-secondary)]">IVA ({(orderData.tax_rate * 100).toFixed(0)}%)</span>
<span className="font-medium">{orderData.tax_amount.toFixed(2)}</span>
</div>
<div className="border-t pt-3">
<div className="flex justify-between">
<span className="text-lg font-semibold text-[var(--text-primary)]">Total</span>
<span className="text-lg font-bold text-[var(--color-info)]">{orderData.total_amount.toFixed(2)}</span>
</div>
</div>
</div>
{/* Discount Code */}
<div className="mt-6 pt-6 border-t border-[var(--border-primary)]">
<div className="space-y-3">
<Input
label="Código de descuento"
value={orderData.discount_code || ''}
onChange={(e) => setOrderData(prev => ({
...prev,
discount_code: e.target.value.toUpperCase()
}))}
placeholder="BIENVENIDO10"
/>
{orderData.customer && orderData.customer.loyalty_points > 0 && (
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Usar puntos de fidelización ({orderData.customer.loyalty_points} disponibles)
</label>
<div className="flex items-center space-x-2">
<input
type="range"
min="0"
max={Math.min(orderData.customer.loyalty_points, orderData.subtotal * 100)}
value={orderData.loyalty_points_to_use}
onChange={(e) => setOrderData(prev => ({
...prev,
loyalty_points_to_use: parseInt(e.target.value)
}))}
className="flex-1"
/>
<span className="text-sm text-[var(--text-secondary)] w-16">
{orderData.loyalty_points_to_use} pts
</span>
</div>
<p className="text-xs text-[var(--text-tertiary)] mt-1">
Ahorro: {(orderData.loyalty_points_to_use * 0.01).toFixed(2)}
</p>
</div>
)}
</div>
</div>
{/* Action Buttons */}
<div className="mt-6 space-y-3">
<Button
onClick={handleSaveOrder}
disabled={orderData.items.length === 0 || !orderData.customer}
className="w-full"
>
{orderId ? 'Actualizar Pedido' : 'Crear Pedido'}
</Button>
<Button
variant="outline"
onClick={onOrderCancel}
className="w-full"
>
Cancelar
</Button>
</div>
</Card>
{/* Order Progress */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Estado del Pedido</h3>
<div className="space-y-3">
<div className="flex items-center">
<div className={`w-3 h-3 rounded-full mr-3 ${
orderData.customer ? 'bg-green-500' : 'bg-gray-300'
}`} />
<span className={`text-sm ${
orderData.customer ? 'text-[var(--text-primary)] font-medium' : 'text-[var(--text-tertiary)]'
}`}>
Cliente seleccionado
</span>
</div>
<div className="flex items-center">
<div className={`w-3 h-3 rounded-full mr-3 ${
orderData.items.length > 0 ? 'bg-green-500' : 'bg-gray-300'
}`} />
<span className={`text-sm ${
orderData.items.length > 0 ? 'text-[var(--text-primary)] font-medium' : 'text-[var(--text-tertiary)]'
}`}>
Productos agregados
</span>
</div>
<div className="flex items-center">
<div className={`w-3 h-3 rounded-full mr-3 ${
orderData.payment_method ? 'bg-green-500' : 'bg-gray-300'
}`} />
<span className={`text-sm ${
orderData.payment_method ? 'text-[var(--text-primary)] font-medium' : 'text-[var(--text-tertiary)]'
}`}>
Método de pago
</span>
</div>
</div>
</Card>
</div>
</div>
{/* Product Selection Modal */}
<Modal
isOpen={showProductModal}
onClose={() => setShowProductModal(false)}
title="Seleccionar Productos"
size="lg"
>
<div className="space-y-4">
{/* Product Search */}
<div className="flex space-x-4">
<div className="flex-1">
<Input
placeholder="Buscar productos..."
value={productSearchTerm}
onChange={(e) => setProductSearchTerm(e.target.value)}
/>
</div>
<div className="w-48">
<Select
value={selectedCategory}
onChange={setSelectedCategory}
options={[
{ value: '', label: 'Todas las categorías' },
...categories.map(cat => ({ value: cat, label: cat }))
]}
/>
</div>
</div>
{/* Products Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 max-h-96 overflow-y-auto">
{filteredProducts.map(product => (
<div key={product.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)]">{product.name}</h4>
<p className="text-sm text-[var(--text-secondary)] mt-1">{product.description}</p>
<div className="flex items-center mt-2">
<Badge variant="soft" color="gray" className="mr-2">
{product.category}
</Badge>
<span className="text-lg font-semibold text-[var(--color-info)]">
{product.price.toFixed(2)}
</span>
</div>
{product.allergens.length > 0 && (
<div className="mt-2">
<p className="text-xs text-[var(--color-primary)]">
Contiene: {product.allergens.join(', ')}
</p>
</div>
)}
</div>
<Button
size="sm"
onClick={() => addProductToOrder(product)}
disabled={product.available_quantity === 0}
>
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Agregar
</Button>
</div>
<div className="text-xs text-[var(--text-tertiary)] mt-2">
Stock: {product.available_quantity} | Preparación: {product.preparation_time}min
</div>
</div>
))}
</div>
{filteredProducts.length === 0 && (
<div className="text-center py-8 text-[var(--text-tertiary)]">
<p>No se encontraron productos con los criterios seleccionados</p>
</div>
)}
</div>
</Modal>
{/* Customer Selection Modal */}
<Modal
isOpen={showCustomerModal}
onClose={() => setShowCustomerModal(false)}
title="Seleccionar Cliente"
size="lg"
>
<div className="space-y-4">
{/* Customer Search */}
<div className="flex space-x-3">
<div className="flex-1">
<Input
placeholder="Buscar por nombre, email o teléfono..."
value={customerSearchTerm}
onChange={(e) => setCustomerSearchTerm(e.target.value)}
/>
</div>
{allowCustomerCreation && (
<Button onClick={() => setShowNewCustomerForm(true)}>
Nuevo Cliente
</Button>
)}
</div>
{/* Customers List */}
<div className="space-y-3 max-h-96 overflow-y-auto">
{customers
.filter(customer =>
!customerSearchTerm ||
customer.name.toLowerCase().includes(customerSearchTerm.toLowerCase()) ||
customer.email?.toLowerCase().includes(customerSearchTerm.toLowerCase()) ||
customer.phone?.includes(customerSearchTerm)
)
.map(customer => (
<div
key={customer.id}
className="border border-[var(--border-primary)] rounded-lg p-4 hover:bg-[var(--color-info)]/5 cursor-pointer"
onClick={() => selectCustomer(customer)}
>
<div className="flex items-start justify-between">
<div>
<h4 className="font-medium text-[var(--text-primary)]">{customer.name}</h4>
<div className="text-sm text-[var(--text-secondary)] space-y-1">
{customer.email && <div>📧 {customer.email}</div>}
{customer.phone && <div>📞 {customer.phone}</div>}
{customer.address && <div>📍 {customer.address}</div>}
</div>
<Badge variant="soft" color="gold" className="mt-2">
{customer.loyalty_points} puntos
</Badge>
</div>
</div>
</div>
))}
</div>
{customers.filter(customer =>
!customerSearchTerm ||
customer.name.toLowerCase().includes(customerSearchTerm.toLowerCase()) ||
customer.email?.toLowerCase().includes(customerSearchTerm.toLowerCase()) ||
customer.phone?.includes(customerSearchTerm)
).length === 0 && (
<div className="text-center py-8 text-[var(--text-tertiary)]">
<p>No se encontraron clientes</p>
{allowCustomerCreation && customerSearchTerm && (
<Button
onClick={() => {
setNewCustomerForm({ name: customerSearchTerm });
setShowNewCustomerForm(true);
}}
className="mt-3"
>
Crear cliente "{customerSearchTerm}"
</Button>
)}
</div>
)}
</div>
</Modal>
{/* New Customer Form Modal */}
<Modal
isOpen={showNewCustomerForm}
onClose={() => setShowNewCustomerForm(false)}
title="Nuevo Cliente"
>
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
label="Nombre *"
value={newCustomerForm.name || ''}
onChange={(e) => setNewCustomerForm(prev => ({ ...prev, name: e.target.value }))}
required
/>
<Input
label="Teléfono *"
value={newCustomerForm.phone || ''}
onChange={(e) => setNewCustomerForm(prev => ({ ...prev, phone: e.target.value }))}
required
/>
</div>
<Input
label="Email"
type="email"
value={newCustomerForm.email || ''}
onChange={(e) => setNewCustomerForm(prev => ({ ...prev, email: e.target.value }))}
/>
<Input
label="Dirección"
value={newCustomerForm.address || ''}
onChange={(e) => setNewCustomerForm(prev => ({ ...prev, address: e.target.value }))}
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
label="Ciudad"
value={newCustomerForm.city || ''}
onChange={(e) => setNewCustomerForm(prev => ({ ...prev, city: e.target.value }))}
/>
<Input
label="Código Postal"
value={newCustomerForm.postal_code || ''}
onChange={(e) => setNewCustomerForm(prev => ({ ...prev, postal_code: e.target.value }))}
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Notas</label>
<textarea
value={newCustomerForm.notes || ''}
onChange={(e) => setNewCustomerForm(prev => ({ ...prev, notes: e.target.value }))}
rows={3}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
placeholder="Alergias, preferencias, observaciones..."
/>
</div>
<div className="flex justify-end space-x-3">
<Button variant="outline" onClick={() => setShowNewCustomerForm(false)}>
Cancelar
</Button>
<Button onClick={createNewCustomer}>
Crear Cliente
</Button>
</div>
</div>
</Modal>
{/* Success/Error Messages */}
{error && (
<div className="fixed bottom-4 right-4 bg-red-50 border border-red-200 rounded-lg p-4 shadow-lg z-50">
<div className="flex">
<svg className="w-5 h-5 text-red-400 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
<div>
<p className="text-sm text-[var(--color-error)]">{error}</p>
<Button
size="sm"
variant="outline"
onClick={() => setError(null)}
className="mt-2"
>
Cerrar
</Button>
</div>
</div>
</div>
)}
{success && (
<div className="fixed bottom-4 right-4 bg-green-50 border border-green-200 rounded-lg p-4 shadow-lg z-50">
<div className="flex">
<svg className="w-5 h-5 text-green-400 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
<div>
<p className="text-sm text-[var(--color-success)]">{success}</p>
<Button
size="sm"
variant="outline"
onClick={() => setSuccess(null)}
className="mt-2"
>
Cerrar
</Button>
</div>
</div>
</div>
)}
</div>
);
};
export default OrderForm;