Files
bakery-ia/frontend/src/components/domain/sales/OrderForm.tsx

1374 lines
51 KiB
TypeScript
Raw Normal View History

2025-08-28 10:41:04 +02:00
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;