Improve backend

This commit is contained in:
Urtzi Alfaro
2025-11-18 07:17:17 +01:00
parent d36f2ab9af
commit 5c45164c8e
61 changed files with 9846 additions and 495 deletions

View File

@@ -506,7 +506,7 @@ export function ActionQueueCard({
if (loading || !actionQueue) {
return (
<div className="rounded-xl shadow-md p-6" style={{ backgroundColor: 'var(--bg-primary)' }}>
<div className="rounded-xl shadow-lg p-6 border" style={{ backgroundColor: 'var(--bg-primary)', borderColor: 'var(--border-primary)' }}>
<div className="animate-pulse space-y-4">
<div className="h-6 rounded w-1/2" style={{ backgroundColor: 'var(--bg-tertiary)' }}></div>
<div className="h-32 rounded" style={{ backgroundColor: 'var(--bg-tertiary)' }}></div>
@@ -519,7 +519,7 @@ export function ActionQueueCard({
if (!actionQueue.actions || actionQueue.actions.length === 0) {
return (
<div
className="border-2 rounded-xl p-8 text-center"
className="border-2 rounded-xl p-8 text-center shadow-lg"
style={{
backgroundColor: 'var(--color-success-50)',
borderColor: 'var(--color-success-200)',
@@ -537,7 +537,7 @@ export function ActionQueueCard({
const displayedActions = showAll ? actionQueue.actions : actionQueue.actions.slice(0, 3);
return (
<div className="rounded-xl shadow-md p-6" style={{ backgroundColor: 'var(--bg-primary)' }}>
<div className="rounded-xl shadow-lg p-6 border" style={{ backgroundColor: 'var(--bg-primary)', borderColor: 'var(--border-primary)' }}>
{/* Header */}
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>{t('jtbd.action_queue.title')}</h2>

View File

@@ -70,7 +70,7 @@ export function OrchestrationSummaryCard({ summary, loading, onWorkflowComplete
if (loading || !summary) {
return (
<div className="rounded-xl shadow-md p-6" style={{ backgroundColor: 'var(--bg-primary)' }}>
<div className="rounded-xl shadow-lg p-6 border" style={{ backgroundColor: 'var(--bg-primary)', borderColor: 'var(--border-primary)' }}>
<div className="animate-pulse space-y-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full" style={{ backgroundColor: 'var(--bg-tertiary)' }}></div>
@@ -89,7 +89,7 @@ export function OrchestrationSummaryCard({ summary, loading, onWorkflowComplete
if (summary.status === 'no_runs') {
return (
<div
className="border-2 rounded-xl p-6"
className="border-2 rounded-xl p-6 shadow-lg"
style={{
backgroundColor: 'var(--surface-secondary)',
borderColor: 'var(--color-info-300)',
@@ -126,7 +126,7 @@ export function OrchestrationSummaryCard({ summary, loading, onWorkflowComplete
return (
<div
className="rounded-xl shadow-md p-6 border"
className="rounded-xl shadow-lg p-6 border"
style={{
background: 'linear-gradient(135deg, var(--bg-secondary) 0%, var(--bg-tertiary) 100%)',
borderColor: 'var(--border-primary)',

View File

@@ -213,7 +213,7 @@ export function ProductionTimelineCard({
if (loading || !filteredTimeline) {
return (
<div className="rounded-xl shadow-md p-6" style={{ backgroundColor: 'var(--bg-primary)' }}>
<div className="rounded-xl shadow-lg p-6 border" style={{ backgroundColor: 'var(--bg-primary)', borderColor: 'var(--border-primary)' }}>
<div className="animate-pulse space-y-4">
<div className="h-6 rounded w-1/2" style={{ backgroundColor: 'var(--bg-tertiary)' }}></div>
<div className="space-y-3">
@@ -227,7 +227,7 @@ export function ProductionTimelineCard({
if (!filteredTimeline.timeline || filteredTimeline.timeline.length === 0) {
return (
<div className="rounded-xl shadow-md p-8 text-center" style={{ backgroundColor: 'var(--bg-primary)' }}>
<div className="rounded-xl shadow-lg p-8 text-center border" style={{ backgroundColor: 'var(--bg-primary)', borderColor: 'var(--border-primary)' }}>
<Factory className="w-16 h-16 mx-auto mb-4" style={{ color: 'var(--text-tertiary)' }} />
<h3 className="text-xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}>
{t('jtbd.production_timeline.no_production')}
@@ -238,7 +238,7 @@ export function ProductionTimelineCard({
}
return (
<div className="rounded-xl shadow-md p-6" style={{ backgroundColor: 'var(--bg-primary)' }}>
<div className="rounded-xl shadow-lg p-6 border" style={{ backgroundColor: 'var(--bg-primary)', borderColor: 'var(--border-primary)' }}>
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">

View File

@@ -0,0 +1,265 @@
import React, { useState, useEffect, useMemo } from 'react';
import { Edit, Package, Calendar, Building2 } from 'lucide-react';
import { AddModal } from '../../ui/AddModal/AddModal';
import { useUpdatePurchaseOrder, usePurchaseOrder } from '../../../api/hooks/purchase-orders';
import { useTenantStore } from '../../../stores/tenant.store';
import type { PurchaseOrderItem } from '../../../api/types/orders';
import { statusColors } from '../../../styles/colors';
interface ModifyPurchaseOrderModalProps {
isOpen: boolean;
onClose: () => void;
poId: string;
onSuccess?: () => void;
}
/**
* ModifyPurchaseOrderModal - Modal for modifying existing purchase orders
* Allows editing of items, delivery dates, and notes for pending approval POs
*/
export const ModifyPurchaseOrderModal: React.FC<ModifyPurchaseOrderModalProps> = ({
isOpen,
onClose,
poId,
onSuccess
}) => {
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState<Record<string, any>>({});
// Get current tenant
const { currentTenant } = useTenantStore();
const tenantId = currentTenant?.id || '';
// Fetch the purchase order details
const { data: purchaseOrder, isLoading: isLoadingPO } = usePurchaseOrder(
tenantId,
poId,
{ enabled: !!tenantId && !!poId && isOpen }
);
// Update purchase order mutation
const updatePurchaseOrderMutation = useUpdatePurchaseOrder();
// Unit options for select field
const unitOptions = [
{ value: 'kg', label: 'Kilogramos' },
{ value: 'g', label: 'Gramos' },
{ value: 'l', label: 'Litros' },
{ value: 'ml', label: 'Mililitros' },
{ value: 'units', label: 'Unidades' },
{ value: 'boxes', label: 'Cajas' },
{ value: 'bags', label: 'Bolsas' }
];
// Priority options
const priorityOptions = [
{ value: 'urgent', label: 'Urgente' },
{ value: 'high', label: 'Alta' },
{ value: 'normal', label: 'Normal' },
{ value: 'low', label: 'Baja' }
];
// Reset form when modal closes
useEffect(() => {
if (!isOpen) {
setFormData({});
}
}, [isOpen]);
const handleSave = async (formData: Record<string, any>) => {
setLoading(true);
try {
const items = formData.items || [];
if (items.length === 0) {
throw new Error('Por favor, agrega al menos un producto');
}
// Validate quantities
const invalidQuantities = items.some((item: any) => item.ordered_quantity <= 0);
if (invalidQuantities) {
throw new Error('Todas las cantidades deben ser mayores a 0');
}
// Validate required fields
const invalidProducts = items.some((item: any) => !item.product_name);
if (invalidProducts) {
throw new Error('Todos los productos deben tener un nombre');
}
// Prepare the update data
const updateData: any = {
notes: formData.notes || undefined,
priority: formData.priority || undefined,
};
// Add delivery date if changed
if (formData.required_delivery_date) {
updateData.required_delivery_date = formData.required_delivery_date;
}
// Update purchase order
await updatePurchaseOrderMutation.mutateAsync({
tenantId,
poId,
data: updateData
});
// Trigger success callback
if (onSuccess) {
onSuccess();
}
} catch (error) {
console.error('Error modifying purchase order:', error);
throw error; // Let AddModal handle error display
} finally {
setLoading(false);
}
};
const statusConfig = {
color: statusColors.pending.primary,
text: 'Modificando Orden',
icon: Edit,
isCritical: false,
isHighlight: true
};
// Build sections dynamically based on purchase order data
const sections = useMemo(() => {
if (!purchaseOrder) return [];
const supplierSection = {
title: 'Información del Proveedor',
icon: Building2,
fields: [
{
label: 'Proveedor',
name: 'supplier_name',
type: 'text' as const,
required: false,
defaultValue: purchaseOrder.supplier_name || '',
span: 2,
disabled: true,
helpText: 'El proveedor no puede ser modificado'
}
]
};
const orderDetailsSection = {
title: 'Detalles de la Orden',
icon: Calendar,
fields: [
{
label: 'Prioridad',
name: 'priority',
type: 'select' as const,
options: priorityOptions,
defaultValue: purchaseOrder.priority || 'normal',
helpText: 'Ajusta la prioridad de esta orden'
},
{
label: 'Fecha de Entrega Requerida',
name: 'required_delivery_date',
type: 'date' as const,
defaultValue: purchaseOrder.required_delivery_date || '',
helpText: 'Fecha límite para la entrega'
},
{
label: 'Notas',
name: 'notes',
type: 'textarea' as const,
placeholder: 'Instrucciones especiales para el proveedor...',
span: 2,
defaultValue: purchaseOrder.notes || '',
helpText: 'Información adicional o instrucciones especiales'
}
]
};
const itemsSection = {
title: 'Productos de la Orden',
icon: Package,
fields: [
{
label: 'Productos',
name: 'items',
type: 'list' as const,
span: 2,
defaultValue: (purchaseOrder.items || []).map((item: PurchaseOrderItem) => ({
id: item.id,
inventory_product_id: item.inventory_product_id,
product_code: item.product_code || '',
product_name: item.product_name || '',
ordered_quantity: item.ordered_quantity,
unit_of_measure: item.unit_of_measure,
unit_price: parseFloat(item.unit_price),
})),
listConfig: {
itemFields: [
{
name: 'product_name',
label: 'Producto',
type: 'text',
required: true,
disabled: true
},
{
name: 'product_code',
label: 'SKU',
type: 'text',
required: false,
disabled: true
},
{
name: 'ordered_quantity',
label: 'Cantidad',
type: 'number',
required: true
},
{
name: 'unit_of_measure',
label: 'Unidad',
type: 'select',
required: true,
options: unitOptions
},
{
name: 'unit_price',
label: 'Precio Unitario (€)',
type: 'currency',
required: true,
placeholder: '0.00'
}
],
addButtonLabel: 'Agregar Producto',
emptyStateText: 'No hay productos en esta orden',
showSubtotals: true,
subtotalFields: { quantity: 'ordered_quantity', price: 'unit_price' },
disabled: false
},
helpText: 'Modifica las cantidades, unidades y precios según sea necesario'
}
]
};
return [supplierSection, orderDetailsSection, itemsSection];
}, [purchaseOrder, priorityOptions, unitOptions]);
return (
<AddModal
isOpen={isOpen}
onClose={onClose}
title="Modificar Orden de Compra"
subtitle={`Orden ${purchaseOrder?.po_number || ''}`}
statusIndicator={statusConfig}
sections={sections}
size="xl"
loading={loading || isLoadingPO}
onSave={handleSave}
/>
);
};
export default ModifyPurchaseOrderModal;

View File

@@ -197,13 +197,6 @@ export const ItemTypeSelector: React.FC<ItemTypeSelectorProps> = ({ onSelect })
);
})}
</div>
{/* Help Text */}
<div className="text-center pt-4 border-t border-[var(--border-primary)]">
<p className="text-sm text-[var(--text-tertiary)]">
{t('itemTypeSelector.helpText', { defaultValue: 'Select an option to start the guided step-by-step process' })}
</p>
</div>
</div>
);
};

View File

@@ -1,10 +1,12 @@
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
import { Users } from 'lucide-react';
import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection';
import Tooltip from '../../../ui/Tooltip/Tooltip';
const CustomerDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
const { t } = useTranslation('wizards');
const data = dataRef?.current || {};
const handleFieldChange = (field: string, value: any) => {
@@ -22,8 +24,8 @@ const CustomerDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange
<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">Customer Details</h3>
<p className="text-sm text-[var(--text-secondary)]">Essential customer information</p>
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">{t('customer.customerDetails')}</h3>
<p className="text-sm text-[var(--text-secondary)]">{t('customer.subtitle')}</p>
</div>
{/* Required Fields */}
@@ -31,21 +33,21 @@ const CustomerDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange
<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">
Customer Name *
{t('customer.fields.name')} *
</label>
<input
type="text"
value={data.name || ''}
onChange={(e) => handleFieldChange('name', e.target.value)}
placeholder="e.g., Restaurant El Molino"
placeholder={t('customer.fields.namePlaceholder')}
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 inline-flex items-center gap-2">
Customer Code *
<Tooltip content="Unique identifier for this customer. Auto-generated but editable.">
{t('customer.fields.customerCode')} *
<Tooltip content={t('customer.tooltips.customerCode')}>
<span />
</Tooltip>
</label>
@@ -53,7 +55,7 @@ const CustomerDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange
type="text"
value={data.customerCode}
onChange={(e) => handleFieldChange('customerCode', e.target.value)}
placeholder="CUST-001"
placeholder={t('customer.fields.customerCodePlaceholder')}
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>
@@ -62,28 +64,28 @@ const CustomerDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange
<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">
Customer Type *
{t('customer.fields.customerType')} *
</label>
<select
value={data.customerType}
onChange={(e) => handleFieldChange('customerType', 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="individual">Individual</option>
<option value="business">Business</option>
<option value="central_bakery">Central Bakery</option>
<option value="individual">{t('customer.customerTypes.individual')}</option>
<option value="business">{t('customer.customerTypes.business')}</option>
<option value="central_bakery">{t('customer.customerTypes.central_bakery')}</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Country *
{t('customer.fields.country')} *
</label>
<input
type="text"
value={data.country}
onChange={(e) => handleFieldChange('country', e.target.value)}
placeholder="US"
placeholder={t('customer.fields.countryPlaceholder')}
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>
@@ -91,13 +93,13 @@ const CustomerDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Business Name
{t('customer.fields.businessName')}
</label>
<input
type="text"
value={data.businessName}
onChange={(e) => handleFieldChange('businessName', e.target.value)}
placeholder="Legal business name"
placeholder={t('customer.fields.businessNamePlaceholder')}
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>
@@ -105,26 +107,26 @@ const CustomerDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange
<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">
Email
{t('customer.fields.email')}
</label>
<input
type="email"
value={data.email}
onChange={(e) => handleFieldChange('email', e.target.value)}
placeholder="contact@company.com"
placeholder={t('customer.fields.emailPlaceholder')}
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">
Phone
{t('customer.fields.phone')}
</label>
<input
type="tel"
value={data.phone}
onChange={(e) => handleFieldChange('phone', e.target.value)}
placeholder="+1 234 567 8900"
placeholder={t('customer.fields.phonePlaceholder')}
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>
@@ -133,126 +135,126 @@ const CustomerDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange
{/* Advanced Options */}
<AdvancedOptionsSection
title="Advanced Options"
description="Additional customer information and business terms"
title={t('customer.advancedOptionsTitle')}
description={t('customer.advancedOptionsDescription')}
>
<div className="space-y-4">
<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">
Address Line 1
{t('customer.fields.addressLine1')}
</label>
<input
type="text"
value={data.addressLine1}
onChange={(e) => handleFieldChange('addressLine1', e.target.value)}
placeholder="Street address"
placeholder={t('customer.fields.addressLine1Placeholder')}
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">
Address Line 2
{t('customer.fields.addressLine2')}
</label>
<input
type="text"
value={data.addressLine2}
onChange={(e) => handleFieldChange('addressLine2', e.target.value)}
placeholder="Apartment, suite, etc."
placeholder={t('customer.fields.addressLine2Placeholder')}
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">
City
{t('customer.fields.city')}
</label>
<input
type="text"
value={data.city}
onChange={(e) => handleFieldChange('city', e.target.value)}
placeholder="City"
placeholder={t('customer.fields.cityPlaceholder')}
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">
State/Province
{t('customer.fields.state')}
</label>
<input
type="text"
value={data.state}
onChange={(e) => handleFieldChange('state', e.target.value)}
placeholder="State"
placeholder={t('customer.fields.statePlaceholder')}
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">
Postal Code
{t('customer.fields.postalCode')}
</label>
<input
type="text"
value={data.postalCode}
onChange={(e) => handleFieldChange('postalCode', e.target.value)}
placeholder="12345"
placeholder={t('customer.fields.postalCodePlaceholder')}
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">
Tax ID
{t('customer.fields.taxId')}
</label>
<input
type="text"
value={data.taxId}
onChange={(e) => handleFieldChange('taxId', e.target.value)}
placeholder="Tax identification number"
placeholder={t('customer.fields.taxIdPlaceholder')}
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">
Business License
{t('customer.fields.businessLicense')}
</label>
<input
type="text"
value={data.businessLicense}
onChange={(e) => handleFieldChange('businessLicense', e.target.value)}
placeholder="Business license number"
placeholder={t('customer.fields.businessLicensePlaceholder')}
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">
Payment Terms
{t('customer.fields.paymentTerms')}
</label>
<select
value={data.paymentTerms}
onChange={(e) => handleFieldChange('paymentTerms', 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="immediate">Immediate</option>
<option value="net_30">Net 30</option>
<option value="net_60">Net 60</option>
<option value="immediate">{t('customer.paymentTerms.immediate')}</option>
<option value="net_30">{t('customer.paymentTerms.net_30')}</option>
<option value="net_60">{t('customer.paymentTerms.net_60')}</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Credit Limit
{t('customer.fields.creditLimit')}
</label>
<input
type="number"
value={data.creditLimit}
onChange={(e) => handleFieldChange('creditLimit', e.target.value)}
placeholder="5000.00"
placeholder={t('customer.fields.creditLimitPlaceholder')}
min="0"
step="0.01"
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)]"
@@ -261,13 +263,13 @@ const CustomerDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Discount Percentage
{t('customer.fields.discountPercentage')}
</label>
<input
type="number"
value={data.discountPercentage}
onChange={(e) => handleFieldChange('discountPercentage', parseFloat(e.target.value) || 0)}
placeholder="10"
placeholder={t('customer.fields.discountPercentagePlaceholder')}
min="0"
max="100"
step="0.01"
@@ -277,57 +279,58 @@ const CustomerDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Customer Segment
{t('customer.fields.customerSegment')}
</label>
<select
value={data.customerSegment}
onChange={(e) => handleFieldChange('customerSegment', 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="vip">VIP</option>
<option value="regular">Regular</option>
<option value="wholesale">Wholesale</option>
<option value="vip">{t('customer.segments.vip')}</option>
<option value="regular">{t('customer.segments.regular')}</option>
<option value="wholesale">{t('customer.segments.wholesale')}</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Priority Level
{t('customer.fields.priorityLevel')}
</label>
<select
value={data.priorityLevel}
onChange={(e) => handleFieldChange('priorityLevel', 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="high">High</option>
<option value="normal">Normal</option>
<option value="low">Low</option>
<option value="high">{t('customer.priorities.high')}</option>
<option value="normal">{t('customer.priorities.normal')}</option>
<option value="low">{t('customer.priorities.low')}</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Preferred Delivery Method
{t('customer.fields.preferredDeliveryMethod')}
</label>
<select
value={data.preferredDeliveryMethod}
onChange={(e) => handleFieldChange('preferredDeliveryMethod', 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="delivery">Delivery</option>
<option value="pickup">Pickup</option>
<option value="delivery">{t('customer.deliveryMethods.delivery')}</option>
<option value="pickup">{t('customer.deliveryMethods.pickup')}</option>
<option value="shipping">{t('customer.deliveryMethods.shipping')}</option>
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Special Instructions
{t('customer.fields.specialInstructions')}
</label>
<textarea
value={data.specialInstructions}
onChange={(e) => handleFieldChange('specialInstructions', e.target.value)}
placeholder="Any special notes or instructions for this customer..."
placeholder={t('customer.fields.specialInstructionsPlaceholder')}
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)]"
rows={3}
/>
@@ -347,20 +350,21 @@ export const CustomerWizardSteps = (
return [
{
id: 'customer-details',
title: 'Customer Details',
description: 'Contact and business information',
title: 'wizards:customer.steps.customerDetails',
description: 'wizards:customer.steps.customerDetailsDescription',
component: CustomerDetailsStep,
validate: async () => {
// Import these at the top level of this file would be better, but for now do it inline
const { useTenant } = await import('../../../../stores/tenant.store');
const OrdersService = (await import('../../../../api/services/orders')).default;
const { showToast } = await import('../../../../utils/toast');
const i18next = (await import('i18next')).default;
const data = dataRef.current;
const { currentTenant } = useTenant.getState();
if (!currentTenant?.id) {
showToast.error('Could not obtain tenant information');
showToast.error(i18next.t('wizards:customer.messages.errorObtainingTenantInfo'));
return false;
}
@@ -391,11 +395,11 @@ export const CustomerWizardSteps = (
};
await OrdersService.createCustomer(currentTenant.id, payload);
showToast.success('Customer created successfully');
showToast.success(i18next.t('wizards:customer.messages.customerCreatedSuccessfully'));
return true;
} catch (err: any) {
console.error('Error creating customer:', err);
const errorMessage = err.response?.data?.detail || 'Error creating customer';
const errorMessage = err.response?.data?.detail || i18next.t('wizards:customer.messages.errorCreatingCustomer');
showToast.error(errorMessage);
return false;
}

View File

@@ -1,8 +1,10 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
import { Wrench } from 'lucide-react';
const EquipmentDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
const { t } = useTranslation('wizards');
const data = dataRef?.current || {};
const handleFieldChange = (field: string, value: any) => {
@@ -13,46 +15,46 @@ const EquipmentDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange
<div className="space-y-6">
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
<Wrench 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">Equipo de Panadería</h3>
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">{t('equipment.equipmentDetails')}</h3>
</div>
<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">Tipo de Equipo *</label>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">{t('equipment.fields.type')} *</label>
<select
value={data.type || 'oven'}
onChange={(e) => handleFieldChange('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="oven">Horno</option>
<option value="mixer">Amasadora</option>
<option value="proofer">Fermentadora</option>
<option value="refrigerator">Refrigerador</option>
<option value="other">Otro</option>
<option value="oven">{t('equipment.types.oven')}</option>
<option value="mixer">{t('equipment.types.mixer')}</option>
<option value="proofer">{t('equipment.types.proofer')}</option>
<option value="refrigerator">{t('equipment.types.refrigerator')}</option>
<option value="other">{t('equipment.types.other')}</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Marca/Modelo</label>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">{t('equipment.fields.brand')}</label>
<input
type="text"
value={data.brand || ''}
onChange={(e) => handleFieldChange('brand', e.target.value)}
placeholder="Ej: Rational SCC 101"
placeholder={t('equipment.fields.brandPlaceholder')}
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">Ubicación</label>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">{t('equipment.fields.location')}</label>
<input
type="text"
value={data.location || ''}
onChange={(e) => handleFieldChange('location', e.target.value)}
placeholder="Ej: Cocina principal"
placeholder={t('equipment.fields.locationPlaceholder')}
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">Fecha de Compra</label>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">{t('equipment.fields.purchaseDate')}</label>
<input
type="date"
value={data.purchaseDate || ''}
@@ -71,25 +73,26 @@ export const EquipmentWizardSteps = (dataRef: React.MutableRefObject<Record<stri
return [
{
id: 'equipment-details',
title: 'Detalles del Equipo',
description: 'Tipo, modelo, ubicación',
title: 'wizards:equipment.steps.equipmentDetails',
description: 'wizards:equipment.steps.equipmentDetailsDescription',
component: EquipmentDetailsStep,
validate: async () => {
const { useTenant } = await import('../../../../stores/tenant.store');
const { equipmentService } = await import('../../../../api/services/equipment');
const { showToast } = await import('../../../../utils/toast');
const i18next = (await import('i18next')).default;
const data = dataRef.current;
const { currentTenant } = useTenant.getState();
if (!currentTenant?.id) {
showToast.error('No se pudo obtener información del tenant');
showToast.error(i18next.t('wizards:equipment.messages.errorGettingTenant'));
return false;
}
try {
const equipmentCreateData: any = {
name: `${data.type || 'oven'} - ${data.brand || 'Sin marca'}`,
name: `${data.type || 'oven'} - ${data.brand || i18next.t('wizards:equipment.messages.noBrand')}`,
type: data.type || 'oven',
model: data.brand || '',
serialNumber: data.model || '',
@@ -103,11 +106,11 @@ export const EquipmentWizardSteps = (dataRef: React.MutableRefObject<Record<stri
};
await equipmentService.createEquipment(currentTenant.id, equipmentCreateData);
showToast.success('Equipo creado exitosamente');
showToast.success(i18next.t('wizards:equipment.messages.successCreate'));
return true;
} catch (err: any) {
console.error('Error creating equipment:', err);
const errorMessage = err.response?.data?.detail || 'Error al crear el equipo';
const errorMessage = err.response?.data?.detail || i18next.t('wizards:equipment.messages.errorCreate');
showToast.error(errorMessage);
return false;
}

View File

@@ -368,19 +368,19 @@ const StockConfigStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) =
<div className="bg-gradient-to-br from-[var(--color-primary)]/5 to-[var(--color-primary)]/10 border border-[var(--color-primary)]/20 rounded-lg p-4">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<span className="text-[var(--text-tertiary)] block mb-1">Producto</span>
<span className="text-[var(--text-tertiary)] block mb-1">{t('inventory.stockConfig.product')}</span>
<span className="font-medium text-[var(--text-primary)]">{data.name || 'N/A'}</span>
</div>
<div>
<span className="text-[var(--text-tertiary)] block mb-1">Unidad</span>
<span className="text-[var(--text-tertiary)] block mb-1">{t('inventory.fields.unitOfMeasure')}</span>
<span className="font-medium text-[var(--text-primary)]">{data.unitOfMeasure || 'N/A'}</span>
</div>
<div>
<span className="text-[var(--text-tertiary)] block mb-1">Cantidad Total</span>
<span className="text-[var(--text-tertiary)] block mb-1">{t('inventory.stockConfig.totalQuantity')}</span>
<span className="font-medium text-[var(--color-primary)]">{totalQuantity.toFixed(2)}</span>
</div>
<div>
<span className="text-[var(--text-tertiary)] block mb-1">Valor Total</span>
<span className="text-[var(--text-tertiary)] block mb-1">{t('inventory.stockConfig.totalValue')}</span>
<span className="font-medium text-green-600">${totalValue.toFixed(2)}</span>
</div>
</div>
@@ -415,7 +415,7 @@ const StockConfigStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) =
<div className="space-y-4">
<h4 className="text-sm font-semibold text-[var(--text-primary)] flex items-center gap-2">
<ShoppingBag className="w-4 h-4" />
Lotes Registrados ({lots.length})
{t('inventory.stockConfig.lotsRegistered')} ({lots.length})
</h4>
{lots.map((lot, index) => (
<div
@@ -424,13 +424,13 @@ const StockConfigStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) =
>
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-semibold text-[var(--text-primary)]">
Lote #{index + 1}
{t('inventory.stockConfig.lot')} #{index + 1}
</span>
<button
onClick={() => handleRemoveLot(lot.id)}
className="text-red-500 hover:text-red-700 transition-colors p-1"
>
<span className="text-xs">Eliminar</span>
<span className="text-xs">{t('inventory.stockConfig.remove')}</span>
</button>
</div>
@@ -438,7 +438,7 @@ const StockConfigStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) =
{/* Quantity */}
<div>
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
Cantidad *
{t('inventory.stockConfig.quantity')} *
</label>
<input
type="number"
@@ -454,7 +454,7 @@ const StockConfigStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) =
{/* Unit Cost */}
<div>
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
Costo Unitario ($)
{t('inventory.stockConfig.unitCost')}
</label>
<input
type="number"
@@ -470,7 +470,7 @@ const StockConfigStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) =
{/* Lot Number */}
<div>
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
Número de Lote
{t('inventory.stockConfig.lotNumber')}
</label>
<input
type="text"
@@ -484,7 +484,7 @@ const StockConfigStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) =
{/* Expiration Date */}
<div>
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
Fecha de Expiración
{t('inventory.stockConfig.expirationDate')}
</label>
<input
type="date"
@@ -497,7 +497,7 @@ const StockConfigStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) =
{/* Location */}
<div className="md:col-span-2">
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
Ubicación
{t('inventory.stockConfig.location')}
</label>
<input
type="text"
@@ -512,7 +512,7 @@ const StockConfigStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) =
{/* Lot Total */}
{lot.quantity && lot.unitCost && (
<div className="text-xs text-[var(--text-tertiary)] pt-2 border-t border-[var(--border-secondary)]">
Valor del lote: <span className="font-semibold text-green-600">
{t('inventory.stockConfig.lotValue')} <span className="font-semibold text-green-600">
${(parseFloat(lot.quantity) * parseFloat(lot.unitCost)).toFixed(2)}
</span>
</div>
@@ -528,13 +528,13 @@ const StockConfigStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) =
className="w-full py-3 border-2 border-dashed border-[var(--color-primary)] text-[var(--color-primary)] rounded-lg hover:bg-[var(--color-primary)]/5 transition-colors font-medium inline-flex items-center justify-center gap-2"
>
<Package className="w-5 h-5" />
{lots.length === 0 ? 'Agregar Lote Inicial' : 'Agregar Otro Lote'}
{lots.length === 0 ? t('inventory.stockConfig.addInitialLot') : t('inventory.stockConfig.addAnotherLot')}
</button>
{/* Skip Option */}
{lots.length === 0 && (
<p className="text-xs text-center text-[var(--text-tertiary)] italic">
Puedes saltar este paso si prefieres agregar el stock inicial más tarde
{t('inventory.stockConfig.skipMessage')}
</p>
)}
</div>

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
import { ChefHat, Package, ClipboardCheck, CheckCircle2, Loader2, Plus, X, Search } from 'lucide-react';
import { useTenant } from '../../../../stores/tenant.store';
@@ -13,6 +14,7 @@ import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection';
import Tooltip from '../../../ui/Tooltip/Tooltip';
const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
const { t } = useTranslation('wizards');
const data = dataRef?.current || {};
const { currentTenant } = useTenant();
const [finishedProducts, setFinishedProducts] = useState<IngredientResponse[]>([]);
@@ -45,21 +47,21 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
<div className="space-y-6">
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
<ChefHat 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">Recipe Details</h3>
<p className="text-sm text-[var(--text-secondary)]">Essential information about your recipe</p>
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">{t('recipe.recipeDetails')}</h3>
<p className="text-sm text-[var(--text-secondary)]">{t('recipe.recipeDetailsDescription')}</p>
</div>
{/* Required Fields */}
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Recipe Name *
{t('recipe.fields.name')} *
</label>
<input
type="text"
value={data.name}
onChange={(e) => handleFieldChange('name', e.target.value)}
placeholder="e.g., Traditional Baguette"
placeholder={t('recipe.fields.namePlaceholder')}
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>
@@ -67,28 +69,28 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
<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">
Category *
{t('recipe.fields.category')} *
</label>
<select
value={data.category}
onChange={(e) => handleFieldChange('category', 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="bread">Bread</option>
<option value="pastries">Pastries</option>
<option value="cakes">Cakes</option>
<option value="cookies">Cookies</option>
<option value="muffins">Muffins</option>
<option value="sandwiches">Sandwiches</option>
<option value="seasonal">Seasonal</option>
<option value="other">Other</option>
<option value="bread">{t('recipe.categories.bread')}</option>
<option value="pastries">{t('recipe.categories.pastries')}</option>
<option value="cakes">{t('recipe.categories.cakes')}</option>
<option value="cookies">{t('recipe.categories.cookies')}</option>
<option value="muffins">{t('recipe.categories.muffins')}</option>
<option value="sandwiches">{t('recipe.categories.sandwiches')}</option>
<option value="seasonal">{t('recipe.categories.seasonal')}</option>
<option value="other">{t('recipe.categories.other')}</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2 inline-flex items-center gap-2">
Finished Product *
<Tooltip content="The final product this recipe produces. Must be created in inventory first.">
{t('recipe.fields.finishedProduct')} *
<Tooltip content={t('recipe.fields.finishedProductTooltip')}>
<span />
</Tooltip>
</label>
@@ -98,7 +100,7 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
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)]"
disabled={loading}
>
<option value="">Select product...</option>
<option value="">{t('recipe.fields.selectProduct')}</option>
{finishedProducts.map(product => (
<option key={product.id} value={product.id}>{product.name}</option>
))}
@@ -109,7 +111,7 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
<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">
Yield Quantity *
{t('recipe.fields.yieldQuantity')} *
</label>
<input
type="number"
@@ -124,26 +126,26 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Yield Unit *
{t('recipe.fields.yieldUnit')} *
</label>
<select
value={data.yieldUnit}
onChange={(e) => handleFieldChange('yieldUnit', 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="units">Units</option>
<option value="pieces">Pieces</option>
<option value="kg">Kilograms (kg)</option>
<option value="g">Grams (g)</option>
<option value="l">Liters (l)</option>
<option value="ml">Milliliters (ml)</option>
<option value="units">{t('recipe.units.units')}</option>
<option value="pieces">{t('recipe.units.pieces')}</option>
<option value="kg">{t('recipe.units.kg')}</option>
<option value="g">{t('recipe.units.g')}</option>
<option value="l">{t('recipe.units.l')}</option>
<option value="ml">{t('recipe.units.ml')}</option>
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Preparation Time (minutes)
{t('recipe.fields.prepTime')}
</label>
<input
type="number"
@@ -157,12 +159,12 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Instructions
{t('recipe.fields.instructions')}
</label>
<textarea
value={data.instructions}
onChange={(e) => handleFieldChange('instructions', e.target.value)}
placeholder="Step-by-step preparation instructions..."
placeholder={t('recipe.fields.instructionsPlaceholder')}
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)]"
rows={4}
/>
@@ -519,6 +521,7 @@ interface SelectedIngredient {
}
const IngredientsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
const { t } = useTranslation('wizards');
const data = dataRef?.current || {};
const { currentTenant } = useTenant();
const [ingredients, setIngredients] = useState<IngredientResponse[]>([]);
@@ -581,7 +584,7 @@ const IngredientsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) =
<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">Ingredients</h3>
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">{t('recipe.ingredients')}</h3>
<p className="text-sm text-[var(--text-secondary)]">{data.name}</p>
</div>
@@ -698,6 +701,7 @@ const IngredientsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) =
};
const QualityTemplatesStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onComplete }) => {
const { t } = useTranslation('wizards');
const data = dataRef?.current || {};
const { currentTenant } = useTenant();
const [templates, setTemplates] = useState<QualityCheckTemplateResponse[]>([]);
@@ -804,11 +808,11 @@ const QualityTemplatesStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange
};
await recipesService.createRecipe(currentTenant.id, recipeData);
showToast.success('Recipe created successfully');
showToast.success(t('recipe.messages.successCreate'));
onComplete();
} catch (err: any) {
console.error('Error creating recipe:', err);
const errorMessage = err.response?.data?.detail || 'Error creating recipe';
const errorMessage = err.response?.data?.detail || t('recipe.messages.errorCreate');
setError(errorMessage);
showToast.error(errorMessage);
} finally {
@@ -933,20 +937,20 @@ export const RecipeWizardSteps = (dataRef: React.MutableRefObject<Record<string,
return [
{
id: 'recipe-details',
title: 'Recipe Details',
description: 'Name, category, yield',
title: 'wizards:recipe.steps.recipeDetails',
description: 'wizards:recipe.steps.recipeDetailsDescription',
component: RecipeDetailsStep,
},
{
id: 'recipe-ingredients',
title: 'Ingredients',
description: 'Selection and quantities',
title: 'wizards:recipe.steps.ingredients',
description: 'wizards:recipe.steps.ingredientsDescription',
component: IngredientsStep,
},
{
id: 'recipe-quality-templates',
title: 'Quality Templates',
description: 'Applicable quality controls',
title: 'wizards:recipe.steps.qualityTemplates',
description: 'wizards:recipe.steps.qualityTemplatesDescription',
component: QualityTemplatesStep,
isOptional: true,
},

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
import {
Edit3,
@@ -12,6 +13,7 @@ import {
CreditCard,
Loader2,
X,
AlertCircle,
} from 'lucide-react';
import { useTenant } from '../../../../stores/tenant.store';
import { salesService } from '../../../../api/services/sales';
@@ -24,6 +26,7 @@ import { showToast } from '../../../../utils/toast';
const EntryMethodStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onNext }) => {
const data = dataRef?.current || {};
const { t } = useTranslation('wizards');
const [selectedMethod, setSelectedMethod] = useState<'manual' | 'upload'>(
data.entryMethod || 'manual'
);
@@ -40,10 +43,10 @@ const EntryMethodStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onN
<div className="space-y-6">
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
¿Cómo deseas registrar las ventas?
{t('salesEntry.entryMethod.title')}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
Elige el método que mejor se adapte a tus necesidades
{t('salesEntry.entryMethod.subtitle')}
</p>
</div>
@@ -77,23 +80,23 @@ const EntryMethodStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onN
</div>
<div className="flex-1">
<h4 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
Entrada Manual
{t('salesEntry.entryMethod.manual.title')}
</h4>
<p className="text-sm text-[var(--text-secondary)] mb-3">
Ingresa una o varias ventas de forma individual
{t('salesEntry.entryMethod.manual.description')}
</p>
<div className="space-y-1 text-xs text-[var(--text-tertiary)]">
<p className="flex items-center gap-1.5">
<CheckCircle2 className="w-3.5 h-3.5 text-green-600" />
Ideal para totales diarios
{t('salesEntry.entryMethod.manual.benefits.1')}
</p>
<p className="flex items-center gap-1.5">
<CheckCircle2 className="w-3.5 h-3.5 text-green-600" />
Control detallado por venta
{t('salesEntry.entryMethod.manual.benefits.2')}
</p>
<p className="flex items-center gap-1.5">
<CheckCircle2 className="w-3.5 h-3.5 text-green-600" />
Fácil y rápido
{t('salesEntry.entryMethod.manual.benefits.3')}
</p>
</div>
</div>
@@ -117,7 +120,7 @@ const EntryMethodStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onN
{/* Recommended Badge */}
<div className="absolute top-3 right-3">
<span className="px-2 py-1 text-xs rounded-full bg-gradient-to-r from-amber-100 to-orange-100 text-orange-800 font-semibold">
Recomendado para históricos
{t('salesEntry.entryMethod.file.recommended')}
</span>
</div>
@@ -136,23 +139,23 @@ const EntryMethodStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onN
</div>
<div className="flex-1">
<h4 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
Cargar Archivo
{t('salesEntry.entryMethod.file.title')}
</h4>
<p className="text-sm text-[var(--text-secondary)] mb-3">
Importa desde Excel o CSV
{t('salesEntry.entryMethod.file.description')}
</p>
<div className="space-y-1 text-xs text-[var(--text-tertiary)]">
<p className="flex items-center gap-1.5">
<CheckCircle2 className="w-3.5 h-3.5 text-green-600" />
Ideal para datos históricos
{t('salesEntry.entryMethod.file.benefits.1')}
</p>
<p className="flex items-center gap-1.5">
<CheckCircle2 className="w-3.5 h-3.5 text-green-600" />
Carga masiva (cientos de registros)
{t('salesEntry.entryMethod.file.benefits.2')}
</p>
<p className="flex items-center gap-1.5">
<CheckCircle2 className="w-3.5 h-3.5 text-green-600" />
Ahorra tiempo significativo
{t('salesEntry.entryMethod.file.benefits.3')}
</p>
</div>
</div>
@@ -169,6 +172,7 @@ const EntryMethodStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onN
const ManualEntryStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onNext }) => {
const data = dataRef?.current || {};
const { t } = useTranslation('wizards');
const { currentTenant } = useTenant();
const [products, setProducts] = useState<any[]>([]);
const [loadingProducts, setLoadingProducts] = useState(true);
@@ -189,7 +193,7 @@ const ManualEntryStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onN
setProducts(finishedProducts);
} catch (err: any) {
console.error('Error fetching products:', err);
setError('Error al cargar los productos');
setError(t('salesEntry.messages.errorLoadingProducts'));
} finally {
setLoadingProducts(false);
}
@@ -249,10 +253,10 @@ const ManualEntryStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onN
<div className="space-y-6">
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
Registrar Venta Manual
{t('salesEntry.manualEntry.title')}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
Ingresa los detalles de la venta
{t('salesEntry.manualEntry.subtitle')}
</p>
</div>
@@ -261,7 +265,7 @@ const ManualEntryStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onN
<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 Venta *
{t('salesEntry.manualEntry.fields.saleDate')} *
</label>
<input
type="date"
@@ -274,18 +278,18 @@ const ManualEntryStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onN
<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 *
{t('salesEntry.manualEntry.fields.paymentMethod')} *
</label>
<select
value={data.paymentMethod || 'cash'}
onChange={(e) => onDataChange?.({ ...data, 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="mobile">Pago Móvil</option>
<option value="transfer">Transferencia</option>
<option value="other">Otro</option>
<option value="cash">{t('salesEntry.paymentMethods.cash')}</option>
<option value="card">{t('salesEntry.paymentMethods.card')}</option>
<option value="mobile">{t('salesEntry.paymentMethods.mobile')}</option>
<option value="transfer">{t('salesEntry.paymentMethods.transfer')}</option>
<option value="other">{t('salesEntry.paymentMethods.other')}</option>
</select>
</div>
</div>
@@ -302,33 +306,33 @@ const ManualEntryStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onN
<div className="flex items-center justify-between">
<label className="block text-sm font-medium text-[var(--text-secondary)]">
<Package className="w-4 h-4 inline mr-1.5" />
Productos Vendidos
{t('salesEntry.manualEntry.products.title')}
</label>
<button
onClick={handleAddItem}
disabled={loadingProducts || products.length === 0}
disabled={loadingProducts}
className="px-3 py-1.5 text-sm bg-[var(--color-primary)] text-white rounded-md hover:bg-[var(--color-primary)]/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
+ Agregar Producto
{t('salesEntry.manualEntry.products.addProduct')}
</button>
</div>
{loadingProducts ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="w-6 h-6 animate-spin text-[var(--color-primary)]" />
<span className="ml-3 text-[var(--text-secondary)]">Cargando productos...</span>
<span className="ml-3 text-[var(--text-secondary)]">{t('salesEntry.manualEntry.products.loading')}</span>
</div>
) : products.length === 0 ? (
<div className="text-center py-8 border-2 border-dashed border-[var(--border-secondary)] rounded-lg text-[var(--text-tertiary)]">
<Package className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p>No hay productos terminados disponibles</p>
<p className="text-sm">Agrega productos al inventario primero</p>
<p>{t('salesEntry.manualEntry.products.noFinishedProducts')}</p>
<p className="text-sm">{t('salesEntry.manualEntry.products.addToInventory')}</p>
</div>
) : (data.salesItems || []).length === 0 ? (
<div className="text-center py-8 border-2 border-dashed border-[var(--border-secondary)] rounded-lg text-[var(--text-tertiary)]">
<Package className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p>No hay productos agregados</p>
<p className="text-sm">Haz clic en "Agregar Producto" para comenzar</p>
<p>{t('salesEntry.manualEntry.products.noProductsAdded')}</p>
<p className="text-sm">{t('salesEntry.manualEntry.products.clickToBegin')}</p>
</div>
) : (
<div className="space-y-2">
@@ -344,7 +348,7 @@ const ManualEntryStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onN
onChange={(e) => handleUpdateItem(index, 'productId', e.target.value)}
className="w-full px-2 py-1.5 text-sm border border-[var(--border-secondary)] rounded focus:outline-none focus:ring-1 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
>
<option value="">Seleccionar producto...</option>
<option value="">{t('salesEntry.manualEntry.products.selectProduct')}</option>
{products.map((product: any) => (
<option key={product.id} value={product.id}>
{product.name} - {(product.average_cost || product.last_purchase_price || 0).toFixed(2)}
@@ -355,7 +359,7 @@ const ManualEntryStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onN
<div className="col-span-4 sm:col-span-2">
<input
type="number"
placeholder="Cant."
placeholder={t('salesEntry.manualEntry.products.quantity')}
value={item.quantity}
onChange={(e) =>
handleUpdateItem(index, 'quantity', parseFloat(e.target.value) || 0)
@@ -368,7 +372,7 @@ const ManualEntryStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onN
<div className="col-span-4 sm:col-span-2">
<input
type="number"
placeholder="Precio"
placeholder={t('salesEntry.manualEntry.products.price')}
value={item.unitPrice}
onChange={(e) =>
handleUpdateItem(index, 'unitPrice', parseFloat(e.target.value) || 0)
@@ -399,7 +403,7 @@ const ManualEntryStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onN
{(data.salesItems || []).length > 0 && (
<div className="pt-3 border-t border-[var(--border-primary)] text-right">
<span className="text-lg font-bold text-[var(--text-primary)]">
Total: {calculateTotal().toFixed(2)}
{t('salesEntry.manualEntry.products.total')} {calculateTotal().toFixed(2)}
</span>
</div>
)}
@@ -408,12 +412,12 @@ const ManualEntryStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onN
{/* Notes */}
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Notas (Opcional)
{t('salesEntry.manualEntry.fields.notes')}
</label>
<textarea
value={data.notes || ''}
onChange={(e) => onDataChange?.({ ...data, notes: e.target.value })}
placeholder="Información adicional sobre esta venta..."
placeholder={t('salesEntry.manualEntry.fields.notesPlaceholder')}
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)] text-sm"
/>
@@ -428,6 +432,7 @@ const ManualEntryStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onN
const FileUploadStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onNext }) => {
const data = dataRef?.current || {};
const { t } = useTranslation('wizards');
const { currentTenant } = useTenant();
const [validating, setValidating] = useState(false);
const [importing, setImporting] = useState(false);
@@ -461,7 +466,7 @@ const FileUploadStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onNe
setValidationResult(result);
} catch (err: any) {
console.error('Error validating file:', err);
setError(err.response?.data?.detail || 'Error al validar el archivo');
setError(err.response?.data?.detail || t('salesEntry.messages.errorValidatingFile'));
} finally {
setValidating(false);
}
@@ -479,7 +484,7 @@ const FileUploadStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onNe
onNext?.();
} catch (err: any) {
console.error('Error importing file:', err);
setError(err.response?.data?.detail || 'Error al importar el archivo');
setError(err.response?.data?.detail || t('salesEntry.messages.errorImportingFile'));
} finally {
setImporting(false);
}
@@ -501,7 +506,7 @@ const FileUploadStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onNe
document.body.removeChild(a);
} catch (err: any) {
console.error('Error downloading template:', err);
setError('Error al descargar la plantilla');
setError(t('salesEntry.messages.errorValidatingFile'));
} finally {
setDownloadingTemplate(false);
}
@@ -511,10 +516,10 @@ const FileUploadStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onNe
<div className="space-y-6">
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
Cargar Archivo de Ventas
{t('salesEntry.fileUpload.title')}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
Importa tus ventas desde Excel o CSV
{t('salesEntry.fileUpload.subtitle')}
</p>
</div>
@@ -535,12 +540,12 @@ const FileUploadStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onNe
{downloadingTemplate ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Descargando...
{t('salesEntry.fileUpload.downloading')}
</>
) : (
<>
<Download className="w-4 h-4" />
Descargar Plantilla CSV
{t('salesEntry.fileUpload.downloadTemplate')}
</>
)}
</button>
@@ -551,10 +556,10 @@ const FileUploadStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onNe
<div className="border-2 border-dashed border-[var(--border-secondary)] rounded-xl p-8 text-center bg-[var(--bg-secondary)]/30">
<FileSpreadsheet className="w-16 h-16 mx-auto mb-4 text-[var(--color-primary)]/50" />
<h4 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
Arrastra un archivo aquí
{t('salesEntry.fileUpload.dragDrop.title')}
</h4>
<p className="text-sm text-[var(--text-secondary)] mb-4">
o haz clic para seleccionar
{t('salesEntry.fileUpload.dragDrop.subtitle')}
</p>
<label className="inline-block">
<input
@@ -564,11 +569,11 @@ const FileUploadStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onNe
className="hidden"
/>
<span className="px-6 py-2 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary)]/90 transition-colors cursor-pointer inline-block">
Seleccionar Archivo
{t('salesEntry.fileUpload.dragDrop.button')}
</span>
</label>
<p className="text-xs text-[var(--text-tertiary)] mt-3">
Formatos soportados: CSV, Excel (.xlsx, .xls)
{t('salesEntry.fileUpload.dragDrop.supportedFormats')}
</p>
</div>
) : (
@@ -595,14 +600,14 @@ const FileUploadStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onNe
{validationResult && (
<div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
<p className="text-sm text-blue-800 font-medium mb-2">
Archivo validado correctamente
{t('salesEntry.fileUpload.validated.title')}
</p>
<div className="text-xs text-blue-700 space-y-1">
<p>Registros encontrados: {validationResult.total_rows || 0}</p>
<p>Registros válidos: {validationResult.valid_rows || 0}</p>
<p>{t('salesEntry.fileUpload.validated.recordsFound')} {validationResult.total_rows || 0}</p>
<p>{t('salesEntry.fileUpload.validated.validRecords')} {validationResult.valid_rows || 0}</p>
{validationResult.errors?.length > 0 && (
<p className="text-red-600">
Errores: {validationResult.errors.length}
{t('salesEntry.fileUpload.validated.errors')} {validationResult.errors.length}
</p>
)}
</div>
@@ -620,12 +625,12 @@ const FileUploadStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onNe
{validating ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Validando...
{t('salesEntry.fileUpload.validating')}
</>
) : (
<>
<CheckCircle2 className="w-4 h-4" />
Validar Archivo
{t('salesEntry.fileUpload.validateButton')}
</>
)}
</button>
@@ -638,12 +643,12 @@ const FileUploadStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onNe
{importing ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Importando...
{t('salesEntry.fileUpload.importing')}
</>
) : (
<>
<Upload className="w-4 h-4" />
Importar Datos
{t('salesEntry.fileUpload.importButton')}
</>
)}
</button>
@@ -652,9 +657,9 @@ const FileUploadStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onNe
)}
<div className="text-center text-sm text-[var(--text-tertiary)]">
<p>El archivo debe contener las columnas:</p>
<p>{t('salesEntry.fileUpload.instructions.title')}</p>
<p className="font-mono text-xs mt-1">
fecha, producto, cantidad, precio_unitario, método_pago
{t('salesEntry.fileUpload.instructions.columns')}
</p>
</div>
</div>
@@ -667,6 +672,7 @@ const FileUploadStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onNe
const ReviewStep: React.FC<WizardStepProps> = ({ dataRef }) => {
const data = dataRef?.current || {};
const { t } = useTranslation('wizards');
const isManual = data.entryMethod === 'manual';
const isUpload = data.entryMethod === 'upload';
@@ -680,10 +686,10 @@ const ReviewStep: React.FC<WizardStepProps> = ({ dataRef }) => {
</div>
</div>
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
Revisar y Confirmar
{t('salesEntry.review.title')}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
Verifica que toda la información sea correcta
{t('salesEntry.review.subtitle')}
</p>
</div>
@@ -693,11 +699,11 @@ const ReviewStep: React.FC<WizardStepProps> = ({ dataRef }) => {
<div className="p-4 bg-[var(--bg-secondary)]/50 rounded-lg">
<div className="grid grid-cols-2 gap-3 text-sm">
<div>
<span className="text-[var(--text-secondary)]">Fecha:</span>
<span className="text-[var(--text-secondary)]">{t('salesEntry.review.fields.date')}</span>
<p className="font-semibold text-[var(--text-primary)]">{data.saleDate}</p>
</div>
<div>
<span className="text-[var(--text-secondary)]">Método de Pago:</span>
<span className="text-[var(--text-secondary)]">{t('salesEntry.review.fields.paymentMethod')}</span>
<p className="font-semibold text-[var(--text-primary)] capitalize">
{data.paymentMethod}
</p>
@@ -708,7 +714,7 @@ const ReviewStep: React.FC<WizardStepProps> = ({ dataRef }) => {
{/* Items */}
<div>
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-2">
Productos ({(data.salesItems || []).length})
{t('salesEntry.review.fields.products')} ({(data.salesItems || []).length})
</h4>
<div className="space-y-2">
{(data.salesItems || []).map((item: any) => (
@@ -735,7 +741,7 @@ const ReviewStep: React.FC<WizardStepProps> = ({ dataRef }) => {
{/* Total */}
<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:</span>
<span className="text-lg font-semibold text-[var(--text-primary)]">{t('salesEntry.review.fields.total')}</span>
<span className="text-2xl font-bold text-[var(--color-primary)]">
{data.totalAmount?.toFixed(2)}
</span>
@@ -745,7 +751,7 @@ const ReviewStep: React.FC<WizardStepProps> = ({ dataRef }) => {
{/* Notes */}
{data.notes && (
<div className="p-3 bg-[var(--bg-secondary)]/30 rounded-lg">
<p className="text-sm text-[var(--text-secondary)] mb-1">Notas:</p>
<p className="text-sm text-[var(--text-secondary)] mb-1">{t('salesEntry.review.fields.notes')}</p>
<p className="text-sm text-[var(--text-primary)]">{data.notes}</p>
</div>
)}
@@ -756,11 +762,11 @@ const ReviewStep: React.FC<WizardStepProps> = ({ dataRef }) => {
<div className="space-y-4">
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
<p className="text-green-800 font-semibold mb-2">
Archivo importado correctamente
{t('salesEntry.review.imported.title')}
</p>
<div className="text-sm text-green-700 space-y-1">
<p>Registros importados: {data.importResult.successful_imports || 0}</p>
<p>Registros fallidos: {data.importResult.failed_imports || 0}</p>
<p>{t('salesEntry.review.imported.recordsImported')} {data.importResult.successful_imports || 0}</p>
<p>{t('salesEntry.review.imported.recordsFailed')} {data.importResult.failed_imports || 0}</p>
</div>
</div>
</div>
@@ -784,8 +790,8 @@ export const SalesEntryWizardSteps = (
const steps: WizardStep[] = [
{
id: 'entry-method',
title: 'Método de Entrada',
description: 'Elige cómo registrar las ventas',
title: 'salesEntry.steps.entryMethod',
description: 'salesEntry.steps.entryMethodDescription',
component: EntryMethodStep,
},
];
@@ -793,8 +799,8 @@ export const SalesEntryWizardSteps = (
if (entryMethod === 'manual') {
steps.push({
id: 'manual-entry',
title: 'Ingresar Datos',
description: 'Registra los detalles de la venta',
title: 'salesEntry.steps.manualEntry',
description: 'salesEntry.steps.manualEntryDescription',
component: ManualEntryStep,
validate: () => {
const data = dataRef.current;
@@ -804,16 +810,16 @@ export const SalesEntryWizardSteps = (
} else if (entryMethod === 'upload') {
steps.push({
id: 'file-upload',
title: 'Cargar Archivo',
description: 'Importa ventas desde archivo',
title: 'salesEntry.steps.fileUpload',
description: 'salesEntry.steps.fileUploadDescription',
component: FileUploadStep,
});
}
steps.push({
id: 'review',
title: 'Revisar',
description: 'Confirma los datos antes de guardar',
title: 'salesEntry.steps.review',
description: 'salesEntry.steps.reviewDescription',
component: ReviewStep,
validate: async () => {
const { useTenant } = await import('../../../../stores/tenant.store');
@@ -824,6 +830,7 @@ export const SalesEntryWizardSteps = (
const { currentTenant } = useTenant.getState();
if (!currentTenant?.id) {
const { showToast } = await import('../../../../utils/toast');
showToast.error('No se pudo obtener información del tenant');
return false;
}
@@ -850,10 +857,12 @@ export const SalesEntryWizardSteps = (
}
}
const { showToast } = await import('../../../../utils/toast');
showToast.success('Registro de ventas guardado exitosamente');
return true;
} catch (err: any) {
console.error('Error saving sales data:', err);
const { showToast } = await import('../../../../utils/toast');
const errorMessage = err.response?.data?.detail || 'Error al guardar los datos de ventas';
showToast.error(errorMessage);
return false;

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
import { Building2, CheckCircle2, Loader2 } from 'lucide-react';
import { useTenant } from '../../../../stores/tenant.store';
@@ -8,6 +9,7 @@ import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection';
import Tooltip from '../../../ui/Tooltip/Tooltip';
const SupplierDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onComplete }) => {
const { t } = useTranslation('wizards');
// New architecture: access data from dataRef.current
const data = dataRef?.current || {};
const { currentTenant } = useTenant();
@@ -26,8 +28,11 @@ const SupplierDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange,
}, [data.name]);
const handleCreateSupplier = async () => {
const i18next = (await import('i18next')).default;
if (!currentTenant?.id) {
setError('Could not obtain tenant information');
const errorMsg = i18next.t('wizards:supplier.messages.errorObtainingTenantInfo');
setError(errorMsg);
return;
}
@@ -69,11 +74,11 @@ const SupplierDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange,
};
await suppliersService.createSupplier(currentTenant.id, payload);
showToast.success('Supplier created successfully');
showToast.success(i18next.t('wizards:supplier.messages.supplierCreatedSuccessfully'));
// Let the wizard handle completion via the Next/Complete button
} catch (err: any) {
console.error('Error creating supplier:', err);
const errorMessage = err.response?.data?.detail || 'Error creating supplier';
const errorMessage = err.response?.data?.detail || i18next.t('wizards:supplier.messages.errorCreatingSupplier');
setError(errorMessage);
showToast.error(errorMessage);
} finally {
@@ -85,8 +90,8 @@ const SupplierDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange,
<div className="space-y-6">
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
<Building2 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">Supplier Details</h3>
<p className="text-sm text-[var(--text-secondary)]">Essential supplier information</p>
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">{t('supplier.supplierDetails')}</h3>
<p className="text-sm text-[var(--text-secondary)]">{t('supplier.subtitle')}</p>
</div>
{error && (
@@ -100,21 +105,21 @@ const SupplierDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange,
<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">
Supplier Name *
{t('supplier.fields.name')} *
</label>
<input
type="text"
value={data.name}
onChange={(e) => handleFieldChange('name', e.target.value)}
placeholder="e.g., Premium Flour Suppliers Ltd."
placeholder={t('supplier.fields.namePlaceholder')}
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 inline-flex items-center gap-2">
Supplier Type *
<Tooltip content="Category of products/services this supplier provides">
{t('supplier.fields.supplierType')} *
<Tooltip content={t('supplier.fields.supplierTypeTooltip')}>
<span />
</Tooltip>
</label>
@@ -123,60 +128,60 @@ const SupplierDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange,
onChange={(e) => handleFieldChange('supplierType', 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="ingredients">Ingredients</option>
<option value="packaging">Packaging</option>
<option value="equipment">Equipment</option>
<option value="services">Services</option>
<option value="utilities">Utilities</option>
<option value="multi">Multi</option>
<option value="ingredients">{t('supplier.supplierTypes.ingredients')}</option>
<option value="packaging">{t('supplier.supplierTypes.packaging')}</option>
<option value="equipment">{t('supplier.supplierTypes.equipment')}</option>
<option value="services">{t('supplier.supplierTypes.services')}</option>
<option value="utilities">{t('supplier.supplierTypes.utilities')}</option>
<option value="multi">{t('supplier.supplierTypes.multi')}</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Status *
{t('supplier.fields.status')} *
</label>
<select
value={data.status}
onChange={(e) => handleFieldChange('status', 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="active">Active</option>
<option value="inactive">Inactive</option>
<option value="pending_approval">Pending Approval</option>
<option value="suspended">Suspended</option>
<option value="blacklisted">Blacklisted</option>
<option value="active">{t('supplier.statuses.active')}</option>
<option value="inactive">{t('supplier.statuses.inactive')}</option>
<option value="pending_approval">{t('supplier.statuses.pending_approval')}</option>
<option value="suspended">{t('supplier.statuses.suspended')}</option>
<option value="blacklisted">{t('supplier.statuses.blacklisted')}</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Payment Terms *
{t('supplier.fields.paymentTerms')} *
</label>
<select
value={data.paymentTerms}
onChange={(e) => handleFieldChange('paymentTerms', 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="cod">COD (Cash on Delivery)</option>
<option value="net_15">Net 15</option>
<option value="net_30">Net 30</option>
<option value="net_45">Net 45</option>
<option value="net_60">Net 60</option>
<option value="prepaid">Prepaid</option>
<option value="credit_terms">Credit Terms</option>
<option value="cod">{t('supplier.paymentTerms.cod')}</option>
<option value="net_15">{t('supplier.paymentTerms.net_15')}</option>
<option value="net_30">{t('supplier.paymentTerms.net_30')}</option>
<option value="net_45">{t('supplier.paymentTerms.net_45')}</option>
<option value="net_60">{t('supplier.paymentTerms.net_60')}</option>
<option value="prepaid">{t('supplier.paymentTerms.prepaid')}</option>
<option value="credit_terms">{t('supplier.paymentTerms.credit_terms')}</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Currency *
{t('supplier.fields.currency')} *
</label>
<input
type="text"
value={data.currency}
onChange={(e) => handleFieldChange('currency', e.target.value)}
placeholder="EUR"
placeholder={t('supplier.fields.currencyPlaceholder')}
maxLength={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)]"
/>
@@ -184,8 +189,8 @@ const SupplierDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange,
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2 inline-flex items-center gap-2">
Standard Lead Time (days) *
<Tooltip content="Typical delivery time from order to delivery">
{t('supplier.fields.leadTime')} *
<Tooltip content={t('supplier.fields.leadTimeTooltip')}>
<span />
</Tooltip>
</label>
@@ -203,39 +208,39 @@ const SupplierDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange,
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Contact Person
{t('supplier.fields.contactPerson')}
</label>
<input
type="text"
value={data.contactPerson}
onChange={(e) => handleFieldChange('contactPerson', e.target.value)}
placeholder="John Doe"
placeholder={t('supplier.fields.contactPersonPlaceholder')}
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">
Email
{t('supplier.fields.email')}
</label>
<input
type="email"
value={data.email}
onChange={(e) => handleFieldChange('email', e.target.value)}
placeholder="contact@supplier.com"
placeholder={t('supplier.fields.emailPlaceholder')}
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">
Phone
{t('supplier.fields.phone')}
</label>
<input
type="tel"
value={data.phone}
onChange={(e) => handleFieldChange('phone', e.target.value)}
placeholder="+1 234 567 8900"
placeholder={t('supplier.fields.phonePlaceholder')}
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>
@@ -244,163 +249,163 @@ const SupplierDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange,
{/* Advanced Options */}
<AdvancedOptionsSection
title="Advanced Options"
description="Additional supplier information and business details"
title={t('supplier.advancedOptionsTitle')}
description={t('supplier.advancedOptionsDescription')}
>
<div className="space-y-4">
<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">
Supplier Code
{t('supplier.fields.supplierCode')}
</label>
<input
type="text"
value={data.supplierCode}
onChange={(e) => handleFieldChange('supplierCode', e.target.value)}
placeholder="SUP-001"
placeholder={t('supplier.fields.supplierCodePlaceholder')}
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">
Mobile
{t('supplier.fields.mobile')}
</label>
<input
type="tel"
value={data.mobile}
onChange={(e) => handleFieldChange('mobile', e.target.value)}
placeholder="+1 234 567 8900"
placeholder={t('supplier.fields.mobilePlaceholder')}
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">
Tax ID
{t('supplier.fields.taxId')}
</label>
<input
type="text"
value={data.taxId}
onChange={(e) => handleFieldChange('taxId', e.target.value)}
placeholder="VAT/Tax ID"
placeholder={t('supplier.fields.taxIdPlaceholder')}
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">
Registration Number
{t('supplier.fields.registrationNumber')}
</label>
<input
type="text"
value={data.registrationNumber}
onChange={(e) => handleFieldChange('registrationNumber', e.target.value)}
placeholder="Business registration number"
placeholder={t('supplier.fields.registrationNumberPlaceholder')}
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">
Website
{t('supplier.fields.website')}
</label>
<input
type="url"
value={data.website}
onChange={(e) => handleFieldChange('website', e.target.value)}
placeholder="https://www.supplier.com"
placeholder={t('supplier.fields.websitePlaceholder')}
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">
Address Line 1
{t('supplier.fields.addressLine1')}
</label>
<input
type="text"
value={data.addressLine1}
onChange={(e) => handleFieldChange('addressLine1', e.target.value)}
placeholder="Street address"
placeholder={t('supplier.fields.addressLine1Placeholder')}
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">
Address Line 2
{t('supplier.fields.addressLine2')}
</label>
<input
type="text"
value={data.addressLine2}
onChange={(e) => handleFieldChange('addressLine2', e.target.value)}
placeholder="Suite, building, etc."
placeholder={t('supplier.fields.addressLine2Placeholder')}
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">
City
{t('supplier.fields.city')}
</label>
<input
type="text"
value={data.city}
onChange={(e) => handleFieldChange('city', e.target.value)}
placeholder="City"
placeholder={t('supplier.fields.cityPlaceholder')}
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">
State/Province
{t('supplier.fields.state')}
</label>
<input
type="text"
value={data.stateProvince}
onChange={(e) => handleFieldChange('stateProvince', e.target.value)}
placeholder="State"
placeholder={t('supplier.fields.statePlaceholder')}
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">
Postal Code
{t('supplier.fields.postalCode')}
</label>
<input
type="text"
value={data.postalCode}
onChange={(e) => handleFieldChange('postalCode', e.target.value)}
placeholder="12345"
placeholder={t('supplier.fields.postalCodePlaceholder')}
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">
Country
{t('supplier.fields.country')}
</label>
<input
type="text"
value={data.country}
onChange={(e) => handleFieldChange('country', e.target.value)}
placeholder="Country"
placeholder={t('supplier.fields.countryPlaceholder')}
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">
Credit Limit
{t('supplier.fields.creditLimit')}
</label>
<input
type="number"
value={data.creditLimit}
onChange={(e) => handleFieldChange('creditLimit', e.target.value)}
placeholder="10000.00"
placeholder={t('supplier.fields.creditLimitPlaceholder')}
min="0"
step="0.01"
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)]"
@@ -409,13 +414,13 @@ const SupplierDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange,
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Minimum Order Amount
{t('supplier.fields.minOrderAmount')}
</label>
<input
type="number"
value={data.minimumOrderAmount}
onChange={(e) => handleFieldChange('minimumOrderAmount', e.target.value)}
placeholder="100.00"
placeholder={t('supplier.fields.minOrderAmountPlaceholder')}
min="0"
step="0.01"
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)]"
@@ -424,13 +429,13 @@ const SupplierDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange,
<div className="md:col-span-2">
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Delivery Area
{t('supplier.fields.deliveryArea')}
</label>
<input
type="text"
value={data.deliveryArea}
onChange={(e) => handleFieldChange('deliveryArea', e.target.value)}
placeholder="e.g., New York Metro Area"
placeholder={t('supplier.fields.deliveryAreaPlaceholder')}
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>
@@ -446,7 +451,7 @@ const SupplierDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange,
className="w-4 h-4 text-[var(--color-primary)] border-[var(--border-secondary)] rounded focus:ring-2 focus:ring-[var(--color-primary)]"
/>
<label htmlFor="isPreferredSupplier" className="text-sm font-medium text-[var(--text-secondary)]">
Preferred Supplier
{t('supplier.fields.preferredSupplier')}
</label>
</div>
@@ -459,45 +464,45 @@ const SupplierDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange,
className="w-4 h-4 text-[var(--color-primary)] border-[var(--border-secondary)] rounded focus:ring-2 focus:ring-[var(--color-primary)]"
/>
<label htmlFor="autoApproveEnabled" className="text-sm font-medium text-[var(--text-secondary)]">
Auto-approve Orders
{t('supplier.fields.autoApproveOrders')}
</label>
</div>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Certifications
{t('supplier.fields.certifications')}
</label>
<input
type="text"
value={data.certifications}
onChange={(e) => handleFieldChange('certifications', e.target.value)}
placeholder="e.g., ISO 9001, HACCP, Organic (comma-separated)"
placeholder={t('supplier.fields.certificationsPlaceholder')}
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">
Specializations
{t('supplier.fields.specializations')}
</label>
<input
type="text"
value={data.specializations}
onChange={(e) => handleFieldChange('specializations', e.target.value)}
placeholder="e.g., Organic flours, Gluten-free products (comma-separated)"
placeholder={t('supplier.fields.specializationsPlaceholder')}
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">
Notes
{t('supplier.fields.notes')}
</label>
<textarea
value={data.notes}
onChange={(e) => handleFieldChange('notes', e.target.value)}
placeholder="Additional notes about this supplier..."
placeholder={t('supplier.fields.notesPlaceholder')}
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)]"
rows={3}
/>
@@ -518,8 +523,8 @@ export const SupplierWizardSteps = (
return [
{
id: 'supplier-details',
title: 'Supplier Details',
description: 'Essential supplier information',
title: 'wizards:supplier.steps.supplierDetails',
description: 'wizards:supplier.steps.supplierDetailsDescription',
component: SupplierDetailsStep,
},
];

View File

@@ -1,8 +1,10 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
import { UserPlus, Shield, Mail, Phone } from 'lucide-react';
const MemberDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
const { t } = useTranslation('wizards');
const data = dataRef?.current || {};
const handleFieldChange = (field: string, value: any) => {
onDataChange?.({ ...data, [field]: value });
@@ -12,69 +14,69 @@ const MemberDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
<div className="space-y-6">
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
<UserPlus 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">Miembro del Equipo</h3>
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">{t('teamMember.memberDetails')}</h3>
</div>
<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 Completo *</label>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">{t('teamMember.fields.fullName')} *</label>
<input
type="text"
value={data.fullName || ''}
onChange={(e) => handleFieldChange('fullName', e.target.value)}
placeholder="Ej: Juan García"
placeholder={t('teamMember.fields.fullNamePlaceholder')}
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">
<Mail className="w-3.5 h-3.5 inline mr-1" />
Email *
{t('teamMember.fields.email')} *
</label>
<input
type="email"
value={data.email || ''}
onChange={(e) => handleFieldChange('email', e.target.value)}
placeholder="juan@panaderia.com"
placeholder={t('teamMember.fields.emailPlaceholder')}
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">
<Phone className="w-3.5 h-3.5 inline mr-1" />
Teléfono
{t('teamMember.fields.phone')}
</label>
<input
type="tel"
value={data.phone || ''}
onChange={(e) => handleFieldChange('phone', e.target.value)}
placeholder="+34 123 456 789"
placeholder={t('teamMember.fields.phonePlaceholder')}
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">Posición *</label>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">{t('teamMember.fields.position')} *</label>
<select
value={data.position || 'baker'}
onChange={(e) => handleFieldChange('position', 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="baker">Panadero</option>
<option value="pastry-chef">Pastelero</option>
<option value="manager">Gerente</option>
<option value="sales">Ventas</option>
<option value="delivery">Repartidor</option>
<option value="baker">{t('teamMember.positions.baker')}</option>
<option value="pastry-chef">{t('teamMember.positions.pastryChef')}</option>
<option value="manager">{t('teamMember.positions.manager')}</option>
<option value="sales">{t('teamMember.positions.sales')}</option>
<option value="delivery">{t('teamMember.positions.delivery')}</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Tipo de Empleo</label>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">{t('teamMember.fields.employmentType')}</label>
<select
value={data.employmentType || 'full-time'}
onChange={(e) => handleFieldChange('employmentType', 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="full-time">Tiempo Completo</option>
<option value="part-time">Medio Tiempo</option>
<option value="contractor">Contratista</option>
<option value="full-time">{t('teamMember.employmentTypes.fullTime')}</option>
<option value="part-time">{t('teamMember.employmentTypes.partTime')}</option>
<option value="contractor">{t('teamMember.employmentTypes.contractor')}</option>
</select>
</div>
</div>
@@ -83,6 +85,7 @@ const MemberDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
};
const PermissionsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
const { t } = useTranslation('wizards');
const data = dataRef?.current || {};
const handleFieldChange = (field: string, value: any) => {
@@ -93,31 +96,31 @@ const PermissionsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) =
<div className="space-y-6">
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
<Shield 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">Rol y Permisos</h3>
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">{t('teamMember.roleAndPermissions')}</h3>
<p className="text-sm text-[var(--text-secondary)]">{data.fullName}</p>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-3">Rol del Sistema</label>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-3">{t('teamMember.fields.systemRole')}</label>
<select
value={data.role}
onChange={(e) => handleFieldChange('role', 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="admin">Administrador</option>
<option value="manager">Gerente</option>
<option value="staff">Personal</option>
<option value="view-only">Solo Lectura</option>
<option value="admin">{t('teamMember.roles.admin')}</option>
<option value="manager">{t('teamMember.roles.manager')}</option>
<option value="staff">{t('teamMember.roles.staff')}</option>
<option value="view-only">{t('teamMember.roles.viewOnly')}</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-3">Permisos Específicos</label>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-3">{t('teamMember.specificPermissions')}</label>
<div className="space-y-2">
{[
{ key: 'canManageInventory', label: 'Gestionar Inventario' },
{ key: 'canViewRecipes', label: 'Ver Recetas' },
{ key: 'canCreateOrders', label: 'Crear Pedidos' },
{ key: 'canViewFinancial', label: 'Ver Datos Financieros' },
{ key: 'canManageInventory', label: t('teamMember.permissions.canManageInventory') },
{ key: 'canViewRecipes', label: t('teamMember.permissions.canViewRecipes') },
{ key: 'canCreateOrders', label: t('teamMember.permissions.canCreateOrders') },
{ key: 'canViewFinancial', label: t('teamMember.permissions.canViewFinancial') },
].map(({ key, label }) => (
<label
key={key}
@@ -144,25 +147,26 @@ export const TeamMemberWizardSteps = (dataRef: React.MutableRefObject<Record<str
return [
{
id: 'member-details',
title: 'Datos Personales',
description: 'Nombre, contacto, posición',
title: 'wizards:teamMember.steps.memberDetails',
description: 'wizards:teamMember.steps.memberDetailsDescription',
component: MemberDetailsStep,
},
{
id: 'member-permissions',
title: 'Rol y Permisos',
description: 'Accesos al sistema',
title: 'wizards:teamMember.steps.roleAndPermissions',
description: 'wizards:teamMember.steps.roleAndPermissionsDescription',
component: PermissionsStep,
validate: async () => {
const { useTenant } = await import('../../../../stores/tenant.store');
const { authService } = await import('../../../../api/services/auth');
const { showToast } = await import('../../../../utils/toast');
const i18next = (await import('i18next')).default;
const data = dataRef.current;
const { currentTenant } = useTenant.getState();
if (!currentTenant?.id) {
showToast.error('No se pudo obtener información del tenant');
showToast.error(i18next.t('wizards:teamMember.messages.errorGettingTenant'));
return false;
}
@@ -187,11 +191,11 @@ export const TeamMemberWizardSteps = (dataRef: React.MutableRefObject<Record<str
// 2. Store permissions in a separate permissions table
// 3. Link user to tenant with specific role
showToast.success('Miembro del equipo agregado exitosamente');
showToast.success(i18next.t('wizards:teamMember.messages.successCreate'));
return true;
} catch (err: any) {
console.error('Error creating team member:', err);
const errorMessage = err.response?.data?.detail || 'Error al crear el miembro del equipo';
const errorMessage = err.response?.data?.detail || i18next.t('wizards:teamMember.messages.errorCreate');
showToast.error(errorMessage);
return false;
}

View File

@@ -8,36 +8,24 @@ export const PricingSection: React.FC = () => {
const { t } = useTranslation();
return (
<section id="pricing" className="py-24 bg-[var(--bg-primary)]">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="text-center mb-8">
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)]">
{t('landing:pricing.title', 'Planes que se Adaptan a tu Negocio')}
</h2>
<p className="mt-4 max-w-2xl mx-auto text-lg text-[var(--text-secondary)]">
{t('landing:pricing.subtitle', 'Sin costos ocultos, sin compromisos largos. Comienza gratis y escala según crezcas.')}
</p>
</div>
<div>
{/* Pricing Cards */}
<SubscriptionPricingCards
mode="landing"
showPilotBanner={true}
pilotTrialMonths={3}
/>
{/* Pricing Cards */}
<SubscriptionPricingCards
mode="landing"
showPilotBanner={true}
pilotTrialMonths={3}
/>
{/* Feature Comparison Link */}
<div className="text-center mt-12">
<Link
to="/plans/compare"
className="text-[var(--color-primary)] hover:text-[var(--color-primary-dark)] font-semibold inline-flex items-center gap-2"
>
{t('landing:pricing.compare_link', 'Ver comparación completa de características')}
<ArrowRight className="w-4 h-4" />
</Link>
</div>
{/* Feature Comparison Link */}
<div className="text-center mt-12">
<Link
to="/plans/compare"
className="text-[var(--color-primary)] hover:text-[var(--color-primary-dark)] font-semibold inline-flex items-center gap-2"
>
{t('landing:pricing.compare_link', 'Ver comparación completa de características')}
<ArrowRight className="w-4 h-4" />
</Link>
</div>
</section>
</div>
);
};