Add order page with real API calls
This commit is contained in:
695
frontend/src/components/domain/orders/OrderFormModal.tsx
Normal file
695
frontend/src/components/domain/orders/OrderFormModal.tsx
Normal file
@@ -0,0 +1,695 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { X, Plus, Minus, User, ShoppingCart, FileText, Calculator, Package } from 'lucide-react';
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Select,
|
||||
Card,
|
||||
Badge
|
||||
} from '../../ui';
|
||||
import { StatusModal } from '../../ui/StatusModal';
|
||||
import type { StatusModalSection } from '../../ui/StatusModal';
|
||||
import {
|
||||
OrderCreate,
|
||||
OrderItemCreate,
|
||||
CustomerResponse,
|
||||
OrderType,
|
||||
PriorityLevel,
|
||||
DeliveryMethod,
|
||||
PaymentMethod,
|
||||
PaymentTerms,
|
||||
OrderSource,
|
||||
SalesChannel,
|
||||
CustomerCreate,
|
||||
CustomerType,
|
||||
CustomerSegment
|
||||
} from '../../../api/types/orders';
|
||||
import { useCustomers, useCreateCustomer } from '../../../api/hooks/orders';
|
||||
import { useIngredients } from '../../../api/hooks/inventory';
|
||||
import { ProductType, ProductCategory } from '../../../api/types/inventory';
|
||||
import { useCurrentTenant } from '../../../stores/tenant.store';
|
||||
import { useAuthUser } from '../../../stores/auth.store';
|
||||
import { useOrderEnums } from '../../../utils/enumHelpers';
|
||||
|
||||
interface OrderFormModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (orderData: OrderCreate) => Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
export const OrderFormModal: React.FC<OrderFormModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave
|
||||
}) => {
|
||||
const currentTenant = useCurrentTenant();
|
||||
const user = useAuthUser();
|
||||
const tenantId = currentTenant?.id || user?.tenant_id || '';
|
||||
const orderEnums = useOrderEnums();
|
||||
|
||||
// Form state
|
||||
const [selectedCustomer, setSelectedCustomer] = useState<CustomerResponse | null>(null);
|
||||
const [orderItems, setOrderItems] = useState<OrderItemCreate[]>([]);
|
||||
const [orderData, setOrderData] = useState<Partial<OrderCreate>>({
|
||||
order_type: OrderType.STANDARD,
|
||||
priority: PriorityLevel.NORMAL,
|
||||
delivery_method: DeliveryMethod.PICKUP,
|
||||
discount_percentage: 0,
|
||||
delivery_fee: 0,
|
||||
payment_terms: PaymentTerms.IMMEDIATE,
|
||||
order_source: OrderSource.MANUAL,
|
||||
sales_channel: SalesChannel.DIRECT
|
||||
});
|
||||
|
||||
// Customer modals
|
||||
const [showCustomerForm, setShowCustomerForm] = useState(false);
|
||||
const [showCustomerSelector, setShowCustomerSelector] = useState(false);
|
||||
const [newCustomerData, setNewCustomerData] = useState<Partial<CustomerCreate>>({
|
||||
customer_type: CustomerType.INDIVIDUAL,
|
||||
country: 'España',
|
||||
is_active: true,
|
||||
preferred_delivery_method: DeliveryMethod.PICKUP,
|
||||
payment_terms: PaymentTerms.IMMEDIATE,
|
||||
discount_percentage: 0,
|
||||
customer_segment: CustomerSegment.REGULAR,
|
||||
priority_level: PriorityLevel.NORMAL
|
||||
});
|
||||
|
||||
// Product selection
|
||||
const [showProductModal, setShowProductModal] = useState(false);
|
||||
const [productSearch, setProductSearch] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>('');
|
||||
|
||||
// API hooks
|
||||
const { data: customers = [] } = useCustomers({
|
||||
tenant_id: tenantId,
|
||||
active_only: true,
|
||||
limit: 100
|
||||
});
|
||||
|
||||
// Fetch finished products from inventory
|
||||
const { data: finishedProducts = [], isLoading: productsLoading } = useIngredients(
|
||||
tenantId,
|
||||
{
|
||||
product_type: ProductType.FINISHED_PRODUCT,
|
||||
is_active: true
|
||||
}
|
||||
);
|
||||
|
||||
const createCustomerMutation = useCreateCustomer();
|
||||
|
||||
// Calculate totals
|
||||
const subtotal = orderItems.reduce((sum, item) => sum + (item.quantity * item.unit_price), 0);
|
||||
const discountAmount = subtotal * (orderData.discount_percentage || 0) / 100;
|
||||
const taxAmount = (subtotal - discountAmount) * 0.21; // 21% VAT
|
||||
const total = subtotal - discountAmount + taxAmount + (orderData.delivery_fee || 0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
// Reset form when modal closes
|
||||
setSelectedCustomer(null);
|
||||
setOrderItems([]);
|
||||
setProductSearch('');
|
||||
setSelectedCategory('');
|
||||
setOrderData({
|
||||
order_type: OrderType.STANDARD,
|
||||
priority: PriorityLevel.NORMAL,
|
||||
delivery_method: DeliveryMethod.PICKUP,
|
||||
discount_percentage: 0,
|
||||
delivery_fee: 0,
|
||||
payment_terms: PaymentTerms.IMMEDIATE,
|
||||
order_source: OrderSource.MANUAL,
|
||||
sales_channel: SalesChannel.DIRECT
|
||||
});
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const handleAddProduct = (product: any) => {
|
||||
const existingItem = orderItems.find(item => item.product_id === product.id);
|
||||
|
||||
if (existingItem) {
|
||||
setOrderItems(items => items.map(item =>
|
||||
item.product_id === product.id
|
||||
? { ...item, quantity: item.quantity + 1 }
|
||||
: item
|
||||
));
|
||||
} else {
|
||||
const newItem: OrderItemCreate = {
|
||||
product_id: product.id,
|
||||
product_name: product.name,
|
||||
product_sku: product.sku || undefined,
|
||||
product_category: product.category || undefined,
|
||||
quantity: 1,
|
||||
unit_of_measure: product.unit_of_measure || 'unidad',
|
||||
unit_price: product.average_cost || product.standard_cost || 0,
|
||||
line_discount: 0
|
||||
};
|
||||
setOrderItems(items => [...items, newItem]);
|
||||
}
|
||||
setShowProductModal(false);
|
||||
};
|
||||
|
||||
const handleUpdateItemQuantity = (productId: string, quantity: number) => {
|
||||
if (quantity <= 0) {
|
||||
setOrderItems(items => items.filter(item => item.product_id !== productId));
|
||||
} else {
|
||||
setOrderItems(items => items.map(item =>
|
||||
item.product_id === productId
|
||||
? { ...item, quantity }
|
||||
: item
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateCustomer = async () => {
|
||||
if (!newCustomerData.name || !tenantId) return;
|
||||
|
||||
try {
|
||||
const customerData: CustomerCreate = {
|
||||
...newCustomerData,
|
||||
tenant_id: tenantId,
|
||||
customer_code: `CUST-${Date.now()}` // Generate simple code
|
||||
} as CustomerCreate;
|
||||
|
||||
const newCustomer = await createCustomerMutation.mutateAsync(customerData);
|
||||
setSelectedCustomer(newCustomer);
|
||||
setShowCustomerForm(false);
|
||||
setNewCustomerData({
|
||||
customer_type: CustomerType.INDIVIDUAL,
|
||||
country: 'España',
|
||||
is_active: true,
|
||||
preferred_delivery_method: DeliveryMethod.PICKUP,
|
||||
payment_terms: PaymentTerms.IMMEDIATE,
|
||||
discount_percentage: 0,
|
||||
customer_segment: CustomerSegment.REGULAR,
|
||||
priority_level: PriorityLevel.NORMAL
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating customer:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveOrder = async () => {
|
||||
if (!selectedCustomer || orderItems.length === 0 || !tenantId) return;
|
||||
|
||||
const finalOrderData: OrderCreate = {
|
||||
tenant_id: tenantId,
|
||||
customer_id: selectedCustomer.id,
|
||||
order_type: orderData.order_type || OrderType.STANDARD,
|
||||
priority: orderData.priority || PriorityLevel.NORMAL,
|
||||
requested_delivery_date: orderData.requested_delivery_date || new Date().toISOString(),
|
||||
delivery_method: orderData.delivery_method || DeliveryMethod.PICKUP,
|
||||
delivery_fee: orderData.delivery_fee || 0,
|
||||
discount_percentage: orderData.discount_percentage || 0,
|
||||
payment_terms: orderData.payment_terms || PaymentTerms.IMMEDIATE,
|
||||
order_source: orderData.order_source || OrderSource.MANUAL,
|
||||
sales_channel: orderData.sales_channel || SalesChannel.DIRECT,
|
||||
items: orderItems,
|
||||
special_instructions: orderData.special_instructions
|
||||
};
|
||||
|
||||
await onSave(finalOrderData);
|
||||
onClose();
|
||||
};
|
||||
|
||||
// Get unique categories for filtering
|
||||
const uniqueCategories = Array.from(new Set(
|
||||
finishedProducts
|
||||
.map(product => product.category)
|
||||
.filter(Boolean)
|
||||
)).sort();
|
||||
|
||||
const filteredProducts = finishedProducts.filter(product => {
|
||||
const matchesSearch = product.name.toLowerCase().includes(productSearch.toLowerCase()) ||
|
||||
(product.category && product.category.toLowerCase().includes(productSearch.toLowerCase())) ||
|
||||
(product.description && product.description.toLowerCase().includes(productSearch.toLowerCase()));
|
||||
|
||||
const matchesCategory = !selectedCategory || product.category === selectedCategory;
|
||||
|
||||
return matchesSearch && matchesCategory;
|
||||
});
|
||||
|
||||
// Convert form data to StatusModal sections
|
||||
const modalSections: StatusModalSection[] = [
|
||||
{
|
||||
title: 'Cliente',
|
||||
icon: User,
|
||||
fields: [
|
||||
{
|
||||
label: 'Cliente Seleccionado',
|
||||
value: selectedCustomer ? `${selectedCustomer.name} (${selectedCustomer.customer_code})` : 'Ninguno',
|
||||
span: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Detalles del Pedido',
|
||||
icon: FileText,
|
||||
fields: [
|
||||
{
|
||||
label: 'Tipo de Pedido',
|
||||
value: orderData.order_type || OrderType.STANDARD,
|
||||
type: 'select',
|
||||
editable: true,
|
||||
options: orderEnums.getOrderTypeOptions()
|
||||
},
|
||||
{
|
||||
label: 'Prioridad',
|
||||
value: orderData.priority || PriorityLevel.NORMAL,
|
||||
type: 'select',
|
||||
editable: true,
|
||||
options: orderEnums.getPriorityLevelOptions()
|
||||
},
|
||||
{
|
||||
label: 'Método de Entrega',
|
||||
value: orderData.delivery_method || DeliveryMethod.PICKUP,
|
||||
type: 'select',
|
||||
editable: true,
|
||||
options: orderEnums.getDeliveryMethodOptions()
|
||||
},
|
||||
{
|
||||
label: 'Fecha de Entrega',
|
||||
value: orderData.requested_delivery_date?.split('T')[0] || '',
|
||||
type: 'date',
|
||||
editable: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Productos',
|
||||
icon: Package,
|
||||
fields: [
|
||||
{
|
||||
label: 'Total de Productos',
|
||||
value: orderItems.length,
|
||||
highlight: true
|
||||
},
|
||||
{
|
||||
label: 'Lista de Productos',
|
||||
value: orderItems.length > 0 ? orderItems.map(item => `${item.product_name} (x${item.quantity})`).join(', ') : 'Sin productos',
|
||||
span: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Resumen Financiero',
|
||||
icon: Calculator,
|
||||
fields: [
|
||||
{
|
||||
label: 'Subtotal',
|
||||
value: subtotal,
|
||||
type: 'currency',
|
||||
highlight: true
|
||||
},
|
||||
{
|
||||
label: 'Descuento',
|
||||
value: -discountAmount,
|
||||
type: 'currency'
|
||||
},
|
||||
{
|
||||
label: 'IVA (21%)',
|
||||
value: taxAmount,
|
||||
type: 'currency'
|
||||
},
|
||||
{
|
||||
label: 'Gastos de Envío',
|
||||
value: orderData.delivery_fee || 0,
|
||||
type: 'currency',
|
||||
editable: true
|
||||
},
|
||||
{
|
||||
label: 'Total',
|
||||
value: total,
|
||||
type: 'currency',
|
||||
highlight: true,
|
||||
span: 2
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const handleFieldChange = (sectionIndex: number, fieldIndex: number, value: string | number) => {
|
||||
const section = modalSections[sectionIndex];
|
||||
const field = section.fields[fieldIndex];
|
||||
|
||||
// Update order data based on field changes
|
||||
if (section.title === 'Detalles del Pedido') {
|
||||
if (field.label === 'Tipo de Pedido') {
|
||||
setOrderData(prev => ({ ...prev, order_type: value as OrderType }));
|
||||
} else if (field.label === 'Prioridad') {
|
||||
setOrderData(prev => ({ ...prev, priority: value as PriorityLevel }));
|
||||
} else if (field.label === 'Método de Entrega') {
|
||||
setOrderData(prev => ({ ...prev, delivery_method: value as DeliveryMethod }));
|
||||
} else if (field.label === 'Fecha de Entrega') {
|
||||
setOrderData(prev => ({ ...prev, requested_delivery_date: value ? `${value}T12:00:00Z` : undefined }));
|
||||
}
|
||||
} else if (section.title === 'Resumen Financiero' && field.label === 'Gastos de Envío') {
|
||||
setOrderData(prev => ({ ...prev, delivery_fee: Number(value) }));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StatusModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
mode="edit"
|
||||
title="Nuevo Pedido"
|
||||
subtitle="Complete los detalles del pedido"
|
||||
sections={modalSections}
|
||||
size="2xl"
|
||||
showDefaultActions={false}
|
||||
actions={[
|
||||
{
|
||||
label: selectedCustomer ? 'Cambiar Cliente' : 'Seleccionar Cliente',
|
||||
variant: 'outline',
|
||||
onClick: () => setShowCustomerSelector(true)
|
||||
},
|
||||
{
|
||||
label: 'Agregar Productos',
|
||||
variant: 'outline',
|
||||
onClick: () => setShowProductModal(true),
|
||||
icon: Plus
|
||||
},
|
||||
{
|
||||
label: 'Cancelar',
|
||||
variant: 'outline',
|
||||
onClick: onClose
|
||||
},
|
||||
{
|
||||
label: 'Crear Pedido',
|
||||
variant: 'primary',
|
||||
onClick: handleSaveOrder,
|
||||
disabled: !selectedCustomer || orderItems.length === 0
|
||||
}
|
||||
]}
|
||||
onFieldChange={handleFieldChange}
|
||||
/>
|
||||
|
||||
{/* Legacy content for sections that need custom UI */}
|
||||
<div className="hidden">
|
||||
|
||||
</div>
|
||||
|
||||
{/* Product Selection Modal - Using StatusModal for consistency */}
|
||||
<StatusModal
|
||||
isOpen={showProductModal}
|
||||
onClose={() => setShowProductModal(false)}
|
||||
mode="view"
|
||||
title="Seleccionar Productos"
|
||||
subtitle="Elija productos terminados para agregar al pedido"
|
||||
size="2xl"
|
||||
showDefaultActions={false}
|
||||
sections={[
|
||||
{
|
||||
title: 'Filtros',
|
||||
icon: Package,
|
||||
fields: [
|
||||
{
|
||||
label: 'Buscar productos',
|
||||
value: productSearch,
|
||||
type: 'text',
|
||||
editable: true,
|
||||
placeholder: 'Buscar productos...',
|
||||
span: 1
|
||||
},
|
||||
{
|
||||
label: 'Categoría',
|
||||
value: selectedCategory,
|
||||
type: 'select',
|
||||
editable: true,
|
||||
placeholder: 'Todas las categorías',
|
||||
options: [
|
||||
{ value: '', label: 'Todas las categorías' },
|
||||
...uniqueCategories.map(category => ({
|
||||
value: category,
|
||||
label: category
|
||||
}))
|
||||
],
|
||||
span: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Productos Disponibles',
|
||||
icon: ShoppingCart,
|
||||
fields: [
|
||||
{
|
||||
label: 'Lista de productos',
|
||||
value: (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 max-h-96 overflow-y-auto">
|
||||
{productsLoading ? (
|
||||
<div className="col-span-2 text-center py-8">
|
||||
<p className="text-[var(--text-secondary)]">Cargando productos...</p>
|
||||
</div>
|
||||
) : filteredProducts.length === 0 ? (
|
||||
<div className="col-span-2 text-center py-8">
|
||||
<p className="text-[var(--text-secondary)]">
|
||||
{productSearch ? 'No se encontraron productos que coincidan con la búsqueda' : 'No hay productos finalizados disponibles'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
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>
|
||||
{product.description && (
|
||||
<p className="text-sm text-[var(--text-secondary)] mt-1">{product.description}</p>
|
||||
)}
|
||||
<div className="flex items-center mt-2 flex-wrap gap-2">
|
||||
{product.category && (
|
||||
<Badge variant="soft" color="gray">
|
||||
{product.category}
|
||||
</Badge>
|
||||
)}
|
||||
<span className="text-lg font-semibold text-[var(--color-info)]">
|
||||
€{(product.average_cost || product.standard_cost || 0).toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleAddProduct(product)}
|
||||
disabled={!product.is_active || (product.total_quantity || 0) <= 0}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
Agregar
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-[var(--text-tertiary)] mt-2 flex justify-between">
|
||||
<span>Stock: {product.total_quantity || 0} {product.unit_of_measure}</span>
|
||||
{product.sku && <span>SKU: {product.sku}</span>}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
span: 2
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
onFieldChange={(sectionIndex, fieldIndex, value) => {
|
||||
if (sectionIndex === 0) {
|
||||
if (fieldIndex === 0) {
|
||||
setProductSearch(String(value));
|
||||
} else if (fieldIndex === 1) {
|
||||
setSelectedCategory(String(value));
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* New Customer Modal - Using StatusModal for consistency */}
|
||||
<StatusModal
|
||||
isOpen={showCustomerForm}
|
||||
onClose={() => setShowCustomerForm(false)}
|
||||
mode="edit"
|
||||
title="Nuevo Cliente"
|
||||
subtitle="Complete la información del cliente"
|
||||
size="lg"
|
||||
showDefaultActions={false}
|
||||
sections={[
|
||||
{
|
||||
title: 'Información Personal',
|
||||
icon: User,
|
||||
fields: [
|
||||
{
|
||||
label: 'Nombre *',
|
||||
value: newCustomerData.name || '',
|
||||
type: 'text',
|
||||
editable: true,
|
||||
required: true,
|
||||
placeholder: 'Nombre del cliente'
|
||||
},
|
||||
{
|
||||
label: 'Teléfono',
|
||||
value: newCustomerData.phone || '',
|
||||
type: 'tel',
|
||||
editable: true,
|
||||
placeholder: 'Número de teléfono'
|
||||
},
|
||||
{
|
||||
label: 'Email',
|
||||
value: newCustomerData.email || '',
|
||||
type: 'email',
|
||||
editable: true,
|
||||
placeholder: 'Correo electrónico',
|
||||
span: 2
|
||||
},
|
||||
{
|
||||
label: 'Tipo de Cliente',
|
||||
value: newCustomerData.customer_type || CustomerType.INDIVIDUAL,
|
||||
type: 'select',
|
||||
editable: true,
|
||||
options: orderEnums.getCustomerTypeOptions()
|
||||
},
|
||||
{
|
||||
label: 'Método de Entrega Preferido',
|
||||
value: newCustomerData.preferred_delivery_method || DeliveryMethod.PICKUP,
|
||||
type: 'select',
|
||||
editable: true,
|
||||
options: orderEnums.getDeliveryMethodOptions()
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
actions={[
|
||||
{
|
||||
label: 'Cancelar',
|
||||
variant: 'outline',
|
||||
onClick: () => setShowCustomerForm(false)
|
||||
},
|
||||
{
|
||||
label: 'Crear Cliente',
|
||||
variant: 'primary',
|
||||
onClick: handleCreateCustomer,
|
||||
disabled: !newCustomerData.name
|
||||
}
|
||||
]}
|
||||
onFieldChange={(sectionIndex, fieldIndex, value) => {
|
||||
// Get the customer modal sections instead of the order modal sections
|
||||
const customerModalSections = [
|
||||
{
|
||||
title: 'Información Personal',
|
||||
icon: User,
|
||||
fields: [
|
||||
{ label: 'Nombre *' },
|
||||
{ label: 'Teléfono' },
|
||||
{ label: 'Email' },
|
||||
{ label: 'Tipo de Cliente' },
|
||||
{ label: 'Método de Entrega Preferido' }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const field = customerModalSections[sectionIndex]?.fields[fieldIndex];
|
||||
if (!field) return;
|
||||
|
||||
if (field.label === 'Nombre *') {
|
||||
setNewCustomerData(prev => ({ ...prev, name: String(value) }));
|
||||
} else if (field.label === 'Teléfono') {
|
||||
setNewCustomerData(prev => ({ ...prev, phone: String(value) }));
|
||||
} else if (field.label === 'Email') {
|
||||
setNewCustomerData(prev => ({ ...prev, email: String(value) }));
|
||||
} else if (field.label === 'Tipo de Cliente') {
|
||||
setNewCustomerData(prev => ({ ...prev, customer_type: value as CustomerType }));
|
||||
} else if (field.label === 'Método de Entrega Preferido') {
|
||||
setNewCustomerData(prev => ({ ...prev, preferred_delivery_method: value as DeliveryMethod }));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Customer Selector Modal */}
|
||||
<StatusModal
|
||||
isOpen={showCustomerSelector}
|
||||
onClose={() => setShowCustomerSelector(false)}
|
||||
mode="view"
|
||||
title="Seleccionar Cliente"
|
||||
subtitle="Elija un cliente existente o cree uno nuevo"
|
||||
size="lg"
|
||||
showDefaultActions={false}
|
||||
sections={[
|
||||
{
|
||||
title: 'Clientes Disponibles',
|
||||
icon: User,
|
||||
fields: [
|
||||
{
|
||||
label: 'Lista de clientes',
|
||||
value: (
|
||||
<div className="space-y-3 max-h-96 overflow-y-auto">
|
||||
{customers.length === 0 ? (
|
||||
<div className="text-center py-8 text-[var(--text-secondary)]">
|
||||
<p>No hay clientes disponibles</p>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setShowCustomerSelector(false);
|
||||
setShowCustomerForm(true);
|
||||
}}
|
||||
className="mt-2"
|
||||
>
|
||||
Crear Primer Cliente
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
customers.map(customer => (
|
||||
<div key={customer.id} className="border border-[var(--border-primary)] rounded-lg p-4 hover:bg-[var(--bg-secondary)]">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-[var(--text-primary)]">{customer.name}</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
Código: {customer.customer_code}
|
||||
</p>
|
||||
<div className="text-sm text-[var(--text-secondary)] space-y-1 mt-2">
|
||||
{customer.email && <div>📧 {customer.email}</div>}
|
||||
{customer.phone && <div>📞 {customer.phone}</div>}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSelectedCustomer(customer);
|
||||
setShowCustomerSelector(false);
|
||||
}}
|
||||
variant={selectedCustomer?.id === customer.id ? 'primary' : 'outline'}
|
||||
>
|
||||
{selectedCustomer?.id === customer.id ? 'Seleccionado' : 'Seleccionar'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
span: 2
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
actions={[
|
||||
{
|
||||
label: 'Nuevo Cliente',
|
||||
variant: 'outline',
|
||||
onClick: () => {
|
||||
setShowCustomerSelector(false);
|
||||
setShowCustomerForm(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Cerrar',
|
||||
variant: 'primary',
|
||||
onClick: () => setShowCustomerSelector(false)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrderFormModal;
|
||||
1
frontend/src/components/domain/orders/index.ts
Normal file
1
frontend/src/components/domain/orders/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as OrderFormModal } from './OrderFormModal';
|
||||
@@ -110,12 +110,12 @@ export const StatusCard: React.FC<StatusCardProps> = ({
|
||||
return (
|
||||
<Card
|
||||
className={`
|
||||
p-6 transition-all duration-200 border-l-4 hover:shadow-lg
|
||||
${hasInteraction ? 'hover:shadow-xl cursor-pointer hover:scale-[1.02]' : ''}
|
||||
p-4 sm:p-6 transition-all duration-200 border-l-4 hover:shadow-lg
|
||||
${hasInteraction ? 'hover:shadow-xl cursor-pointer hover:scale-[1.01]' : ''}
|
||||
${statusIndicator.isCritical
|
||||
? 'ring-2 ring-red-200 shadow-md border-l-8'
|
||||
? 'ring-2 ring-red-200 shadow-md border-l-6 sm:border-l-8'
|
||||
: statusIndicator.isHighlight
|
||||
? 'ring-1 ring-yellow-200'
|
||||
? 'ring-1 ring-yellow-200 border-l-6'
|
||||
: ''
|
||||
}
|
||||
${className}
|
||||
@@ -130,30 +130,30 @@ export const StatusCard: React.FC<StatusCardProps> = ({
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-4 sm:space-y-5">
|
||||
{/* Header with status indicator */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-4 flex-1">
|
||||
<div
|
||||
className={`flex-shrink-0 p-3 rounded-xl shadow-sm ${
|
||||
className={`flex-shrink-0 p-2 sm:p-3 rounded-xl shadow-sm ${
|
||||
statusIndicator.isCritical ? 'ring-2 ring-white' : ''
|
||||
}`}
|
||||
style={{ backgroundColor: `${statusIndicator.color}20` }}
|
||||
>
|
||||
{StatusIcon && (
|
||||
<StatusIcon
|
||||
className="w-5 h-5"
|
||||
className="w-4 h-4 sm:w-5 sm:h-5"
|
||||
style={{ color: statusIndicator.color }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-semibold text-[var(--text-primary)] text-lg leading-tight mb-1">
|
||||
<div className="font-semibold text-[var(--text-primary)] text-base sm:text-lg leading-tight mb-1 truncate">
|
||||
{title}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div
|
||||
className={`inline-flex items-center px-3 py-1.5 rounded-full text-xs font-semibold transition-all ${
|
||||
className={`inline-flex items-center px-2 sm:px-3 py-1 sm:py-1.5 rounded-full text-xs font-semibold transition-all ${
|
||||
statusIndicator.isCritical
|
||||
? 'bg-red-100 text-red-800 ring-2 ring-red-300 shadow-sm animate-pulse'
|
||||
: statusIndicator.isHighlight
|
||||
@@ -186,25 +186,25 @@ export const StatusCard: React.FC<StatusCardProps> = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right flex-shrink-0 ml-4">
|
||||
<div className="text-3xl font-bold text-[var(--text-primary)] leading-none">
|
||||
<div className="text-right flex-shrink-0 ml-4 min-w-0">
|
||||
<div className="text-2xl sm:text-3xl font-bold text-[var(--text-primary)] leading-none truncate">
|
||||
{primaryValue}
|
||||
</div>
|
||||
{primaryValueLabel && (
|
||||
<div className="text-xs text-[var(--text-tertiary)] uppercase tracking-wide mt-1">
|
||||
<div className="text-xs text-[var(--text-tertiary)] uppercase tracking-wide mt-1 truncate">
|
||||
{primaryValueLabel}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Secondary info */}
|
||||
{/* Secondary info - Mobile optimized */}
|
||||
{secondaryInfo && (
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-[var(--text-secondary)]">
|
||||
<div className="flex items-center justify-between text-sm gap-2">
|
||||
<span className="text-[var(--text-secondary)] truncate flex-shrink-0">
|
||||
{secondaryInfo.label}
|
||||
</span>
|
||||
<span className="font-medium text-[var(--text-primary)]">
|
||||
<span className="font-medium text-[var(--text-primary)] truncate text-right">
|
||||
{secondaryInfo.value}
|
||||
</span>
|
||||
</div>
|
||||
@@ -228,36 +228,36 @@ export const StatusCard: React.FC<StatusCardProps> = ({
|
||||
)}
|
||||
|
||||
|
||||
{/* Metadata */}
|
||||
{/* Metadata - Improved mobile layout */}
|
||||
{metadata.length > 0 && (
|
||||
<div className="text-xs text-[var(--text-secondary)] space-y-1">
|
||||
{metadata.map((item, index) => (
|
||||
<div key={index}>{item}</div>
|
||||
<div key={index} className="truncate" title={item}>{item}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Simplified Action System */}
|
||||
{/* Simplified Action System - Mobile optimized */}
|
||||
{actions.length > 0 && (
|
||||
<div className="pt-4 border-t border-[var(--border-primary)]">
|
||||
<div className="pt-3 sm:pt-4 border-t border-[var(--border-primary)]">
|
||||
{/* All actions in a clean horizontal layout */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center justify-between gap-2 flex-wrap">
|
||||
|
||||
{/* Primary action as a subtle text button */}
|
||||
{primaryActions.length > 0 && (
|
||||
<button
|
||||
onClick={primaryActions[0].onClick}
|
||||
className={`
|
||||
flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg
|
||||
transition-all duration-200 hover:scale-105 active:scale-95
|
||||
flex items-center gap-1 sm:gap-2 px-2 sm:px-3 py-1.5 sm:py-2 text-xs sm:text-sm font-medium rounded-lg
|
||||
transition-all duration-200 hover:scale-105 active:scale-95 flex-shrink-0
|
||||
${primaryActions[0].destructive
|
||||
? 'text-red-600 hover:bg-red-50 hover:text-red-700'
|
||||
: 'text-[var(--color-primary-600)] hover:bg-[var(--color-primary-50)] hover:text-[var(--color-primary-700)]'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{primaryActions[0].icon && React.createElement(primaryActions[0].icon, { className: "w-4 h-4" })}
|
||||
<span>{primaryActions[0].label}</span>
|
||||
{primaryActions[0].icon && React.createElement(primaryActions[0].icon, { className: "w-3 h-3 sm:w-4 sm:h-4" })}
|
||||
<span className="truncate">{primaryActions[0].label}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -269,14 +269,14 @@ export const StatusCard: React.FC<StatusCardProps> = ({
|
||||
onClick={action.onClick}
|
||||
title={action.label}
|
||||
className={`
|
||||
p-2 rounded-lg transition-all duration-200 hover:scale-110 active:scale-95
|
||||
p-1.5 sm:p-2 rounded-lg transition-all duration-200 hover:scale-110 active:scale-95
|
||||
${action.destructive
|
||||
? 'text-red-500 hover:bg-red-50 hover:text-red-600'
|
||||
: 'text-[var(--text-tertiary)] hover:text-[var(--text-primary)] hover:bg-[var(--surface-secondary)]'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{action.icon && React.createElement(action.icon, { className: "w-4 h-4" })}
|
||||
{action.icon && React.createElement(action.icon, { className: "w-3 h-3 sm:w-4 sm:h-4" })}
|
||||
</button>
|
||||
))}
|
||||
|
||||
@@ -287,14 +287,14 @@ export const StatusCard: React.FC<StatusCardProps> = ({
|
||||
onClick={action.onClick}
|
||||
title={action.label}
|
||||
className={`
|
||||
p-2 rounded-lg transition-all duration-200 hover:scale-110 active:scale-95
|
||||
p-1.5 sm:p-2 rounded-lg transition-all duration-200 hover:scale-110 active:scale-95
|
||||
${action.destructive
|
||||
? 'text-red-500 hover:bg-red-50 hover:text-red-600'
|
||||
: 'text-[var(--color-primary-500)] hover:text-[var(--color-primary-600)] hover:bg-[var(--color-primary-50)]'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{action.icon && React.createElement(action.icon, { className: "w-4 h-4" })}
|
||||
{action.icon && React.createElement(action.icon, { className: "w-3 h-3 sm:w-4 sm:h-4" })}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user