- Added bg-[var(--bg-primary)] and text-[var(--text-primary)] CSS variables - Fixes white background + white text issue in dark mode - Applied to all input, select, and textarea elements across 8 wizards Wizards fixed: - InventoryWizard - CustomerWizard - SupplierWizard - RecipeWizard - EquipmentWizard - QualityTemplateWizard - TeamMemberWizard - CustomerOrderWizard (SalesEntryWizard was already fixed in previous commit) This completes the dark mode UI improvements (High Priority item). All form inputs now properly support dark mode with correct contrast.
884 lines
35 KiB
TypeScript
884 lines
35 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
|
|
import {
|
|
Users,
|
|
Plus,
|
|
Package,
|
|
Truck,
|
|
CreditCard,
|
|
Search,
|
|
CheckCircle2,
|
|
Calendar,
|
|
MapPin,
|
|
Loader2,
|
|
} from 'lucide-react';
|
|
import { useTenant } from '../../../../stores/tenant.store';
|
|
import OrdersService from '../../../../api/services/orders';
|
|
import { inventoryService } from '../../../../api/services/inventory';
|
|
import { showToast } from '../../../../utils/toast';
|
|
import {
|
|
CustomerCreate,
|
|
CustomerType,
|
|
DeliveryMethod,
|
|
PaymentTerms,
|
|
CustomerSegment,
|
|
PriorityLevel,
|
|
OrderCreate,
|
|
OrderItemCreate,
|
|
OrderType,
|
|
OrderSource,
|
|
SalesChannel,
|
|
PaymentMethod,
|
|
} from '../../../../api/types/orders';
|
|
import { ProductType } from '../../../../api/types/inventory';
|
|
|
|
interface WizardDataProps extends WizardStepProps {
|
|
data: Record<string, any>;
|
|
onDataChange: (data: Record<string, any>) => void;
|
|
}
|
|
|
|
// Step 1: Customer Selection
|
|
const CustomerSelectionStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNext }) => {
|
|
const { currentTenant } = useTenant();
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [showNewCustomerForm, setShowNewCustomerForm] = useState(false);
|
|
const [selectedCustomer, setSelectedCustomer] = useState(data.customer || null);
|
|
const [customers, setCustomers] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [creatingCustomer, setCreatingCustomer] = useState(false);
|
|
|
|
const [newCustomer, setNewCustomer] = useState({
|
|
name: '',
|
|
type: CustomerType.BUSINESS,
|
|
phone: '',
|
|
email: '',
|
|
});
|
|
|
|
useEffect(() => {
|
|
fetchCustomers();
|
|
}, []);
|
|
|
|
const fetchCustomers = async () => {
|
|
if (!currentTenant?.id) return;
|
|
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const result = await OrdersService.getCustomers({
|
|
tenant_id: currentTenant.id,
|
|
active_only: true,
|
|
});
|
|
setCustomers(result);
|
|
} catch (err: any) {
|
|
console.error('Error loading customers:', err);
|
|
setError('Error al cargar clientes');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const filteredCustomers = customers.filter((customer) =>
|
|
customer.name.toLowerCase().includes(searchQuery.toLowerCase())
|
|
);
|
|
|
|
const handleSelectCustomer = (customer: any) => {
|
|
setSelectedCustomer(customer);
|
|
setShowNewCustomerForm(false);
|
|
};
|
|
|
|
const handleContinue = async () => {
|
|
if (!currentTenant?.id) {
|
|
setError('No se pudo obtener información del tenant');
|
|
return;
|
|
}
|
|
|
|
if (showNewCustomerForm) {
|
|
// Create new customer via API
|
|
setCreatingCustomer(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const customerData: CustomerCreate = {
|
|
tenant_id: currentTenant.id,
|
|
customer_code: `CUST-${Date.now()}`,
|
|
name: newCustomer.name,
|
|
customer_type: newCustomer.type,
|
|
phone: newCustomer.phone,
|
|
email: newCustomer.email,
|
|
country: 'ES',
|
|
is_active: true,
|
|
preferred_delivery_method: DeliveryMethod.DELIVERY,
|
|
payment_terms: PaymentTerms.IMMEDIATE,
|
|
discount_percentage: 0,
|
|
customer_segment: CustomerSegment.REGULAR,
|
|
priority_level: PriorityLevel.NORMAL,
|
|
};
|
|
|
|
const createdCustomer = await OrdersService.createCustomer(customerData);
|
|
showToast.success('Cliente creado exitosamente');
|
|
onDataChange({ ...data, customer: createdCustomer, isNewCustomer: true });
|
|
onNext();
|
|
} catch (err: any) {
|
|
console.error('Error creating customer:', err);
|
|
const errorMessage = err.response?.data?.detail || 'Error al crear el cliente';
|
|
setError(errorMessage);
|
|
showToast.error(errorMessage);
|
|
} finally {
|
|
setCreatingCustomer(false);
|
|
}
|
|
} else {
|
|
onDataChange({ ...data, customer: selectedCustomer, isNewCustomer: false });
|
|
onNext();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
|
|
<Users className="w-12 h-12 mx-auto mb-3 text-[var(--color-primary)]" />
|
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
|
¿Para quién es este pedido?
|
|
</h3>
|
|
<p className="text-sm text-[var(--text-secondary)]">
|
|
Busca un cliente existente o crea uno nuevo
|
|
</p>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<Loader2 className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
|
|
<span className="ml-3 text-[var(--text-secondary)]">Cargando clientes...</span>
|
|
</div>
|
|
) : !showNewCustomerForm ? (
|
|
<>
|
|
{/* Search Bar */}
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--text-tertiary)]" />
|
|
<input
|
|
type="text"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
placeholder="Buscar cliente por nombre..."
|
|
className="w-full pl-10 pr-4 py-3 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
|
|
{/* Customer List */}
|
|
<div className="space-y-2 max-h-64 overflow-y-auto">
|
|
{filteredCustomers.map((customer) => (
|
|
<button
|
|
key={customer.id}
|
|
onClick={() => handleSelectCustomer(customer)}
|
|
className={`w-full p-4 rounded-lg border-2 transition-all text-left ${
|
|
selectedCustomer?.id === customer.id
|
|
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5'
|
|
: 'border-[var(--border-secondary)] hover:border-[var(--color-primary)]/50'
|
|
}`}
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="font-semibold text-[var(--text-primary)]">{customer.name}</p>
|
|
<p className="text-sm text-[var(--text-secondary)]">
|
|
{customer.customer_type} • {customer.phone || 'Sin teléfono'}
|
|
</p>
|
|
</div>
|
|
{selectedCustomer?.id === customer.id && (
|
|
<CheckCircle2 className="w-5 h-5 text-[var(--color-primary)]" />
|
|
)}
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Create New Customer Button */}
|
|
<button
|
|
onClick={() => setShowNewCustomerForm(true)}
|
|
className="w-full p-4 border-2 border-dashed border-[var(--border-secondary)] rounded-lg hover:border-[var(--color-primary)] transition-colors text-[var(--text-secondary)] hover:text-[var(--color-primary)] flex items-center justify-center gap-2"
|
|
>
|
|
<Plus className="w-5 h-5" />
|
|
<span className="font-medium">Crear nuevo cliente</span>
|
|
</button>
|
|
</>
|
|
) : (
|
|
<>
|
|
{/* New Customer Form */}
|
|
<div className="p-4 bg-[var(--bg-secondary)]/30 rounded-lg border border-[var(--border-secondary)]">
|
|
<h4 className="font-semibold text-[var(--text-primary)] mb-4 flex items-center gap-2">
|
|
<Plus className="w-5 h-5" />
|
|
Nuevo Cliente
|
|
</h4>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="md:col-span-2">
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Nombre del Cliente *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={newCustomer.name}
|
|
onChange={(e) => setNewCustomer({ ...newCustomer, name: e.target.value })}
|
|
placeholder="Ej: Restaurante El Molino"
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Tipo de Cliente *
|
|
</label>
|
|
<select
|
|
value={newCustomer.type}
|
|
onChange={(e) => setNewCustomer({ ...newCustomer, type: e.target.value })}
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
>
|
|
<option value="retail">Minorista</option>
|
|
<option value="wholesale">Mayorista</option>
|
|
<option value="event">Eventos</option>
|
|
<option value="restaurant">Restaurante</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Teléfono *
|
|
</label>
|
|
<input
|
|
type="tel"
|
|
value={newCustomer.phone}
|
|
onChange={(e) => setNewCustomer({ ...newCustomer, phone: e.target.value })}
|
|
placeholder="+34 123 456 789"
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
|
|
<div className="md:col-span-2">
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Email (Opcional)
|
|
</label>
|
|
<input
|
|
type="email"
|
|
value={newCustomer.email}
|
|
onChange={(e) => setNewCustomer({ ...newCustomer, email: e.target.value })}
|
|
placeholder="contacto@restaurante.com"
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => setShowNewCustomerForm(false)}
|
|
className="mt-4 text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
|
>
|
|
← Volver a la lista de clientes
|
|
</button>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Continue Button */}
|
|
<div className="flex justify-end pt-4 border-t border-[var(--border-primary)]">
|
|
<button
|
|
onClick={handleContinue}
|
|
disabled={
|
|
creatingCustomer ||
|
|
loading ||
|
|
(!selectedCustomer && (!showNewCustomerForm || !newCustomer.name || !newCustomer.phone))
|
|
}
|
|
className="px-6 py-2.5 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary)]/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors inline-flex items-center gap-2"
|
|
>
|
|
{creatingCustomer && <Loader2 className="w-4 h-4 animate-spin" />}
|
|
{creatingCustomer ? 'Creando cliente...' : 'Continuar'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Step 2: Order Items
|
|
const OrderItemsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNext }) => {
|
|
const { currentTenant } = useTenant();
|
|
const [orderItems, setOrderItems] = useState(data.orderItems || []);
|
|
const [products, setProducts] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
fetchProducts();
|
|
}, []);
|
|
|
|
const fetchProducts = async () => {
|
|
if (!currentTenant?.id) return;
|
|
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const allIngredients = await inventoryService.getIngredients(currentTenant.id);
|
|
// Filter for finished products only
|
|
const finishedProducts = allIngredients.filter(
|
|
(ingredient) => ingredient.product_type === ProductType.FINISHED_PRODUCT
|
|
);
|
|
setProducts(finishedProducts);
|
|
} catch (err: any) {
|
|
console.error('Error loading products:', err);
|
|
setError('Error al cargar productos');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleAddItem = () => {
|
|
setOrderItems([
|
|
...orderItems,
|
|
{ id: Date.now(), productId: '', productName: '', quantity: 1, unitPrice: 0, customRequirements: '', subtotal: 0 },
|
|
]);
|
|
};
|
|
|
|
const handleUpdateItem = (index: number, field: string, value: any) => {
|
|
const updated = orderItems.map((item: any, i: number) => {
|
|
if (i === index) {
|
|
const newItem = { ...item, [field]: value };
|
|
|
|
// If product selected, update price and name
|
|
if (field === 'productId') {
|
|
const product = products.find((p) => p.id === value);
|
|
if (product) {
|
|
newItem.productName = product.name;
|
|
newItem.unitPrice = product.average_cost || product.last_purchase_price || 0;
|
|
newItem.unitOfMeasure = product.unit_of_measure;
|
|
}
|
|
}
|
|
|
|
// Auto-calculate subtotal
|
|
if (field === 'quantity' || field === 'unitPrice' || field === 'productId') {
|
|
newItem.subtotal = (newItem.quantity || 0) * (newItem.unitPrice || 0);
|
|
}
|
|
return newItem;
|
|
}
|
|
return item;
|
|
});
|
|
setOrderItems(updated);
|
|
};
|
|
|
|
const handleRemoveItem = (index: number) => {
|
|
setOrderItems(orderItems.filter((_: any, i: number) => i !== index));
|
|
};
|
|
|
|
const calculateTotal = () => {
|
|
return orderItems.reduce((sum: number, item: any) => sum + (item.subtotal || 0), 0);
|
|
};
|
|
|
|
const handleContinue = () => {
|
|
onDataChange({ ...data, orderItems, totalAmount: calculateTotal() });
|
|
onNext();
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
|
|
<Package className="w-12 h-12 mx-auto mb-3 text-[var(--color-primary)]" />
|
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
|
¿Qué productos incluye el pedido?
|
|
</h3>
|
|
<p className="text-sm text-[var(--text-secondary)]">
|
|
Cliente: <span className="font-semibold">{data.customer?.name}</span>
|
|
</p>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<Loader2 className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
|
|
<span className="ml-3 text-[var(--text-secondary)]">Cargando productos...</span>
|
|
</div>
|
|
) : (
|
|
<>
|
|
{/* Order Items */}
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)]">
|
|
Productos del Pedido
|
|
</label>
|
|
<button
|
|
onClick={handleAddItem}
|
|
className="px-3 py-1.5 text-sm bg-[var(--color-primary)] text-white rounded-md hover:bg-[var(--color-primary)]/90 transition-colors flex items-center gap-1"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
Agregar Producto
|
|
</button>
|
|
</div>
|
|
|
|
{orderItems.length === 0 ? (
|
|
<div className="text-center py-12 border-2 border-dashed border-[var(--border-secondary)] rounded-lg text-[var(--text-tertiary)]">
|
|
<Package className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
|
<p className="mb-2">No hay productos en el pedido</p>
|
|
<p className="text-sm">Haz clic en "Agregar Producto" para comenzar</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{orderItems.map((item: any, index: number) => (
|
|
<div
|
|
key={item.id}
|
|
className="p-4 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-secondary)]/30 space-y-3"
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm font-semibold text-[var(--text-primary)]">
|
|
Producto #{index + 1}
|
|
</span>
|
|
<button
|
|
onClick={() => handleRemoveItem(index)}
|
|
className="p-1 text-red-500 hover:text-red-700 transition-colors"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
<div className="md:col-span-2">
|
|
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
|
|
Producto *
|
|
</label>
|
|
<select
|
|
value={item.productId}
|
|
onChange={(e) => handleUpdateItem(index, 'productId', e.target.value)}
|
|
className="w-full px-3 py-2 text-sm border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
|
|
>
|
|
<option value="">Seleccionar producto...</option>
|
|
{products.map((product) => (
|
|
<option key={product.id} value={product.id}>
|
|
{product.name} - €{(product.average_cost || product.last_purchase_price || 0).toFixed(2)} / {product.unit_of_measure}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
|
|
Cantidad *
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={item.quantity}
|
|
onChange={(e) => handleUpdateItem(index, 'quantity', parseFloat(e.target.value) || 0)}
|
|
className="w-full px-3 py-2 text-sm border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
|
|
min="0"
|
|
step="1"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
|
|
Precio Unitario (€)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={item.unitPrice}
|
|
onChange={(e) => handleUpdateItem(index, 'unitPrice', parseFloat(e.target.value) || 0)}
|
|
className="w-full px-3 py-2 text-sm border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
|
|
min="0"
|
|
step="0.01"
|
|
/>
|
|
</div>
|
|
|
|
<div className="md:col-span-2">
|
|
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
|
|
Requisitos Especiales (Opcional)
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={item.customRequirements}
|
|
onChange={(e) => handleUpdateItem(index, 'customRequirements', e.target.value)}
|
|
placeholder="Ej: Sin nueces, extra chocolate..."
|
|
className="w-full px-3 py-2 text-sm border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="pt-2 border-t border-[var(--border-primary)] text-sm">
|
|
<span className="font-semibold text-[var(--text-primary)]">
|
|
Subtotal: €{item.subtotal.toFixed(2)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Total */}
|
|
{orderItems.length > 0 && (
|
|
<div className="p-4 bg-gradient-to-r from-[var(--color-primary)]/5 to-[var(--color-primary)]/10 rounded-lg border-2 border-[var(--color-primary)]/20">
|
|
<div className="flex justify-between items-center">
|
|
<span className="text-lg font-semibold text-[var(--text-primary)]">Total del Pedido:</span>
|
|
<span className="text-2xl font-bold text-[var(--color-primary)]">
|
|
€{calculateTotal().toFixed(2)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Continue Button */}
|
|
<div className="flex justify-end pt-4 border-t border-[var(--border-primary)]">
|
|
<button
|
|
onClick={handleContinue}
|
|
disabled={loading || orderItems.length === 0}
|
|
className="px-6 py-2.5 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary)]/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
>
|
|
Continuar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Step 3: Delivery & Payment
|
|
const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComplete }) => {
|
|
const { currentTenant } = useTenant();
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [deliveryData, setDeliveryData] = useState({
|
|
deliveryDate: data.deliveryDate || '',
|
|
deliveryTime: data.deliveryTime || '',
|
|
deliveryMethod: data.deliveryMethod || 'pickup',
|
|
deliveryAddress: data.deliveryAddress || '',
|
|
paymentMethod: data.paymentMethod || 'invoice',
|
|
specialInstructions: data.specialInstructions || '',
|
|
orderStatus: data.orderStatus || 'pending',
|
|
});
|
|
|
|
const handleConfirm = async () => {
|
|
if (!currentTenant?.id) {
|
|
setError('No se pudo obtener información del tenant');
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
// Map UI delivery method to API enum
|
|
const deliveryMethodMap: Record<string, DeliveryMethod> = {
|
|
pickup: DeliveryMethod.PICKUP,
|
|
delivery: DeliveryMethod.DELIVERY,
|
|
shipping: DeliveryMethod.DELIVERY,
|
|
};
|
|
|
|
// Map UI payment method to API enum
|
|
const paymentMethodMap: Record<string, PaymentMethod> = {
|
|
cash: PaymentMethod.CASH,
|
|
card: PaymentMethod.CARD,
|
|
transfer: PaymentMethod.BANK_TRANSFER,
|
|
invoice: PaymentMethod.ACCOUNT,
|
|
'invoice-30': PaymentMethod.ACCOUNT,
|
|
paid: PaymentMethod.CARD,
|
|
};
|
|
|
|
// Map UI payment method to payment terms
|
|
const paymentTermsMap: Record<string, PaymentTerms> = {
|
|
cash: PaymentTerms.IMMEDIATE,
|
|
card: PaymentTerms.IMMEDIATE,
|
|
transfer: PaymentTerms.IMMEDIATE,
|
|
invoice: PaymentTerms.NET_30,
|
|
'invoice-30': PaymentTerms.NET_30,
|
|
paid: PaymentTerms.IMMEDIATE,
|
|
};
|
|
|
|
// Prepare order items
|
|
const orderItems: OrderItemCreate[] = data.orderItems.map((item: any) => ({
|
|
product_id: item.productId,
|
|
product_name: item.productName,
|
|
quantity: item.quantity,
|
|
unit_of_measure: item.unitOfMeasure || 'unit',
|
|
unit_price: item.unitPrice,
|
|
line_discount: 0,
|
|
customization_details: item.customRequirements || undefined,
|
|
special_instructions: item.customRequirements || undefined,
|
|
}));
|
|
|
|
// Prepare delivery address
|
|
const deliveryAddress = (deliveryData.deliveryMethod === 'delivery' || deliveryData.deliveryMethod === 'shipping')
|
|
? { address: deliveryData.deliveryAddress }
|
|
: undefined;
|
|
|
|
// Create order data
|
|
const orderData: OrderCreate = {
|
|
tenant_id: currentTenant.id,
|
|
customer_id: data.customer.id,
|
|
order_type: OrderType.STANDARD,
|
|
priority: PriorityLevel.NORMAL,
|
|
requested_delivery_date: deliveryData.deliveryDate,
|
|
delivery_method: deliveryMethodMap[deliveryData.deliveryMethod],
|
|
delivery_address: deliveryAddress,
|
|
delivery_instructions: deliveryData.specialInstructions || undefined,
|
|
discount_percentage: 0,
|
|
delivery_fee: 0,
|
|
payment_method: paymentMethodMap[deliveryData.paymentMethod],
|
|
payment_terms: paymentTermsMap[deliveryData.paymentMethod],
|
|
special_instructions: deliveryData.specialInstructions || undefined,
|
|
order_source: OrderSource.MANUAL,
|
|
sales_channel: SalesChannel.DIRECT,
|
|
items: orderItems,
|
|
};
|
|
|
|
await OrdersService.createOrder(orderData);
|
|
|
|
showToast.success('Pedido creado exitosamente');
|
|
onDataChange({ ...data, ...deliveryData });
|
|
onComplete();
|
|
} catch (err: any) {
|
|
console.error('Error creating order:', err);
|
|
const errorMessage = err.response?.data?.detail || 'Error al crear el pedido';
|
|
setError(errorMessage);
|
|
showToast.error(errorMessage);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
|
|
<Truck className="w-12 h-12 mx-auto mb-3 text-[var(--color-primary)]" />
|
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
|
Entrega y Pago
|
|
</h3>
|
|
<p className="text-sm text-[var(--text-secondary)]">
|
|
Configura los detalles de entrega y forma de pago
|
|
</p>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-4">
|
|
{/* Delivery Date & Time */}
|
|
<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">
|
|
<Calendar className="w-4 h-4 inline mr-1.5" />
|
|
Fecha de Entrega *
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={deliveryData.deliveryDate}
|
|
onChange={(e) => setDeliveryData({ ...deliveryData, deliveryDate: e.target.value })}
|
|
min={new Date().toISOString().split('T')[0]}
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Hora de Entrega
|
|
</label>
|
|
<input
|
|
type="time"
|
|
value={deliveryData.deliveryTime}
|
|
onChange={(e) => setDeliveryData({ ...deliveryData, deliveryTime: e.target.value })}
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Delivery Method */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
<Truck className="w-4 h-4 inline mr-1.5" />
|
|
Método de Entrega *
|
|
</label>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
<button
|
|
onClick={() => setDeliveryData({ ...deliveryData, deliveryMethod: 'pickup' })}
|
|
className={`p-3 rounded-lg border-2 transition-all ${
|
|
deliveryData.deliveryMethod === 'pickup'
|
|
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5'
|
|
: 'border-[var(--border-secondary)]'
|
|
}`}
|
|
>
|
|
<p className="font-semibold text-sm">Recogida</p>
|
|
<p className="text-xs text-[var(--text-tertiary)]">Cliente recoge</p>
|
|
</button>
|
|
<button
|
|
onClick={() => setDeliveryData({ ...deliveryData, deliveryMethod: 'delivery' })}
|
|
className={`p-3 rounded-lg border-2 transition-all ${
|
|
deliveryData.deliveryMethod === 'delivery'
|
|
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5'
|
|
: 'border-[var(--border-secondary)]'
|
|
}`}
|
|
>
|
|
<p className="font-semibold text-sm">Entrega</p>
|
|
<p className="text-xs text-[var(--text-tertiary)]">Envío a domicilio</p>
|
|
</button>
|
|
<button
|
|
onClick={() => setDeliveryData({ ...deliveryData, deliveryMethod: 'shipping' })}
|
|
className={`p-3 rounded-lg border-2 transition-all ${
|
|
deliveryData.deliveryMethod === 'shipping'
|
|
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5'
|
|
: 'border-[var(--border-secondary)]'
|
|
}`}
|
|
>
|
|
<p className="font-semibold text-sm">Envío</p>
|
|
<p className="text-xs text-[var(--text-tertiary)]">Mensajería</p>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Delivery Address (conditional) */}
|
|
{(deliveryData.deliveryMethod === 'delivery' || deliveryData.deliveryMethod === 'shipping') && (
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
<MapPin className="w-4 h-4 inline mr-1.5" />
|
|
Dirección de Entrega *
|
|
</label>
|
|
<textarea
|
|
value={deliveryData.deliveryAddress}
|
|
onChange={(e) => setDeliveryData({ ...deliveryData, deliveryAddress: e.target.value })}
|
|
placeholder="Calle, número, piso, código postal, ciudad..."
|
|
rows={3}
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Payment Method */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
<CreditCard className="w-4 h-4 inline mr-1.5" />
|
|
Método de Pago *
|
|
</label>
|
|
<select
|
|
value={deliveryData.paymentMethod}
|
|
onChange={(e) => setDeliveryData({ ...deliveryData, paymentMethod: e.target.value })}
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
>
|
|
<option value="cash">Efectivo</option>
|
|
<option value="card">Tarjeta</option>
|
|
<option value="transfer">Transferencia</option>
|
|
<option value="invoice">Factura (Net 15)</option>
|
|
<option value="invoice-30">Factura (Net 30)</option>
|
|
<option value="paid">Ya Pagado</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* Order Status */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Estado del Pedido
|
|
</label>
|
|
<select
|
|
value={deliveryData.orderStatus}
|
|
onChange={(e) => setDeliveryData({ ...deliveryData, orderStatus: e.target.value })}
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
>
|
|
<option value="pending">Pendiente</option>
|
|
<option value="confirmed">Confirmado</option>
|
|
<option value="in-progress">En Producción</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* Special Instructions */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Instrucciones Especiales (Opcional)
|
|
</label>
|
|
<textarea
|
|
value={deliveryData.specialInstructions}
|
|
onChange={(e) => setDeliveryData({ ...deliveryData, specialInstructions: e.target.value })}
|
|
placeholder="Notas sobre el pedido, preferencias de entrega, etc..."
|
|
rows={3}
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
|
|
{/* Order Summary */}
|
|
<div className="p-4 bg-[var(--bg-secondary)]/50 rounded-lg border border-[var(--border-secondary)]">
|
|
<h4 className="font-semibold text-[var(--text-primary)] mb-3">Resumen del Pedido</h4>
|
|
<div className="space-y-2 text-sm">
|
|
<div className="flex justify-between">
|
|
<span className="text-[var(--text-secondary)]">Cliente:</span>
|
|
<span className="font-medium">{data.customer?.name}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-[var(--text-secondary)]">Productos:</span>
|
|
<span className="font-medium">{data.orderItems?.length || 0} items</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-[var(--text-secondary)]">Total:</span>
|
|
<span className="font-semibold text-lg text-[var(--color-primary)]">
|
|
€{data.totalAmount?.toFixed(2)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Confirm Button */}
|
|
<div className="flex justify-end gap-3 pt-4 border-t border-[var(--border-primary)]">
|
|
<button
|
|
onClick={handleConfirm}
|
|
disabled={
|
|
loading ||
|
|
!deliveryData.deliveryDate ||
|
|
(deliveryData.deliveryMethod !== 'pickup' && !deliveryData.deliveryAddress)
|
|
}
|
|
className="px-8 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-semibold inline-flex items-center gap-2"
|
|
>
|
|
{loading ? (
|
|
<>
|
|
<Loader2 className="w-5 h-5 animate-spin" />
|
|
Creando pedido...
|
|
</>
|
|
) : (
|
|
<>
|
|
<CheckCircle2 className="w-5 h-5" />
|
|
Confirmar Pedido
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const CustomerOrderWizardSteps = (
|
|
data: Record<string, any>,
|
|
setData: (data: Record<string, any>) => void
|
|
): WizardStep[] => [
|
|
{
|
|
id: 'customer-selection',
|
|
title: 'Seleccionar Cliente',
|
|
description: 'Buscar o crear cliente',
|
|
component: (props) => <CustomerSelectionStep {...props} data={data} onDataChange={setData} />,
|
|
},
|
|
{
|
|
id: 'order-items',
|
|
title: 'Productos del Pedido',
|
|
description: 'Agregar productos y cantidades',
|
|
component: (props) => <OrderItemsStep {...props} data={data} onDataChange={setData} />,
|
|
},
|
|
{
|
|
id: 'delivery-payment',
|
|
title: 'Entrega y Pago',
|
|
description: 'Configurar entrega y forma de pago',
|
|
component: (props) => <DeliveryPaymentStep {...props} data={data} onDataChange={setData} />,
|
|
},
|
|
];
|