1374 lines
51 KiB
TypeScript
1374 lines
51 KiB
TypeScript
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; |