Add order page with real API calls

This commit is contained in:
Urtzi Alfaro
2025-09-19 11:44:38 +02:00
parent 447e2a5012
commit 105410c9d3
22 changed files with 2556 additions and 463 deletions

View File

@@ -25,6 +25,9 @@ class ApiClient {
private tenantId: string | null = null;
private refreshToken: string | null = null;
private isRefreshing: boolean = false;
private refreshAttempts: number = 0;
private maxRefreshAttempts: number = 3;
private lastRefreshAttempt: number = 0;
private failedQueue: Array<{
resolve: (value?: any) => void;
reject: (error?: any) => void;
@@ -72,6 +75,14 @@ class ApiClient {
// Check if error is 401 and we have a refresh token
if (error.response?.status === 401 && this.refreshToken && !originalRequest._retry) {
// Check if we've exceeded max refresh attempts in a short time
const now = Date.now();
if (this.refreshAttempts >= this.maxRefreshAttempts && (now - this.lastRefreshAttempt) < 30000) {
console.log('Max refresh attempts exceeded, logging out');
await this.handleAuthFailure();
return Promise.reject(this.handleError(error));
}
if (this.isRefreshing) {
// If already refreshing, queue this request
return new Promise((resolve, reject) => {
@@ -81,8 +92,12 @@ class ApiClient {
originalRequest._retry = true;
this.isRefreshing = true;
this.refreshAttempts++;
this.lastRefreshAttempt = now;
try {
console.log(`Attempting token refresh (attempt ${this.refreshAttempts})...`);
// Attempt to refresh the token
const response = await this.client.post('/auth/refresh', {
refresh_token: this.refreshToken
@@ -90,6 +105,11 @@ class ApiClient {
const { access_token, refresh_token } = response.data;
console.log('Token refresh successful');
// Reset refresh attempts on success
this.refreshAttempts = 0;
// Update tokens
this.setAuthToken(access_token);
if (refresh_token) {
@@ -107,6 +127,7 @@ class ApiClient {
return this.client(originalRequest);
} catch (refreshError) {
console.error(`Token refresh failed (attempt ${this.refreshAttempts}):`, refreshError);
// Refresh failed, clear tokens and redirect to login
this.processQueue(refreshError, null);
await this.handleAuthFailure();
@@ -165,13 +186,14 @@ class ApiClient {
try {
// Dynamically import to avoid circular dependency
const { useAuthStore } = await import('../../stores/auth.store');
const store = useAuthStore.getState();
const setState = useAuthStore.setState;
// Update the store with new tokens
store.token = accessToken;
if (refreshToken) {
store.refreshToken = refreshToken;
}
setState(state => ({
...state,
token: accessToken,
refreshToken: refreshToken || state.refreshToken,
}));
} catch (error) {
console.warn('Failed to update auth store:', error);
}

View File

@@ -3,18 +3,86 @@
* Based on backend schemas in services/orders/app/schemas/order_schemas.py
*/
export type CustomerType = 'individual' | 'business' | 'central_bakery';
export type DeliveryMethod = 'delivery' | 'pickup';
export type PaymentTerms = 'immediate' | 'net_30' | 'net_60';
export type PaymentMethod = 'cash' | 'card' | 'bank_transfer' | 'account';
export type PaymentStatus = 'pending' | 'partial' | 'paid' | 'failed' | 'refunded';
export type CustomerSegment = 'vip' | 'regular' | 'wholesale';
export type PriorityLevel = 'high' | 'normal' | 'low';
export type OrderType = 'standard' | 'rush' | 'recurring' | 'special';
export type OrderStatus = 'pending' | 'confirmed' | 'in_production' | 'ready' | 'out_for_delivery' | 'delivered' | 'cancelled' | 'failed';
export type OrderSource = 'manual' | 'online' | 'phone' | 'app' | 'api';
export type SalesChannel = 'direct' | 'wholesale' | 'retail';
export type BusinessModel = 'individual_bakery' | 'central_bakery';
export enum CustomerType {
INDIVIDUAL = 'individual',
BUSINESS = 'business',
CENTRAL_BAKERY = 'central_bakery'
}
export enum DeliveryMethod {
DELIVERY = 'delivery',
PICKUP = 'pickup'
}
export enum PaymentTerms {
IMMEDIATE = 'immediate',
NET_30 = 'net_30',
NET_60 = 'net_60'
}
export enum PaymentMethod {
CASH = 'cash',
CARD = 'card',
BANK_TRANSFER = 'bank_transfer',
ACCOUNT = 'account'
}
export enum PaymentStatus {
PENDING = 'pending',
PARTIAL = 'partial',
PAID = 'paid',
FAILED = 'failed',
REFUNDED = 'refunded'
}
export enum CustomerSegment {
VIP = 'vip',
REGULAR = 'regular',
WHOLESALE = 'wholesale'
}
export enum PriorityLevel {
HIGH = 'high',
NORMAL = 'normal',
LOW = 'low'
}
export enum OrderType {
STANDARD = 'standard',
RUSH = 'rush',
RECURRING = 'recurring',
SPECIAL = 'special'
}
export enum OrderStatus {
PENDING = 'pending',
CONFIRMED = 'confirmed',
IN_PRODUCTION = 'in_production',
READY = 'ready',
OUT_FOR_DELIVERY = 'out_for_delivery',
DELIVERED = 'delivered',
CANCELLED = 'cancelled',
FAILED = 'failed'
}
export enum OrderSource {
MANUAL = 'manual',
ONLINE = 'online',
PHONE = 'phone',
APP = 'app',
API = 'api'
}
export enum SalesChannel {
DIRECT = 'direct',
WHOLESALE = 'wholesale',
RETAIL = 'retail'
}
export enum BusinessModel {
INDIVIDUAL_BAKERY = 'individual_bakery',
CENTRAL_BAKERY = 'central_bakery'
}
// ===== Customer Types =====

View 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;

View File

@@ -0,0 +1 @@
export { default as OrderFormModal } from './OrderFormModal';

View File

@@ -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>

View File

@@ -1,5 +1,6 @@
import React, { createContext, useContext, useEffect, ReactNode } from 'react';
import React, { createContext, useContext, useEffect, ReactNode, useState } from 'react';
import { useAuthStore, User } from '../stores/auth.store';
import { authService } from '../api/services/auth';
interface AuthContextType {
user: User | null;
@@ -31,29 +32,43 @@ interface AuthProviderProps {
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const authStore = useAuthStore();
const [isInitializing, setIsInitializing] = useState(true);
// Initialize auth on mount
useEffect(() => {
const initializeAuth = async () => {
setIsInitializing(true);
// Wait a bit for zustand persist to rehydrate
await new Promise(resolve => setTimeout(resolve, 100));
// Check if we have stored auth data
const storedAuth = localStorage.getItem('auth-storage');
if (storedAuth) {
if (authStore.token && authStore.refreshToken) {
try {
const { state } = JSON.parse(storedAuth);
if (state.token && state.user) {
// Validate token by attempting to refresh
try {
await authStore.refreshAuth();
} catch (error) {
// Token is invalid, clear auth
authStore.logout();
}
}
// Validate current token by trying to verify it
await authService.verifyToken();
console.log('Token is valid, user authenticated');
} catch (error) {
console.error('Error parsing stored auth:', error);
authStore.logout();
console.log('Token expired, attempting refresh...');
// Token is invalid, try to refresh
try {
await authStore.refreshAuth();
console.log('Token refreshed successfully');
} catch (refreshError) {
console.log('Token refresh failed, logging out:', refreshError);
// Refresh failed, clear auth
authStore.logout();
}
}
} else if (authStore.isAuthenticated) {
// User is marked as authenticated but no tokens, logout
console.log('No tokens found but user marked as authenticated, logging out');
authStore.logout();
} else {
console.log('No stored auth data found');
}
setIsInitializing(false);
};
initializeAuth();
@@ -63,7 +78,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const contextValue: AuthContextType = {
user: authStore.user,
isAuthenticated: authStore.isAuthenticated,
isLoading: authStore.isLoading,
isLoading: authStore.isLoading || isInitializing,
error: authStore.error,
login: authStore.login,
logout: authStore.logout,

View File

@@ -0,0 +1,106 @@
{
"customer_types": {
"individual": "Individual",
"business": "Empresa",
"central_bakery": "Panadería Central"
},
"delivery_methods": {
"pickup": "Recogida",
"delivery": "Entrega a domicilio"
},
"payment_terms": {
"immediate": "Inmediato",
"net_30": "Neto 30 días",
"net_60": "Neto 60 días"
},
"payment_methods": {
"cash": "Efectivo",
"card": "Tarjeta",
"bank_transfer": "Transferencia bancaria",
"account": "Cuenta"
},
"payment_status": {
"pending": "Pendiente",
"partial": "Parcial",
"paid": "Pagado",
"failed": "Fallido",
"refunded": "Reembolsado"
},
"customer_segments": {
"regular": "Regular",
"vip": "VIP",
"wholesale": "Mayorista"
},
"priority_levels": {
"low": "Baja",
"normal": "Normal",
"high": "Alta"
},
"order_types": {
"standard": "Estándar",
"rush": "Urgente",
"recurring": "Recurrente",
"special": "Especial"
},
"order_status": {
"pending": "Pendiente",
"confirmed": "Confirmado",
"in_production": "En Producción",
"ready": "Listo",
"out_for_delivery": "En Reparto",
"delivered": "Entregado",
"cancelled": "Cancelado",
"failed": "Fallido"
},
"order_sources": {
"manual": "Manual",
"online": "En línea",
"phone": "Teléfono",
"app": "Aplicación",
"api": "API"
},
"sales_channels": {
"direct": "Directo",
"wholesale": "Mayorista",
"retail": "Minorista"
},
"labels": {
"name": "Nombre",
"business_name": "Nombre Comercial",
"customer_code": "Código de Cliente",
"email": "Email",
"phone": "Teléfono",
"address": "Dirección",
"city": "Ciudad",
"state": "Estado/Provincia",
"postal_code": "Código Postal",
"country": "País",
"customer_type": "Tipo de Cliente",
"delivery_method": "Método de Entrega",
"payment_terms": "Términos de Pago",
"payment_method": "Método de Pago",
"payment_status": "Estado del Pago",
"customer_segment": "Segmento de Cliente",
"priority_level": "Nivel de Prioridad",
"order_type": "Tipo de Pedido",
"order_status": "Estado del Pedido",
"order_source": "Origen del Pedido",
"sales_channel": "Canal de Ventas",
"order_number": "Número de Pedido",
"order_date": "Fecha del Pedido",
"delivery_date": "Fecha de Entrega",
"total_amount": "Importe Total",
"subtotal": "Subtotal",
"discount": "Descuento",
"tax": "Impuestos",
"shipping": "Envío",
"special_instructions": "Instrucciones Especiales"
},
"descriptions": {
"customer_type": "Tipo de cliente para determinar precios y términos comerciales",
"delivery_method": "Método preferido para la entrega de pedidos",
"payment_terms": "Términos y condiciones de pago acordados",
"customer_segment": "Segmentación del cliente para ofertas personalizadas",
"priority_level": "Nivel de prioridad para el procesamiento de pedidos"
}
}

View File

@@ -4,6 +4,7 @@ import authEs from './es/auth.json';
import inventoryEs from './es/inventory.json';
import foodSafetyEs from './es/foodSafety.json';
import suppliersEs from './es/suppliers.json';
import ordersEs from './es/orders.json';
import errorsEs from './es/errors.json';
// Translation resources by language
@@ -14,6 +15,7 @@ export const resources = {
inventory: inventoryEs,
foodSafety: foodSafetyEs,
suppliers: suppliersEs,
orders: ordersEs,
errors: errorsEs,
},
};
@@ -37,7 +39,7 @@ export const languageConfig = {
};
// Namespaces available in translations
export const namespaces = ['common', 'auth', 'inventory', 'foodSafety', 'suppliers', 'errors'] as const;
export const namespaces = ['common', 'auth', 'inventory', 'foodSafety', 'suppliers', 'orders', 'errors'] as const;
export type Namespace = typeof namespaces[number];
// Helper function to get language display name
@@ -51,7 +53,7 @@ export const isSupportedLanguage = (language: string): language is SupportedLang
};
// Export individual language modules for direct imports
export { commonEs, authEs, inventoryEs, foodSafetyEs, suppliersEs, errorsEs };
export { commonEs, authEs, inventoryEs, foodSafetyEs, suppliersEs, ordersEs, errorsEs };
// Default export with all translations
export default resources;

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { Plus, Search, Download, ShoppingCart, Truck, DollarSign, Calendar, Clock, CheckCircle, AlertCircle, Package, Eye, Loader, Edit, ArrowRight, Play, Pause, X, Save } from 'lucide-react';
import { Plus, Search, Download, ShoppingCart, Truck, DollarSign, Calendar, Clock, CheckCircle, AlertCircle, Package, Eye, Loader, Edit, ArrowRight, Play, Pause, X, Save, Building2 } from 'lucide-react';
import { Button, Input, Card, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui';
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
import { PageHeader } from '../../../../components/layout';
@@ -410,17 +410,22 @@ const ProcurementPage: React.FC = () => {
id={plan.plan_number}
statusIndicator={statusConfig}
title={`Plan ${plan.plan_number}`}
subtitle={new Date(plan.plan_date).toLocaleDateString('es-ES')}
primaryValue={formatters.currency(plan.total_estimated_cost)}
primaryValueLabel={`${plan.total_requirements} requerimientos`}
subtitle={`${new Date(plan.plan_date).toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit', year: '2-digit' })} • ${plan.procurement_strategy}`}
primaryValue={plan.total_requirements}
primaryValueLabel="requerimientos"
secondaryInfo={{
label: 'Período',
value: `${new Date(plan.plan_period_start).toLocaleDateString('es-ES')} - ${new Date(plan.plan_period_end).toLocaleDateString('es-ES')}`
label: 'Presupuesto',
value: `${formatters.compact(plan.total_estimated_cost)}`
}}
progress={plan.planning_horizon_days ? {
label: `${plan.planning_horizon_days} días de horizonte`,
percentage: Math.min((plan.planning_horizon_days / 30) * 100, 100),
color: plan.planning_horizon_days > 14 ? '#10b981' : plan.planning_horizon_days > 7 ? '#f59e0b' : '#ef4444'
} : undefined}
metadata={[
`${plan.planning_horizon_days} días de horizonte`,
`Estrategia: ${plan.procurement_strategy}`,
...(plan.special_requirements ? [`"${plan.special_requirements}"`] : [])
`Período: ${new Date(plan.plan_period_start).toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit' })} - ${new Date(plan.plan_period_end).toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit' })}`,
`Creado: ${new Date(plan.created_at).toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit' })}`,
...(plan.special_requirements ? [`Req. especiales: ${plan.special_requirements}`] : [])
]}
actions={actions}
/>
@@ -541,24 +546,43 @@ const ProcurementPage: React.FC = () => {
isCritical: true
}}
title={requirement.product_name}
subtitle={requirement.requirement_number}
primaryValue={`${requirement.required_quantity} ${requirement.unit_of_measure}`}
primaryValueLabel="Cantidad requerida"
subtitle={`${requirement.requirement_number}${requirement.supplier_name || 'Sin proveedor'}`}
primaryValue={requirement.required_quantity}
primaryValueLabel={requirement.unit_of_measure}
secondaryInfo={{
label: 'Fecha límite',
value: new Date(requirement.required_by_date).toLocaleDateString('es-ES')
label: 'Límite',
value: new Date(requirement.required_by_date).toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit' })
}}
progress={requirement.current_stock_level && requirement.required_quantity ? {
label: `${Math.round((requirement.current_stock_level / requirement.required_quantity) * 100)}% cubierto`,
percentage: Math.min((requirement.current_stock_level / requirement.required_quantity) * 100, 100),
color: requirement.current_stock_level >= requirement.required_quantity ? '#10b981' : requirement.current_stock_level >= requirement.required_quantity * 0.5 ? '#f59e0b' : '#ef4444'
} : undefined}
metadata={[
`Stock actual: ${requirement.current_stock_level} ${requirement.unit_of_measure}`,
`Proveedor: ${requirement.supplier_name || 'No asignado'}`,
`Costo estimado: ${formatters.currency(requirement.estimated_total_cost || 0)}`
`Stock: ${requirement.current_stock_level || 0} ${requirement.unit_of_measure}`,
`Necesario: ${requirement.required_quantity - (requirement.current_stock_level || 0)} ${requirement.unit_of_measure}`,
`Costo: ${formatters.compact(requirement.estimated_total_cost || 0)}`,
`Días restantes: ${Math.ceil((new Date(requirement.required_by_date).getTime() - Date.now()) / (1000 * 60 * 60 * 24))}`
]}
actions={[
{
label: 'Ver Detalles',
icon: Eye,
variant: 'outline',
variant: 'primary',
priority: 'primary',
onClick: () => console.log('View requirement details')
},
{
label: 'Asignar Proveedor',
icon: Building2,
priority: 'secondary',
onClick: () => console.log('Assign supplier')
},
{
label: 'Comprar Ahora',
icon: ShoppingCart,
priority: 'secondary',
onClick: () => console.log('Purchase now')
}
]}
/>

View File

@@ -254,11 +254,11 @@ const RecipesPage: React.FC = () => {
statusIndicator={statusConfig}
title={recipe.name}
subtitle={`${statusConfig.text}${statusConfig.difficultyLabel}${statusConfig.isHighlight ? ' ★' + recipe.rating : ''}`}
primaryValue={formatters.currency(recipe.profit)}
primaryValueLabel="margen"
primaryValue={recipe.ingredients.length}
primaryValueLabel="ingredientes"
secondaryInfo={{
label: 'Precio de venta',
value: `${formatters.currency(recipe.price)} (costo: ${formatters.currency(recipe.cost)})`
label: 'Margen',
value: `${formatters.compact(recipe.profit)}`
}}
progress={{
label: 'Margen de beneficio',

View File

@@ -216,12 +216,12 @@ const SuppliersPage: React.FC = () => {
id={supplier.id}
statusIndicator={statusConfig}
title={supplier.name}
subtitle={supplier.supplier_code}
primaryValue={supplier.city || 'Sin ubicación'}
primaryValueLabel={getSupplierTypeText(supplier.supplier_type)}
subtitle={`${getSupplierTypeText(supplier.supplier_type)}${supplier.city || 'Sin ubicación'}`}
primaryValue={supplier.standard_lead_time || 0}
primaryValueLabel="días"
secondaryInfo={{
label: 'Condiciones',
value: getPaymentTermsText(supplier.payment_terms)
label: 'Pedido Min.',
value: `${formatters.compact(supplier.minimum_order_amount || 0)}`
}}
metadata={[
supplier.contact_person || 'Sin contacto',

View File

@@ -377,11 +377,11 @@ const TeamPage: React.FC = () => {
statusIndicator={getMemberStatusConfig(member)}
title={member.user?.full_name || member.user_full_name}
subtitle={member.user?.email || member.user_email}
primaryValue={member.is_active ? 'Activo' : 'Inactivo'}
primaryValueLabel="Estado"
primaryValue={Math.floor((Date.now() - new Date(member.joined_at).getTime()) / (1000 * 60 * 60 * 24))}
primaryValueLabel="días"
secondaryInfo={{
label: 'Se unió',
value: new Date(member.joined_at).toLocaleDateString('es-ES')
label: 'Estado',
value: member.is_active ? 'Activo' : 'Inactivo'
}}
metadata={[
`Email: ${member.user?.email || member.user_email}`,

View File

@@ -256,16 +256,15 @@ export const useAuthStore = create<AuthState>()(
onRehydrateStorage: () => (state) => {
// Initialize API client with stored tokens when store rehydrates
if (state?.token) {
import('../api').then(({ apiClient }) => {
apiClient.setAuthToken(state.token!);
if (state.refreshToken) {
apiClient.setRefreshToken(state.refreshToken);
}
// Use direct import to avoid timing issues
apiClient.setAuthToken(state.token);
if (state.refreshToken) {
apiClient.setRefreshToken(state.refreshToken);
}
if (state.user?.tenant_id) {
apiClient.setTenantId(state.user.tenant_id);
}
});
if (state.user?.tenant_id) {
apiClient.setTenantId(state.user.tenant_id);
}
}
},
}

View File

@@ -17,6 +17,20 @@ import {
type EnumOption
} from '../api/types/suppliers';
import {
CustomerType,
DeliveryMethod,
PaymentTerms as OrderPaymentTerms,
PaymentMethod,
PaymentStatus,
CustomerSegment,
PriorityLevel,
OrderType,
OrderStatus,
OrderSource,
SalesChannel
} from '../api/types/orders';
/**
* Generic function to convert enum to select options with i18n translations
*/
@@ -163,4 +177,132 @@ export function isValidEnumValue<T>(
value: unknown
): value is T {
return Object.values(enumObject).includes(value as T);
}
/**
* Hook for orders enum utilities
*/
export function useOrderEnums() {
const { t } = useTranslation('orders');
return {
// Customer Type
getCustomerTypeOptions: (): SelectOption[] =>
enumToSelectOptions(CustomerType, 'customer_types', t),
getCustomerTypeLabel: (type: CustomerType): string => {
if (!type) return 'Tipo no definido';
const translated = t(`customer_types.${type}`);
if (translated === `customer_types.${type}`) {
return type.charAt(0).toUpperCase() + type.slice(1);
}
return translated;
},
// Delivery Method
getDeliveryMethodOptions: (): SelectOption[] =>
enumToSelectOptions(DeliveryMethod, 'delivery_methods', t),
getDeliveryMethodLabel: (method: DeliveryMethod): string => {
if (!method) return 'Método no definido';
const translated = t(`delivery_methods.${method}`);
if (translated === `delivery_methods.${method}`) {
return method.charAt(0).toUpperCase() + method.slice(1);
}
return translated;
},
// Payment Terms
getPaymentTermsOptions: (): SelectOption[] =>
enumToSelectOptions(OrderPaymentTerms, 'payment_terms', t),
getPaymentTermsLabel: (terms: OrderPaymentTerms): string => {
if (!terms) return 'Términos no definidos';
return t(`payment_terms.${terms}`);
},
// Payment Method
getPaymentMethodOptions: (): SelectOption[] =>
enumToSelectOptions(PaymentMethod, 'payment_methods', t),
getPaymentMethodLabel: (method: PaymentMethod): string => {
if (!method) return 'Método no definido';
return t(`payment_methods.${method}`);
},
// Payment Status
getPaymentStatusOptions: (): SelectOption[] =>
enumToSelectOptions(PaymentStatus, 'payment_status', t),
getPaymentStatusLabel: (status: PaymentStatus): string => {
if (!status) return 'Estado no definido';
return t(`payment_status.${status}`);
},
// Customer Segment
getCustomerSegmentOptions: (): SelectOption[] =>
enumToSelectOptions(CustomerSegment, 'customer_segments', t),
getCustomerSegmentLabel: (segment: CustomerSegment): string => {
if (!segment) return 'Segmento no definido';
return t(`customer_segments.${segment}`);
},
// Priority Level
getPriorityLevelOptions: (): SelectOption[] =>
enumToSelectOptions(PriorityLevel, 'priority_levels', t),
getPriorityLevelLabel: (level: PriorityLevel): string => {
if (!level) return 'Prioridad no definida';
return t(`priority_levels.${level}`);
},
// Order Type
getOrderTypeOptions: (): SelectOption[] =>
enumToSelectOptions(OrderType, 'order_types', t),
getOrderTypeLabel: (type: OrderType): string => {
if (!type) return 'Tipo no definido';
const translated = t(`order_types.${type}`);
// If translation failed, return a fallback
if (translated === `order_types.${type}`) {
return type.charAt(0).toUpperCase() + type.slice(1);
}
return translated;
},
// Order Status
getOrderStatusOptions: (): SelectOption[] =>
enumToSelectOptions(OrderStatus, 'order_status', t),
getOrderStatusLabel: (status: OrderStatus): string => {
if (!status) return 'Estado no definido';
return t(`order_status.${status}`);
},
// Order Source
getOrderSourceOptions: (): SelectOption[] =>
enumToSelectOptions(OrderSource, 'order_sources', t),
getOrderSourceLabel: (source: OrderSource): string => {
if (!source) return 'Origen no definido';
return t(`order_sources.${source}`);
},
// Sales Channel
getSalesChannelOptions: (): SelectOption[] =>
enumToSelectOptions(SalesChannel, 'sales_channels', t),
getSalesChannelLabel: (channel: SalesChannel): string => {
if (!channel) return 'Canal no definido';
return t(`sales_channels.${channel}`);
},
// Field Labels
getFieldLabel: (field: string): string =>
t(`labels.${field}`),
getFieldDescription: (field: string): string =>
t(`descriptions.${field}`)
};
}