Improve backend
This commit is contained in:
@@ -147,12 +147,10 @@ export interface MLInsightsSettings {
|
||||
}
|
||||
|
||||
export interface NotificationSettings {
|
||||
// WhatsApp Configuration
|
||||
// WhatsApp Configuration (Shared Account Model)
|
||||
whatsapp_enabled: boolean;
|
||||
whatsapp_phone_number_id: string;
|
||||
whatsapp_access_token: string;
|
||||
whatsapp_business_account_id: string;
|
||||
whatsapp_api_version: string;
|
||||
whatsapp_display_phone_number: string;
|
||||
whatsapp_default_language: string;
|
||||
|
||||
// Email Configuration
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
{
|
||||
"hero": {
|
||||
"pre_headline": "For Bakeries Losing €500-2,000/Month on Waste",
|
||||
"pre_headline": "For Bakeries Losing Money on Waste",
|
||||
"scarcity": "Only 12 spots left out of 20 • 3 months FREE",
|
||||
"scarcity_badge": "🔥 Only 12 spots left out of 20 in pilot program",
|
||||
"badge": "Advanced AI for Modern Bakeries",
|
||||
"title_line1": "Increase Profits,",
|
||||
"title_line2": "Reduce Waste",
|
||||
"title_option_a_line1": "Save €500-2,000 Per Month",
|
||||
"title_option_a_line2": "By Producing Exactly What You'll Sell",
|
||||
"title_option_a_line1": "Produce Exactly What You'll Sell",
|
||||
"title_option_a_line2": "and Save Thousands",
|
||||
"title_option_b": "Stop Guessing How Much to Bake Every Day",
|
||||
"subtitle": "AI that predicts demand using local data so you produce exactly what you'll sell. Reduce waste, improve margins, save time.",
|
||||
"subtitle_option_a": "The first AI that knows your neighborhood: nearby schools, local weather, your competition, events. Automatic system every morning. Ready at 6 AM.",
|
||||
"subtitle_option_a": "AI that knows your neighborhood. Predictions ready every morning at 6 AM.",
|
||||
"subtitle_option_b": "AI that knows your area predicts sales with 92% accuracy. Wake up with your plan ready: what to make, what to order, when it arrives. Save €500-2,000/month on waste.",
|
||||
"cta_primary": "Join Pilot Program",
|
||||
"cta_secondary": "See How It Works (2 min)",
|
||||
"cta_demo": "See Demo",
|
||||
"social_proof": {
|
||||
"bakeries": "20 bakeries already saving €1,500/month on average",
|
||||
"accuracy": "92% accurate predictions (vs 60% generic systems)",
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
},
|
||||
"actions": {
|
||||
"approve": "Approve",
|
||||
"reject": "Reject",
|
||||
"view_details": "View Details",
|
||||
"modify": "Modify",
|
||||
"dismiss": "Dismiss",
|
||||
|
||||
@@ -16,7 +16,26 @@
|
||||
"steps": {
|
||||
"productType": "Product Type",
|
||||
"basicInfo": "Basic Information",
|
||||
"stockConfig": "Stock Configuration"
|
||||
"stockConfig": "Stock Configuration",
|
||||
"initialStock": "Initial Stock"
|
||||
},
|
||||
"initialStockDescription": "Add one or more lots to register the initial inventory",
|
||||
"stockConfig": {
|
||||
"product": "Product",
|
||||
"totalQuantity": "Total Quantity",
|
||||
"totalValue": "Total Value",
|
||||
"lotsRegistered": "Lots Registered",
|
||||
"lot": "Lot",
|
||||
"remove": "Remove",
|
||||
"quantity": "Quantity",
|
||||
"unitCost": "Unit Cost ($)",
|
||||
"lotNumber": "Lot Number",
|
||||
"expirationDate": "Expiration Date",
|
||||
"location": "Location",
|
||||
"lotValue": "Lot value:",
|
||||
"addInitialLot": "Add Initial Lot",
|
||||
"addAnotherLot": "Add Another Lot",
|
||||
"skipMessage": "You can skip this step if you prefer to add the initial stock later"
|
||||
},
|
||||
"typeDescriptions": {
|
||||
"ingredient": "Raw materials and ingredients used in recipes",
|
||||
@@ -499,6 +518,125 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"salesEntry": {
|
||||
"title": "Sales Record",
|
||||
"steps": {
|
||||
"entryMethod": "Entry Method",
|
||||
"entryMethodDescription": "Choose how to register sales",
|
||||
"manualEntry": "Enter Data",
|
||||
"manualEntryDescription": "Record sale details",
|
||||
"fileUpload": "Upload File",
|
||||
"fileUploadDescription": "Import sales from file",
|
||||
"review": "Review",
|
||||
"reviewDescription": "Confirm data before saving"
|
||||
},
|
||||
"entryMethod": {
|
||||
"title": "How do you want to register sales?",
|
||||
"subtitle": "Choose the method that best suits your needs",
|
||||
"manual": {
|
||||
"title": "Manual Entry",
|
||||
"description": "Enter one or more sales individually",
|
||||
"benefits": {
|
||||
"1": "Ideal for daily totals",
|
||||
"2": "Detailed control per sale",
|
||||
"3": "Easy and fast"
|
||||
}
|
||||
},
|
||||
"file": {
|
||||
"title": "Upload File",
|
||||
"description": "Import from Excel or CSV",
|
||||
"recommended": "⭐ Recommended for historical data",
|
||||
"benefits": {
|
||||
"1": "Ideal for historical data",
|
||||
"2": "Bulk upload (hundreds of records)",
|
||||
"3": "Saves significant time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"manualEntry": {
|
||||
"title": "Record Manual Sale",
|
||||
"subtitle": "Enter sale details",
|
||||
"fields": {
|
||||
"saleDate": "Sale Date",
|
||||
"paymentMethod": "Payment Method",
|
||||
"notes": "Notes (Optional)",
|
||||
"notesPlaceholder": "Additional information about this sale..."
|
||||
},
|
||||
"products": {
|
||||
"title": "Products Sold",
|
||||
"addProduct": "+ Add Product",
|
||||
"loading": "Loading products...",
|
||||
"noFinishedProducts": "No finished products available",
|
||||
"addToInventory": "Add products to inventory first",
|
||||
"noProductsAdded": "No products added",
|
||||
"clickToBegin": "Click 'Add Product' to begin",
|
||||
"selectProduct": "Select product...",
|
||||
"quantity": "Qty.",
|
||||
"price": "Price",
|
||||
"removeProduct": "Remove product",
|
||||
"total": "Total:"
|
||||
}
|
||||
},
|
||||
"fileUpload": {
|
||||
"title": "Upload Sales File",
|
||||
"subtitle": "Import your sales from Excel or CSV",
|
||||
"downloadTemplate": "Download CSV Template",
|
||||
"downloading": "Downloading...",
|
||||
"dragDrop": {
|
||||
"title": "Drag a file here",
|
||||
"subtitle": "or click to select",
|
||||
"button": "Select File",
|
||||
"supportedFormats": "Supported formats: CSV, Excel (.xlsx, .xls)"
|
||||
},
|
||||
"validated": {
|
||||
"title": "✓ File validated successfully",
|
||||
"recordsFound": "Records found:",
|
||||
"validRecords": "Valid records:",
|
||||
"errors": "Errors:"
|
||||
},
|
||||
"validateButton": "Validate File",
|
||||
"validating": "Validating...",
|
||||
"importButton": "Import Data",
|
||||
"importing": "Importing...",
|
||||
"instructions": {
|
||||
"title": "The file must contain the columns:",
|
||||
"columns": "date, product, quantity, unit_price, payment_method"
|
||||
}
|
||||
},
|
||||
"review": {
|
||||
"title": "Review and Confirm",
|
||||
"subtitle": "Verify that all information is correct",
|
||||
"fields": {
|
||||
"date": "Date:",
|
||||
"paymentMethod": "Payment Method:",
|
||||
"products": "Products",
|
||||
"total": "Total:",
|
||||
"notes": "Notes:"
|
||||
},
|
||||
"imported": {
|
||||
"title": "✓ File imported successfully",
|
||||
"recordsImported": "Records imported:",
|
||||
"recordsFailed": "Records failed:"
|
||||
}
|
||||
},
|
||||
"paymentMethods": {
|
||||
"cash": "Cash",
|
||||
"card": "Card",
|
||||
"mobile": "Mobile Payment",
|
||||
"transfer": "Transfer",
|
||||
"other": "Other"
|
||||
},
|
||||
"messages": {
|
||||
"errorObtainingTenantInfo": "Could not obtain tenant information",
|
||||
"errorLoadingProducts": "Error loading products",
|
||||
"salesEntryCreatedSuccessfully": "Sales entry created successfully",
|
||||
"errorCreatingSalesEntry": "Error creating sales entry",
|
||||
"errorValidatingFile": "Error validating file",
|
||||
"errorImportingFile": "Error importing file",
|
||||
"fileValidatedSuccessfully": "File validated successfully",
|
||||
"fileImportedSuccessfully": "File imported successfully"
|
||||
}
|
||||
},
|
||||
"tooltips": {
|
||||
"averageCost": "Average cost per unit based on purchase history",
|
||||
"standardCost": "Standard/expected cost per unit for costing calculations",
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
{
|
||||
"hero": {
|
||||
"pre_headline": "Para Panaderías que Pierden €500-2,000/Mes en Desperdicios",
|
||||
"pre_headline": "Para Panaderías que Pierden Dinero en Desperdicios",
|
||||
"scarcity": "Solo 12 plazas restantes de 20 • 3 meses GRATIS",
|
||||
"scarcity_badge": "🔥 Solo 12 plazas restantes de 20 en el programa piloto",
|
||||
"badge": "IA Avanzada para Panaderías Modernas",
|
||||
"title_line1": "Aumenta Ganancias,",
|
||||
"title_line2": "Reduce Desperdicios",
|
||||
"title_option_a_line1": "Ahorra €500-2,000 al Mes",
|
||||
"title_option_a_line2": "Produciendo Exactamente Lo Que Venderás",
|
||||
"title_option_a_line1": "Produce Exactamente Lo Que Venderás",
|
||||
"title_option_a_line2": "y Ahorra Miles",
|
||||
"title_option_b": "Deja de Adivinar Cuánto Hornear Cada Día",
|
||||
"subtitle": "IA que predice demanda con datos de tu zona para que produzcas exactamente lo que vas a vender. Reduce desperdicios, mejora márgenes y ahorra tiempo.",
|
||||
"subtitle_option_a": "La primera IA que conoce tu barrio: colegios cerca, clima local, tu competencia, eventos. Sistema automático cada mañana. Listo a las 6 AM.",
|
||||
"subtitle_option_a": "IA que conoce tu barrio. Predicciones listas cada mañana a las 6 AM.",
|
||||
"subtitle_option_b": "IA que conoce tu zona predice ventas con 92% de precisión. Despierta con tu plan listo: qué hacer, qué pedir, cuándo llegará. Ahorra €500-2,000/mes en desperdicios.",
|
||||
"cta_primary": "Únete al Programa Piloto",
|
||||
"cta_secondary": "Ver Cómo Funciona (2 min)",
|
||||
"cta_demo": "Ver Demo",
|
||||
"social_proof": {
|
||||
"bakeries": "20 panaderías ya ahorran €1,500/mes de promedio",
|
||||
"accuracy": "Predicciones 92% precisas (vs 60% sistemas genéricos)",
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
},
|
||||
"actions": {
|
||||
"approve": "Aprobar",
|
||||
"reject": "Rechazar",
|
||||
"view_details": "Ver Detalles",
|
||||
"modify": "Modificar",
|
||||
"dismiss": "Descartar",
|
||||
|
||||
@@ -6,7 +6,13 @@
|
||||
"leaveEmptyForAutoGeneration": "Dejar vacío para auto-generar",
|
||||
"readOnly": "Solo lectura - Auto-generado",
|
||||
"willBeGeneratedAutomatically": "Se generará automáticamente",
|
||||
"autoGeneratedOnSave": "Auto-generado al guardar"
|
||||
"autoGeneratedOnSave": "Auto-generado al guardar",
|
||||
"show": "Mostrar",
|
||||
"hide": "Ocultar",
|
||||
"next": "Siguiente",
|
||||
"back": "Atrás",
|
||||
"complete": "Completar",
|
||||
"stepOf": "Paso {{current}} de {{total}}"
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Agregar Inventario",
|
||||
@@ -16,7 +22,26 @@
|
||||
"steps": {
|
||||
"productType": "Tipo de Producto",
|
||||
"basicInfo": "Información Básica",
|
||||
"stockConfig": "Configuración de Stock"
|
||||
"stockConfig": "Configuración de Stock",
|
||||
"initialStock": "Stock Inicial"
|
||||
},
|
||||
"initialStockDescription": "Agrega uno o más lotes para registrar el inventario inicial",
|
||||
"stockConfig": {
|
||||
"product": "Producto",
|
||||
"totalQuantity": "Cantidad Total",
|
||||
"totalValue": "Valor Total",
|
||||
"lotsRegistered": "Lotes Registrados",
|
||||
"lot": "Lote",
|
||||
"remove": "Eliminar",
|
||||
"quantity": "Cantidad",
|
||||
"unitCost": "Costo Unitario ($)",
|
||||
"lotNumber": "Número de Lote",
|
||||
"expirationDate": "Fecha de Expiración",
|
||||
"location": "Ubicación",
|
||||
"lotValue": "Valor del lote:",
|
||||
"addInitialLot": "Agregar Lote Inicial",
|
||||
"addAnotherLot": "Agregar Otro Lote",
|
||||
"skipMessage": "Puedes saltar este paso si prefieres agregar el stock inicial más tarde"
|
||||
},
|
||||
"typeDescriptions": {
|
||||
"ingredient": "Materias primas e ingredientes utilizados en recetas",
|
||||
@@ -257,6 +282,20 @@
|
||||
"subtitle": "Seleccione productos y cantidades",
|
||||
"addItem": "Agregar Artículo",
|
||||
"removeItem": "Eliminar artículo",
|
||||
"customer": "Cliente",
|
||||
"orderProducts": "Productos del Pedido",
|
||||
"productNumber": "Producto #{{number}}",
|
||||
"product": "Producto",
|
||||
"productPlaceholder": "Seleccionar producto...",
|
||||
"selectProduct": "Seleccionar producto...",
|
||||
"quantity": "Cantidad",
|
||||
"unitPrice": "Precio Unitario (€)",
|
||||
"specialRequirements": "Requisitos Personalizados",
|
||||
"specialRequirementsPlaceholder": "Instrucciones especiales...",
|
||||
"customRequirements": "Requisitos Personalizados",
|
||||
"customRequirementsPlaceholder": "Instrucciones especiales...",
|
||||
"subtotal": "Subtotal",
|
||||
"total": "Cantidad Total",
|
||||
"fields": {
|
||||
"product": "Producto",
|
||||
"productPlaceholder": "Seleccionar producto...",
|
||||
@@ -265,8 +304,7 @@
|
||||
"customRequirements": "Requisitos Personalizados",
|
||||
"customRequirementsPlaceholder": "Instrucciones especiales...",
|
||||
"subtotal": "Subtotal"
|
||||
},
|
||||
"total": "Cantidad Total"
|
||||
}
|
||||
},
|
||||
"deliveryPayment": {
|
||||
"title": "Detalles de Entrega y Pago",
|
||||
@@ -474,7 +512,7 @@
|
||||
"description": "Crear una nueva receta o fórmula"
|
||||
},
|
||||
"equipment": {
|
||||
"title": "Equipo",
|
||||
"title": "Maquinaria",
|
||||
"description": "Registrar equipo o maquinaria de panadería"
|
||||
},
|
||||
"quality-template": {
|
||||
@@ -518,5 +556,516 @@
|
||||
"parameters": "Parámetros de plantilla",
|
||||
"thresholds": "Valores de umbral",
|
||||
"scoringCriteria": "Criterios de puntuación personalizados"
|
||||
},
|
||||
"supplier": {
|
||||
"title": "Agregar Proveedor",
|
||||
"supplierDetails": "Detalles del Proveedor",
|
||||
"subtitle": "Información esencial del proveedor",
|
||||
"advancedOptionsTitle": "Opciones Avanzadas",
|
||||
"advancedOptionsDescription": "Información adicional del proveedor y detalles comerciales",
|
||||
"fields": {
|
||||
"name": "Nombre del Proveedor",
|
||||
"namePlaceholder": "Ej: Premium Flour Suppliers Ltd.",
|
||||
"supplierCode": "Código de Proveedor",
|
||||
"supplierCodePlaceholder": "SUP-001",
|
||||
"supplierType": "Tipo de Proveedor",
|
||||
"supplierTypeTooltip": "Categoría de productos/servicios que proporciona este proveedor",
|
||||
"status": "Estado",
|
||||
"paymentTerms": "Términos de Pago",
|
||||
"currency": "Moneda",
|
||||
"currencyPlaceholder": "EUR",
|
||||
"leadTime": "Tiempo de Entrega Estándar (días)",
|
||||
"leadTimeTooltip": "Tiempo típico de entrega desde el pedido hasta la entrega",
|
||||
"contactPerson": "Persona de Contacto",
|
||||
"contactPersonPlaceholder": "Nombre del contacto",
|
||||
"email": "Correo Electrónico",
|
||||
"emailPlaceholder": "contacto@proveedor.com",
|
||||
"phone": "Teléfono",
|
||||
"phonePlaceholder": "+1 234 567 8900",
|
||||
"mobile": "Móvil",
|
||||
"mobilePlaceholder": "+1 234 567 8900",
|
||||
"taxId": "Identificación Fiscal",
|
||||
"taxIdPlaceholder": "NIF/CIF",
|
||||
"registrationNumber": "Número de Registro",
|
||||
"registrationNumberPlaceholder": "Número de registro mercantil",
|
||||
"website": "Sitio Web",
|
||||
"websitePlaceholder": "https://www.proveedor.com",
|
||||
"addressLine1": "Dirección - Línea 1",
|
||||
"addressLine1Placeholder": "Dirección de calle",
|
||||
"addressLine2": "Dirección - Línea 2",
|
||||
"addressLine2Placeholder": "Suite, edificio, etc.",
|
||||
"city": "Ciudad",
|
||||
"cityPlaceholder": "Ciudad",
|
||||
"state": "Estado/Provincia",
|
||||
"statePlaceholder": "Estado",
|
||||
"postalCode": "Código Postal",
|
||||
"postalCodePlaceholder": "Código postal",
|
||||
"country": "País",
|
||||
"countryPlaceholder": "País",
|
||||
"creditLimit": "Límite de Crédito",
|
||||
"creditLimitPlaceholder": "10000.00",
|
||||
"minOrderAmount": "Cantidad Mínima de Pedido",
|
||||
"minOrderAmountPlaceholder": "100.00",
|
||||
"deliveryArea": "Área de Entrega",
|
||||
"deliveryAreaPlaceholder": "Ej: Área Metropolitana de Madrid",
|
||||
"certifications": "Certificaciones",
|
||||
"certificationsPlaceholder": "Ej: ISO 9001, HACCP, Orgánico (separado por comas)",
|
||||
"specializations": "Especializaciones",
|
||||
"specializationsPlaceholder": "Ej: Harinas orgánicas, Productos sin gluten (separado por comas)",
|
||||
"notes": "Notas",
|
||||
"notesPlaceholder": "Notas adicionales sobre este proveedor...",
|
||||
"preferredSupplier": "Proveedor Preferido",
|
||||
"autoApproveOrders": "Auto-aprobar Pedidos"
|
||||
},
|
||||
"supplierTypes": {
|
||||
"ingredients": "Ingredientes",
|
||||
"packaging": "Embalaje",
|
||||
"equipment": "Equipo",
|
||||
"services": "Servicios",
|
||||
"utilities": "Servicios Públicos",
|
||||
"multi": "Múltiple"
|
||||
},
|
||||
"statuses": {
|
||||
"active": "Activo",
|
||||
"inactive": "Inactivo",
|
||||
"pending_approval": "Pendiente de Aprobación",
|
||||
"suspended": "Suspendido",
|
||||
"blacklisted": "En Lista Negra"
|
||||
},
|
||||
"paymentTerms": {
|
||||
"cod": "Contra Reembolso",
|
||||
"net_15": "Neto 15",
|
||||
"net_30": "Neto 30",
|
||||
"net_45": "Neto 45",
|
||||
"net_60": "Neto 60",
|
||||
"prepaid": "Prepago",
|
||||
"credit_terms": "Términos de Crédito"
|
||||
},
|
||||
"steps": {
|
||||
"supplierDetails": "Detalles del Proveedor",
|
||||
"supplierDetailsDescription": "Información esencial del proveedor"
|
||||
},
|
||||
"messages": {
|
||||
"errorObtainingTenantInfo": "No se pudo obtener información del tenant",
|
||||
"supplierCreatedSuccessfully": "Proveedor creado exitosamente",
|
||||
"errorCreatingSupplier": "Error al crear el proveedor"
|
||||
}
|
||||
},
|
||||
"recipe": {
|
||||
"title": "Agregar Receta",
|
||||
"recipeDetails": "Detalles de la Receta",
|
||||
"recipeDetailsDescription": "Información esencial sobre tu receta",
|
||||
"ingredients": "Ingredientes",
|
||||
"subtitle": "Información esencial sobre tu receta",
|
||||
"advancedOptionsTitle": "Opciones Avanzadas",
|
||||
"advancedOptionsDescription": "Campos opcionales para gestión detallada de recetas",
|
||||
"fields": {
|
||||
"name": "Nombre de la Receta",
|
||||
"namePlaceholder": "Ej: Baguette Tradicional",
|
||||
"category": "Categoría",
|
||||
"finishedProduct": "Producto Terminado",
|
||||
"finishedProductTooltip": "El producto final que produce esta receta. Debe crearse primero en el inventario.",
|
||||
"selectProduct": "Seleccionar producto...",
|
||||
"yieldQuantity": "Cantidad de Rendimiento",
|
||||
"yieldUnit": "Unidad de Rendimiento",
|
||||
"prepTime": "Tiempo de Preparación (minutos)",
|
||||
"prepTimePlaceholder": "30",
|
||||
"cookTime": "Tiempo de Cocción (minutos)",
|
||||
"cookTimePlaceholder": "45",
|
||||
"restTime": "Tiempo de Reposo (minutos)",
|
||||
"restTimeTooltip": "Tiempo para levar, enfriar o reposar",
|
||||
"restTimePlaceholder": "60",
|
||||
"totalTime": "Tiempo Total (minutos)",
|
||||
"totalTimePlaceholder": "135",
|
||||
"instructions": "Instrucciones",
|
||||
"instructionsPlaceholder": "Instrucciones de preparación paso a paso...",
|
||||
"recipeCode": "Código/SKU de Receta",
|
||||
"recipeCodePlaceholder": "RCP-001",
|
||||
"version": "Versión",
|
||||
"versionPlaceholder": "1.0",
|
||||
"difficulty": "Nivel de Dificultad (1-5)",
|
||||
"difficultyTooltip": "1 = Muy Fácil, 5 = Nivel Experto",
|
||||
"servesCount": "Número de Porciones",
|
||||
"servesCountPlaceholder": "12",
|
||||
"batchSizeMultiplier": "Multiplicador de Tamaño de Lote",
|
||||
"batchSizeMultiplierTooltip": "Factor de escalado predeterminado para producción en lote",
|
||||
"batchSizeMultiplierPlaceholder": "1.0",
|
||||
"minBatchSize": "Tamaño Mínimo de Lote",
|
||||
"minBatchSizePlaceholder": "1",
|
||||
"maxBatchSize": "Tamaño Máximo de Lote",
|
||||
"maxBatchSizePlaceholder": "100",
|
||||
"optimalTemp": "Temperatura Óptima de Producción (°C)",
|
||||
"optimalTempPlaceholder": "22",
|
||||
"optimalHumidity": "Humedad Óptima (%)",
|
||||
"optimalHumidityPlaceholder": "65",
|
||||
"targetMargin": "Margen Objetivo (%)",
|
||||
"targetMarginPlaceholder": "50",
|
||||
"description": "Descripción",
|
||||
"descriptionPlaceholder": "Descripción detallada de la receta...",
|
||||
"prepNotes": "Notas de Preparación",
|
||||
"prepNotesPlaceholder": "Consejos y notas para la preparación...",
|
||||
"storageInstructions": "Instrucciones de Almacenamiento",
|
||||
"storageInstructionsPlaceholder": "Cómo almacenar el producto terminado...",
|
||||
"allergens": "Alérgenos",
|
||||
"allergensPlaceholder": "Ej: gluten, lácteos, huevos (separado por comas)",
|
||||
"dietaryTags": "Etiquetas Dietéticas",
|
||||
"dietaryTagsPlaceholder": "Ej: vegano, sin gluten, orgánico (separado por comas)",
|
||||
"seasonalItem": "Artículo Estacional",
|
||||
"signatureItem": "Artículo Insignia",
|
||||
"seasonStartMonth": "Mes de Inicio de Temporada",
|
||||
"seasonStartMonthPlaceholder": "Seleccionar mes...",
|
||||
"seasonEndMonth": "Mes de Fin de Temporada",
|
||||
"seasonEndMonthPlaceholder": "Seleccionar mes..."
|
||||
},
|
||||
"categories": {
|
||||
"bread": "Pan",
|
||||
"pastries": "Pastelería",
|
||||
"cakes": "Pasteles",
|
||||
"cookies": "Galletas",
|
||||
"muffins": "Muffins",
|
||||
"sandwiches": "Sándwiches",
|
||||
"seasonal": "Estacional",
|
||||
"other": "Otro"
|
||||
},
|
||||
"units": {
|
||||
"units": "Unidades",
|
||||
"pieces": "Piezas",
|
||||
"kg": "Kilogramos (kg)",
|
||||
"g": "Gramos (g)",
|
||||
"l": "Litros (l)",
|
||||
"ml": "Mililitros (ml)",
|
||||
"cups": "Tazas",
|
||||
"tablespoons": "Cucharadas",
|
||||
"teaspoons": "Cucharaditas"
|
||||
},
|
||||
"ingredients": {
|
||||
"title": "Ingredientes",
|
||||
"noIngredientsAdded": "No se agregaron ingredientes",
|
||||
"clickToBegin": "Haz clic en \"Agregar Ingrediente\" para comenzar",
|
||||
"ingredient": "Ingrediente",
|
||||
"ingredientPlaceholder": "Seleccionar...",
|
||||
"quantity": "Cantidad",
|
||||
"unit": "Unidad",
|
||||
"notes": "Notas",
|
||||
"notesPlaceholder": "Opcional",
|
||||
"removeIngredient": "Eliminar ingrediente",
|
||||
"addIngredient": "Agregar Ingrediente"
|
||||
},
|
||||
"qualityTemplates": {
|
||||
"title": "Plantillas de Calidad (Opcional)",
|
||||
"subtitle": "Selecciona plantillas de control de calidad para aplicar a esta receta",
|
||||
"errorLoading": "Error al cargar plantillas de calidad",
|
||||
"loading": "Cargando plantillas...",
|
||||
"noTemplates": "No hay plantillas de calidad disponibles",
|
||||
"createFromWizard": "Puedes crear plantillas desde el asistente principal",
|
||||
"required": "Requerido",
|
||||
"type": "Tipo:",
|
||||
"everyXDays": "Cada X días",
|
||||
"templatesSelected": "plantilla(s) seleccionada(s)"
|
||||
},
|
||||
"steps": {
|
||||
"recipeDetails": "Detalles de la Receta",
|
||||
"recipeDetailsDescription": "Nombre, categoría, rendimiento",
|
||||
"ingredients": "Ingredientes",
|
||||
"ingredientsDescription": "Selección y cantidades",
|
||||
"qualityTemplates": "Plantillas de Calidad",
|
||||
"qualityTemplatesDescription": "Controles de calidad aplicables"
|
||||
},
|
||||
"messages": {
|
||||
"errorGettingTenant": "No se pudo obtener información del tenant",
|
||||
"creatingRecipe": "Creando receta...",
|
||||
"createRecipe": "Crear Receta",
|
||||
"successCreate": "Receta creada exitosamente",
|
||||
"errorCreate": "Error al crear la receta"
|
||||
}
|
||||
},
|
||||
"customer": {
|
||||
"title": "Agregar Cliente",
|
||||
"customerDetails": "Detalles del Cliente",
|
||||
"subtitle": "Información esencial del cliente",
|
||||
"advancedOptionsTitle": "Opciones Avanzadas",
|
||||
"advancedOptionsDescription": "Información adicional del cliente y términos comerciales",
|
||||
"tooltips": {
|
||||
"customerCode": "Identificador único para este cliente. Auto-generado pero editable."
|
||||
},
|
||||
"fields": {
|
||||
"name": "Nombre del Cliente",
|
||||
"namePlaceholder": "Ej: Restaurante El Molino",
|
||||
"customerCode": "Código de Cliente",
|
||||
"customerCodePlaceholder": "CUST-001",
|
||||
"customerType": "Tipo de Cliente",
|
||||
"email": "Correo Electrónico",
|
||||
"emailPlaceholder": "contacto@empresa.com",
|
||||
"phone": "Teléfono",
|
||||
"phonePlaceholder": "+1 234 567 8900",
|
||||
"country": "País",
|
||||
"countryPlaceholder": "US",
|
||||
"businessName": "Nombre Comercial",
|
||||
"businessNamePlaceholder": "Nombre legal del negocio",
|
||||
"addressLine1": "Dirección - Línea 1",
|
||||
"addressLine1Placeholder": "Dirección de calle",
|
||||
"addressLine2": "Dirección - Línea 2",
|
||||
"addressLine2Placeholder": "Apartamento, suite, etc.",
|
||||
"city": "Ciudad",
|
||||
"cityPlaceholder": "Ciudad",
|
||||
"state": "Estado/Provincia",
|
||||
"statePlaceholder": "Estado",
|
||||
"postalCode": "Código Postal",
|
||||
"postalCodePlaceholder": "12345",
|
||||
"taxId": "Identificación Fiscal",
|
||||
"taxIdPlaceholder": "Número de identificación fiscal",
|
||||
"businessLicense": "Licencia Comercial",
|
||||
"businessLicensePlaceholder": "Número de licencia comercial",
|
||||
"paymentTerms": "Términos de Pago",
|
||||
"creditLimit": "Límite de Crédito (€)",
|
||||
"creditLimitPlaceholder": "5000.00",
|
||||
"discountPercentage": "Porcentaje de Descuento (%)",
|
||||
"discountPercentagePlaceholder": "10",
|
||||
"customerSegment": "Segmento de Cliente",
|
||||
"priorityLevel": "Nivel de Prioridad",
|
||||
"preferredDeliveryMethod": "Método de Entrega Preferido",
|
||||
"specialInstructions": "Instrucciones Especiales",
|
||||
"specialInstructionsPlaceholder": "Notas o instrucciones especiales para este cliente..."
|
||||
},
|
||||
"customerTypes": {
|
||||
"individual": "Individual",
|
||||
"business": "Empresa",
|
||||
"central_bakery": "Panadería Central"
|
||||
},
|
||||
"paymentTerms": {
|
||||
"immediate": "Inmediato",
|
||||
"net_30": "Neto 30",
|
||||
"net_60": "Neto 60"
|
||||
},
|
||||
"segments": {
|
||||
"vip": "VIP",
|
||||
"regular": "Regular",
|
||||
"wholesale": "Mayorista"
|
||||
},
|
||||
"priorities": {
|
||||
"high": "Alta",
|
||||
"normal": "Normal",
|
||||
"low": "Baja"
|
||||
},
|
||||
"deliveryMethods": {
|
||||
"delivery": "Entrega a Domicilio",
|
||||
"pickup": "Recogida",
|
||||
"shipping": "Envío"
|
||||
},
|
||||
"steps": {
|
||||
"customerDetails": "Detalles del Cliente",
|
||||
"customerDetailsDescription": "Información de contacto y negocio"
|
||||
},
|
||||
"messages": {
|
||||
"errorObtainingTenantInfo": "No se pudo obtener información del tenant",
|
||||
"customerCreatedSuccessfully": "Cliente creado exitosamente",
|
||||
"errorCreatingCustomer": "Error al crear el cliente"
|
||||
}
|
||||
},
|
||||
"equipment": {
|
||||
"title": "Agregar Maquinaria",
|
||||
"equipmentDetails": "Detalles de la Maquinaria",
|
||||
"subtitle": "Equipo de Panadería",
|
||||
"fields": {
|
||||
"type": "Tipo de Equipo",
|
||||
"brand": "Marca/Modelo",
|
||||
"brandPlaceholder": "Ej: Rational SCC 101",
|
||||
"model": "Modelo",
|
||||
"location": "Ubicación",
|
||||
"locationPlaceholder": "Ej: Cocina principal",
|
||||
"status": "Estado",
|
||||
"purchaseDate": "Fecha de Compra"
|
||||
},
|
||||
"equipmentTypes": {
|
||||
"oven": "Horno",
|
||||
"mixer": "Amasadora",
|
||||
"proofer": "Fermentadora",
|
||||
"refrigerator": "Refrigerador",
|
||||
"other": "Otro"
|
||||
},
|
||||
"steps": {
|
||||
"equipmentDetails": "Detalles del Equipo",
|
||||
"equipmentDetailsDescription": "Tipo, modelo, ubicación"
|
||||
},
|
||||
"messages": {
|
||||
"errorGettingTenant": "No se pudo obtener información del tenant",
|
||||
"noBrand": "Sin marca",
|
||||
"successCreate": "Equipo creado exitosamente",
|
||||
"errorCreate": "Error al crear el equipo"
|
||||
}
|
||||
},
|
||||
"teamMember": {
|
||||
"title": "Agregar Miembro del Equipo",
|
||||
"memberDetails": "Miembro del Equipo",
|
||||
"roleAndPermissions": "Rol y Permisos",
|
||||
"specificPermissions": "Permisos Específicos",
|
||||
"subtitle": "Miembro del Equipo",
|
||||
"permissionsTitle": "Rol y Permisos",
|
||||
"steps": {
|
||||
"memberDetails": "Datos Personales",
|
||||
"memberDetailsDescription": "Nombre, contacto, posición",
|
||||
"roleAndPermissions": "Rol y Permisos",
|
||||
"roleAndPermissionsDescription": "Accesos al sistema"
|
||||
},
|
||||
"fields": {
|
||||
"fullName": "Nombre Completo",
|
||||
"fullNamePlaceholder": "Ej: Juan García",
|
||||
"email": "Correo Electrónico",
|
||||
"emailPlaceholder": "juan@panaderia.com",
|
||||
"phone": "Teléfono",
|
||||
"phonePlaceholder": "+34 123 456 789",
|
||||
"position": "Posición",
|
||||
"employmentType": "Tipo de Empleo",
|
||||
"systemRole": "Rol del Sistema",
|
||||
"specificPermissions": "Permisos Específicos"
|
||||
},
|
||||
"positions": {
|
||||
"baker": "Panadero",
|
||||
"pastryChef": "Pastelero",
|
||||
"manager": "Gerente",
|
||||
"sales": "Ventas",
|
||||
"delivery": "Repartidor"
|
||||
},
|
||||
"employmentTypes": {
|
||||
"fullTime": "Tiempo Completo",
|
||||
"partTime": "Medio Tiempo",
|
||||
"contractor": "Contratista"
|
||||
},
|
||||
"roles": {
|
||||
"admin": "Administrador",
|
||||
"manager": "Gerente",
|
||||
"staff": "Personal",
|
||||
"viewOnly": "Solo Lectura"
|
||||
},
|
||||
"permissions": {
|
||||
"canManageInventory": "Gestionar Inventario",
|
||||
"canViewRecipes": "Ver Recetas",
|
||||
"canCreateOrders": "Crear Pedidos",
|
||||
"canViewFinancial": "Ver Datos Financieros"
|
||||
},
|
||||
"messages": {
|
||||
"errorGettingTenant": "No se pudo obtener información del tenant",
|
||||
"successCreate": "Miembro del equipo agregado exitosamente",
|
||||
"errorCreate": "Error al crear el miembro del equipo"
|
||||
}
|
||||
},
|
||||
"salesEntry": {
|
||||
"title": "Registro de Ventas",
|
||||
"steps": {
|
||||
"entryMethod": "Método de Entrada",
|
||||
"entryMethodDescription": "Elige cómo registrar las ventas",
|
||||
"manualEntry": "Ingresar Datos",
|
||||
"manualEntryDescription": "Registra los detalles de la venta",
|
||||
"fileUpload": "Cargar Archivo",
|
||||
"fileUploadDescription": "Importa ventas desde archivo",
|
||||
"review": "Revisar",
|
||||
"reviewDescription": "Confirma los datos antes de guardar"
|
||||
},
|
||||
"entryMethod": {
|
||||
"title": "¿Cómo deseas registrar las ventas?",
|
||||
"subtitle": "Elige el método que mejor se adapte a tus necesidades",
|
||||
"manual": {
|
||||
"title": "Entrada Manual",
|
||||
"description": "Ingresa una o varias ventas de forma individual",
|
||||
"benefits": {
|
||||
"1": "Ideal para totales diarios",
|
||||
"2": "Control detallado por venta",
|
||||
"3": "Fácil y rápido"
|
||||
}
|
||||
},
|
||||
"file": {
|
||||
"title": "Cargar Archivo",
|
||||
"description": "Importa desde Excel o CSV",
|
||||
"recommended": "⭐ Recomendado para históricos",
|
||||
"benefits": {
|
||||
"1": "Ideal para datos históricos",
|
||||
"2": "Carga masiva (cientos de registros)",
|
||||
"3": "Ahorra tiempo significativo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"manualEntry": {
|
||||
"title": "Registrar Venta Manual",
|
||||
"subtitle": "Ingresa los detalles de la venta",
|
||||
"fields": {
|
||||
"saleDate": "Fecha de Venta",
|
||||
"paymentMethod": "Método de Pago",
|
||||
"notes": "Notas (Opcional)",
|
||||
"notesPlaceholder": "Información adicional sobre esta venta..."
|
||||
},
|
||||
"products": {
|
||||
"title": "Productos Vendidos",
|
||||
"addProduct": "+ Agregar Producto",
|
||||
"loading": "Cargando productos...",
|
||||
"noFinishedProducts": "No hay productos terminados disponibles",
|
||||
"addToInventory": "Agrega productos al inventario primero",
|
||||
"noProductsAdded": "No hay productos agregados",
|
||||
"clickToBegin": "Haz clic en 'Agregar Producto' para comenzar",
|
||||
"selectProduct": "Seleccionar producto...",
|
||||
"quantity": "Cant.",
|
||||
"price": "Precio",
|
||||
"removeProduct": "Eliminar producto",
|
||||
"total": "Total:"
|
||||
}
|
||||
},
|
||||
"fileUpload": {
|
||||
"title": "Cargar Archivo de Ventas",
|
||||
"subtitle": "Importa tus ventas desde Excel o CSV",
|
||||
"downloadTemplate": "Descargar Plantilla CSV",
|
||||
"downloading": "Descargando...",
|
||||
"dragDrop": {
|
||||
"title": "Arrastra un archivo aquí",
|
||||
"subtitle": "o haz clic para seleccionar",
|
||||
"button": "Seleccionar Archivo",
|
||||
"supportedFormats": "Formatos soportados: CSV, Excel (.xlsx, .xls)"
|
||||
},
|
||||
"validated": {
|
||||
"title": "✓ Archivo validado correctamente",
|
||||
"recordsFound": "Registros encontrados:",
|
||||
"validRecords": "Registros válidos:",
|
||||
"errors": "Errores:"
|
||||
},
|
||||
"validateButton": "Validar Archivo",
|
||||
"validating": "Validando...",
|
||||
"importButton": "Importar Datos",
|
||||
"importing": "Importando...",
|
||||
"instructions": {
|
||||
"title": "El archivo debe contener las columnas:",
|
||||
"columns": "fecha, producto, cantidad, precio_unitario, método_pago"
|
||||
}
|
||||
},
|
||||
"review": {
|
||||
"title": "Revisar y Confirmar",
|
||||
"subtitle": "Verifica que toda la información sea correcta",
|
||||
"fields": {
|
||||
"date": "Fecha:",
|
||||
"paymentMethod": "Método de Pago:",
|
||||
"products": "Productos",
|
||||
"total": "Total:",
|
||||
"notes": "Notas:"
|
||||
},
|
||||
"imported": {
|
||||
"title": "✓ Archivo importado correctamente",
|
||||
"recordsImported": "Registros importados:",
|
||||
"recordsFailed": "Registros fallidos:"
|
||||
}
|
||||
},
|
||||
"paymentMethods": {
|
||||
"cash": "Efectivo",
|
||||
"card": "Tarjeta",
|
||||
"mobile": "Pago Móvil",
|
||||
"transfer": "Transferencia",
|
||||
"other": "Otro"
|
||||
},
|
||||
"messages": {
|
||||
"errorObtainingTenantInfo": "No se pudo obtener información del tenant",
|
||||
"errorLoadingProducts": "Error al cargar productos",
|
||||
"salesEntryCreatedSuccessfully": "Entrada de ventas creada exitosamente",
|
||||
"errorCreatingSalesEntry": "Error al crear la entrada de ventas",
|
||||
"errorValidatingFile": "Error al validar el archivo",
|
||||
"errorImportingFile": "Error al importar el archivo",
|
||||
"fileValidatedSuccessfully": "Archivo validado exitosamente",
|
||||
"fileImportedSuccessfully": "Archivo importado exitosamente"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
{
|
||||
"hero": {
|
||||
"pre_headline": "Hondakinetan Dirua Galtzen Duten Okindegientzat",
|
||||
"scarcity": "20tik 12 plaza bakarrik geratzen dira • 3 hilabete DOAN",
|
||||
"scarcity_badge": "🔥 20tik 12 plaza bakarrik geratzen dira pilotu programan",
|
||||
"badge": "AA Aurreratua Okindegi Modernoetarako",
|
||||
"title_line1": "Utzi Galtzea €2,000 Hilean",
|
||||
"title_line2": "Inork Erosten Ez Duen Ogian",
|
||||
"subtitle": "IAk aurreikusten du zehatz-mehatz bihar zer salduko duzun. Ekoiztu justua. Murriztu hondakinak. Handitu irabaziak. <strong>3 hilabete doan lehenengo 20 okindegientzat</strong>.",
|
||||
"title_line1": "Handitu Irabaziak,",
|
||||
"title_line2": "Murriztu Hondakinak",
|
||||
"title_option_a_line1": "Ekoiztu Zehazki Salduko Duzuna",
|
||||
"title_option_a_line2": "eta Aurreztu Milaka",
|
||||
"title_option_b": "Utzi Asmatu Egunero Zenbat Labean Sartu",
|
||||
"subtitle": "IAk eskariaren aurreikuspena egiten du zure eremuaren datuekin, zehazki salduko duzuna ekoiztu dezazun. Murriztu hondakinak, hobetu marjinak, aurreztu denbora.",
|
||||
"subtitle_option_a": "IAk zure auzoa ezagutzen du. Aurreikuspenak prest goiz bakoitzean 6:00etan.",
|
||||
"subtitle_option_b": "Zure eremua ezagutzen duen IAk salmentak aurreikusten ditu %92ko zehaztasunarekin. Esnatu zure plana prestekin: zer egin, zer eskatu, noiz helduko den. Aurreztu €500-2,000/hilean hondakinetan.",
|
||||
"cta_primary": "Eskatu Pilotuko Plaza",
|
||||
"cta_secondary": "Ikusi Nola Lan Egiten Duen (2 min)",
|
||||
"cta_demo": "Ikusi Demoa",
|
||||
"trust": {
|
||||
"no_cc": "3 hilabete doan",
|
||||
"card": "Txartela beharrezkoa",
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
},
|
||||
"actions": {
|
||||
"approve": "Onartu",
|
||||
"reject": "Baztertu",
|
||||
"view_details": "Xehetasunak Ikusi",
|
||||
"modify": "Aldatu",
|
||||
"dismiss": "Baztertu",
|
||||
|
||||
@@ -16,7 +16,26 @@
|
||||
"steps": {
|
||||
"productType": "Produktu Mota",
|
||||
"basicInfo": "Oinarrizko Informazioa",
|
||||
"stockConfig": "Stock Konfigurazioa"
|
||||
"stockConfig": "Stock Konfigurazioa",
|
||||
"initialStock": "Hasierako Stock-a"
|
||||
},
|
||||
"initialStockDescription": "Gehitu lote bat edo gehiago hasierako inventarioa erregistratzeko",
|
||||
"stockConfig": {
|
||||
"product": "Produktua",
|
||||
"totalQuantity": "Kantitate Osoa",
|
||||
"totalValue": "Balio Osoa",
|
||||
"lotsRegistered": "Erregistratutako Loteak",
|
||||
"lot": "Lotea",
|
||||
"remove": "Kendu",
|
||||
"quantity": "Kantitatea",
|
||||
"unitCost": "Unitate Kostua ($)",
|
||||
"lotNumber": "Lote Zenbakia",
|
||||
"expirationDate": "Iraungitze Data",
|
||||
"location": "Kokapena",
|
||||
"lotValue": "Lotearen balioa:",
|
||||
"addInitialLot": "Gehitu Hasierako Lotea",
|
||||
"addAnotherLot": "Gehitu Beste Lote Bat",
|
||||
"skipMessage": "Urrats hau saltatu dezakezu hasierako stock-a geroago gehitzea nahiago baduzu"
|
||||
},
|
||||
"typeDescriptions": {
|
||||
"ingredient": "Errezetetan erabiltzen diren lehengaiak eta osagaiak",
|
||||
@@ -499,6 +518,125 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"salesEntry": {
|
||||
"title": "Salmenta Erregistroa",
|
||||
"steps": {
|
||||
"entryMethod": "Sarrera Metodoa",
|
||||
"entryMethodDescription": "Aukeratu salmentak erregistratzeko modua",
|
||||
"manualEntry": "Datuak Sartu",
|
||||
"manualEntryDescription": "Erregistratu salmenta xehetasunak",
|
||||
"fileUpload": "Fitxategia Kargatu",
|
||||
"fileUploadDescription": "Inportatu salmentak fitxategitik",
|
||||
"review": "Berrikusi",
|
||||
"reviewDescription": "Berretsi datuak gorde aurretik"
|
||||
},
|
||||
"entryMethod": {
|
||||
"title": "Nola nahi dituzu salmentak erregistratu?",
|
||||
"subtitle": "Aukeratu zure beharrei hobekien egokitzen zaion metodoa",
|
||||
"manual": {
|
||||
"title": "Eskuzko Sarrera",
|
||||
"description": "Sartu salmenta bat edo gehiago banaka",
|
||||
"benefits": {
|
||||
"1": "Egokia eguneko totaletarako",
|
||||
"2": "Kontrol zehatza salmenta bakoitzeko",
|
||||
"3": "Erraza eta azkarra"
|
||||
}
|
||||
},
|
||||
"file": {
|
||||
"title": "Fitxategia Kargatu",
|
||||
"description": "Inportatu Excel edo CSV-tik",
|
||||
"recommended": "⭐ Gomendatua datu historikoentzat",
|
||||
"benefits": {
|
||||
"1": "Egokia datu historikoentzat",
|
||||
"2": "Karga masiboa (ehunka erregistro)",
|
||||
"3": "Denbora asko aurrezten du"
|
||||
}
|
||||
}
|
||||
},
|
||||
"manualEntry": {
|
||||
"title": "Erregistratu Eskuzko Salmenta",
|
||||
"subtitle": "Sartu salmenta xehetasunak",
|
||||
"fields": {
|
||||
"saleDate": "Salmenta Data",
|
||||
"paymentMethod": "Ordainketa Metodoa",
|
||||
"notes": "Oharrak (Aukerakoa)",
|
||||
"notesPlaceholder": "Informazio gehigarria salmenta honi buruz..."
|
||||
},
|
||||
"products": {
|
||||
"title": "Saldutako Produktuak",
|
||||
"addProduct": "+ Gehitu Produktua",
|
||||
"loading": "Produktuak kargatzen...",
|
||||
"noFinishedProducts": "Ez dago produktu amaiturik eskuragarri",
|
||||
"addToInventory": "Gehitu produktuak inventariora lehenik",
|
||||
"noProductsAdded": "Ez da produkturik gehitu",
|
||||
"clickToBegin": "Egin klik 'Gehitu Produktua'-n hasteko",
|
||||
"selectProduct": "Hautatu produktua...",
|
||||
"quantity": "Kant.",
|
||||
"price": "Prezioa",
|
||||
"removeProduct": "Kendu produktua",
|
||||
"total": "Guztira:"
|
||||
}
|
||||
},
|
||||
"fileUpload": {
|
||||
"title": "Kargatu Salmenta Fitxategia",
|
||||
"subtitle": "Inportatu zure salmentak Excel edo CSV-tik",
|
||||
"downloadTemplate": "Deskargatu CSV Txantiloia",
|
||||
"downloading": "Deskargatzen...",
|
||||
"dragDrop": {
|
||||
"title": "Arrastatu fitxategi bat hona",
|
||||
"subtitle": "edo egin klik hautatzeko",
|
||||
"button": "Hautatu Fitxategia",
|
||||
"supportedFormats": "Onartutako formatuak: CSV, Excel (.xlsx, .xls)"
|
||||
},
|
||||
"validated": {
|
||||
"title": "✓ Fitxategia ondo baliozkotuta",
|
||||
"recordsFound": "Aurkitutako erregistroak:",
|
||||
"validRecords": "Erregistro baliozkoak:",
|
||||
"errors": "Erroreak:"
|
||||
},
|
||||
"validateButton": "Baliozkotu Fitxategia",
|
||||
"validating": "Baliozkotzean...",
|
||||
"importButton": "Inportatu Datuak",
|
||||
"importing": "Inportatzen...",
|
||||
"instructions": {
|
||||
"title": "Fitxategiak zutabe hauek eduki behar ditu:",
|
||||
"columns": "data, produktua, kantitatea, unitate_prezioa, ordainketa_metodoa"
|
||||
}
|
||||
},
|
||||
"review": {
|
||||
"title": "Berrikusi eta Berretsi",
|
||||
"subtitle": "Egiaztatu informazio guztia zuzena dela",
|
||||
"fields": {
|
||||
"date": "Data:",
|
||||
"paymentMethod": "Ordainketa Metodoa:",
|
||||
"products": "Produktuak",
|
||||
"total": "Guztira:",
|
||||
"notes": "Oharrak:"
|
||||
},
|
||||
"imported": {
|
||||
"title": "✓ Fitxategia ondo inportatu da",
|
||||
"recordsImported": "Inportatutako erregistroak:",
|
||||
"recordsFailed": "Huts egin duten erregistroak:"
|
||||
}
|
||||
},
|
||||
"paymentMethods": {
|
||||
"cash": "Dirua",
|
||||
"card": "Txartela",
|
||||
"mobile": "Mugikorreko Ordainketa",
|
||||
"transfer": "Transferentzia",
|
||||
"other": "Bestelakoa"
|
||||
},
|
||||
"messages": {
|
||||
"errorObtainingTenantInfo": "Ezin izan da tenant informazioa lortu",
|
||||
"errorLoadingProducts": "Errorea produktuak kargatzean",
|
||||
"salesEntryCreatedSuccessfully": "Salmenta erregistroa ondo sortu da",
|
||||
"errorCreatingSalesEntry": "Errorea salmenta erregistroa sortzean",
|
||||
"errorValidatingFile": "Errorea fitxategia baliozkotzean",
|
||||
"errorImportingFile": "Errorea fitxategia inportatzen",
|
||||
"fileValidatedSuccessfully": "Fitxategia ondo baliozkotu da",
|
||||
"fileImportedSuccessfully": "Fitxategia ondo inportatu da"
|
||||
}
|
||||
},
|
||||
"tooltips": {
|
||||
"averageCost": "Batez besteko kostua unitateko erosketa historikoan oinarrituta",
|
||||
"standardCost": "Kostu estandarra/espero unitateko kostu kalkuluetarako",
|
||||
|
||||
@@ -37,6 +37,7 @@ import { OrchestrationSummaryCard } from '../../components/dashboard/Orchestrati
|
||||
import { ProductionTimelineCard } from '../../components/dashboard/ProductionTimelineCard';
|
||||
import { InsightsGrid } from '../../components/dashboard/InsightsGrid';
|
||||
import { PurchaseOrderDetailsModal } from '../../components/dashboard/PurchaseOrderDetailsModal';
|
||||
import { ModifyPurchaseOrderModal } from '../../components/domain/procurement/ModifyPurchaseOrderModal';
|
||||
import { UnifiedAddWizard } from '../../components/domain/unified-wizard';
|
||||
import type { ItemType } from '../../components/domain/unified-wizard';
|
||||
import { useDemoTour, shouldStartTour, clearTourStartPending } from '../../features/demo-onboarding';
|
||||
@@ -57,6 +58,10 @@ export function NewDashboardPage() {
|
||||
const [selectedPOId, setSelectedPOId] = useState<string | null>(null);
|
||||
const [isPOModalOpen, setIsPOModalOpen] = useState(false);
|
||||
|
||||
// PO Modify Modal state
|
||||
const [modifyPOId, setModifyPOId] = useState<string | null>(null);
|
||||
const [isModifyPOModalOpen, setIsModifyPOModalOpen] = useState(false);
|
||||
|
||||
// Data fetching
|
||||
const {
|
||||
data: healthStatus,
|
||||
@@ -124,8 +129,9 @@ export function NewDashboardPage() {
|
||||
};
|
||||
|
||||
const handleModify = (actionId: string) => {
|
||||
// Navigate to procurement page for modification
|
||||
navigate(`/app/operations/procurement`);
|
||||
// Open modal to modify PO
|
||||
setModifyPOId(actionId);
|
||||
setIsModifyPOModalOpen(true);
|
||||
};
|
||||
|
||||
const handleStartBatch = async (batchId: string) => {
|
||||
@@ -209,7 +215,7 @@ export function NewDashboardPage() {
|
||||
}, [isDemoMode, startTour]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen pb-20 md:pb-8" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
||||
<div className="min-h-screen pb-20 md:pb-8">
|
||||
{/* Mobile-optimized container */}
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
{/* Header */}
|
||||
@@ -307,7 +313,7 @@ export function NewDashboardPage() {
|
||||
</div>
|
||||
|
||||
{/* SECTION 6: Quick Action Links */}
|
||||
<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)' }}>
|
||||
<h2 className="text-xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>{t('dashboard:sections.quick_actions')}</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<button
|
||||
@@ -374,6 +380,23 @@ export function NewDashboardPage() {
|
||||
onModify={handleModify}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Modify Purchase Order Modal */}
|
||||
{modifyPOId && (
|
||||
<ModifyPurchaseOrderModal
|
||||
poId={modifyPOId}
|
||||
isOpen={isModifyPOModalOpen}
|
||||
onClose={() => {
|
||||
setIsModifyPOModalOpen(false);
|
||||
setModifyPOId(null);
|
||||
}}
|
||||
onSuccess={() => {
|
||||
setIsModifyPOModalOpen(false);
|
||||
setModifyPOId(null);
|
||||
handleRefreshAll();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
296
frontend/src/pages/app/admin/WhatsAppAdminPage.tsx
Normal file
296
frontend/src/pages/app/admin/WhatsAppAdminPage.tsx
Normal file
@@ -0,0 +1,296 @@
|
||||
// frontend/src/pages/app/admin/WhatsAppAdminPage.tsx
|
||||
/**
|
||||
* WhatsApp Admin Management Page
|
||||
* Admin-only interface for assigning WhatsApp phone numbers to tenants
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { MessageSquare, Phone, CheckCircle, AlertCircle, Loader2, Users } from 'lucide-react';
|
||||
import axios from 'axios';
|
||||
|
||||
interface PhoneNumberInfo {
|
||||
id: string;
|
||||
display_phone_number: string;
|
||||
verified_name: string;
|
||||
quality_rating: string;
|
||||
}
|
||||
|
||||
interface TenantWhatsAppStatus {
|
||||
tenant_id: string;
|
||||
tenant_name: string;
|
||||
whatsapp_enabled: boolean;
|
||||
phone_number_id: string | null;
|
||||
display_phone_number: string | null;
|
||||
}
|
||||
|
||||
const WhatsAppAdminPage: React.FC = () => {
|
||||
const [availablePhones, setAvailablePhones] = useState<PhoneNumberInfo[]>([]);
|
||||
const [tenants, setTenants] = useState<TenantWhatsAppStatus[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [assigningPhone, setAssigningPhone] = useState<string | null>(null);
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8001';
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Fetch available phone numbers
|
||||
const phonesResponse = await axios.get(`${API_BASE_URL}/api/v1/admin/whatsapp/phone-numbers`);
|
||||
setAvailablePhones(phonesResponse.data);
|
||||
|
||||
// Fetch tenant WhatsApp status
|
||||
const tenantsResponse = await axios.get(`${API_BASE_URL}/api/v1/admin/whatsapp/tenants`);
|
||||
setTenants(tenantsResponse.data);
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.detail || 'Failed to load WhatsApp data');
|
||||
console.error('Failed to fetch WhatsApp data:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const assignPhoneNumber = async (tenantId: string, phoneNumberId: string, displayPhone: string) => {
|
||||
setAssigningPhone(tenantId);
|
||||
|
||||
try {
|
||||
await axios.post(`${API_BASE_URL}/api/v1/admin/whatsapp/tenants/${tenantId}/assign-phone`, {
|
||||
phone_number_id: phoneNumberId,
|
||||
display_phone_number: displayPhone
|
||||
});
|
||||
|
||||
// Refresh data
|
||||
await fetchData();
|
||||
} catch (err: any) {
|
||||
alert(err.response?.data?.detail || 'Failed to assign phone number');
|
||||
console.error('Failed to assign phone:', err);
|
||||
} finally {
|
||||
setAssigningPhone(null);
|
||||
}
|
||||
};
|
||||
|
||||
const unassignPhoneNumber = async (tenantId: string) => {
|
||||
if (!confirm('Are you sure you want to unassign this phone number?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
setAssigningPhone(tenantId);
|
||||
|
||||
try {
|
||||
await axios.delete(`${API_BASE_URL}/api/v1/admin/whatsapp/tenants/${tenantId}/unassign-phone`);
|
||||
|
||||
// Refresh data
|
||||
await fetchData();
|
||||
} catch (err: any) {
|
||||
alert(err.response?.data?.detail || 'Failed to unassign phone number');
|
||||
console.error('Failed to unassign phone:', err);
|
||||
} finally {
|
||||
setAssigningPhone(null);
|
||||
}
|
||||
};
|
||||
|
||||
const getQualityRatingColor = (rating: string) => {
|
||||
switch (rating.toUpperCase()) {
|
||||
case 'GREEN':
|
||||
return 'text-green-600 bg-green-100';
|
||||
case 'YELLOW':
|
||||
return 'text-yellow-600 bg-yellow-100';
|
||||
case 'RED':
|
||||
return 'text-red-600 bg-red-100';
|
||||
default:
|
||||
return 'text-gray-600 bg-gray-100';
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 flex items-center gap-3">
|
||||
<MessageSquare className="w-8 h-8 text-blue-600" />
|
||||
WhatsApp Admin Management
|
||||
</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Assign WhatsApp phone numbers to bakery tenants
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg flex items-start gap-2">
|
||||
<AlertCircle className="w-5 h-5 text-red-600 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-sm text-red-800">{error}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Available Phone Numbers */}
|
||||
<div className="mb-8 bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||
<div className="p-6 border-b border-gray-200 bg-gray-50">
|
||||
<h2 className="text-xl font-semibold text-gray-900 flex items-center gap-2">
|
||||
<Phone className="w-5 h-5" />
|
||||
Available Phone Numbers ({availablePhones.length})
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-gray-200">
|
||||
{availablePhones.length === 0 ? (
|
||||
<div className="p-6 text-center text-gray-500">
|
||||
<p>No phone numbers available. Please add phone numbers to your WhatsApp Business Account.</p>
|
||||
</div>
|
||||
) : (
|
||||
availablePhones.map((phone) => (
|
||||
<div key={phone.id} className="p-4 hover:bg-gray-50 transition-colors">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<Phone className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-mono font-semibold text-gray-900">{phone.display_phone_number}</p>
|
||||
<p className="text-sm text-gray-500">{phone.verified_name}</p>
|
||||
<p className="text-xs text-gray-400 mt-1">ID: {phone.id}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`px-3 py-1 rounded-full text-xs font-semibold ${getQualityRatingColor(phone.quality_rating)}`}>
|
||||
{phone.quality_rating}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tenants List */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||
<div className="p-6 border-b border-gray-200 bg-gray-50">
|
||||
<h2 className="text-xl font-semibold text-gray-900 flex items-center gap-2">
|
||||
<Users className="w-5 h-5" />
|
||||
Bakery Tenants ({tenants.length})
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-gray-200">
|
||||
{tenants.length === 0 ? (
|
||||
<div className="p-6 text-center text-gray-500">
|
||||
<p>No tenants found.</p>
|
||||
</div>
|
||||
) : (
|
||||
tenants.map((tenant) => (
|
||||
<div key={tenant.tenant_id} className="p-4 hover:bg-gray-50 transition-colors">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3">
|
||||
<h3 className="font-semibold text-gray-900">{tenant.tenant_name}</h3>
|
||||
{tenant.whatsapp_enabled && tenant.display_phone_number ? (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 bg-green-100 text-green-800 rounded-full text-xs font-medium">
|
||||
<CheckCircle className="w-3 h-3" />
|
||||
Active
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 bg-gray-100 text-gray-600 rounded-full text-xs font-medium">
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
Not Configured
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{tenant.display_phone_number ? (
|
||||
<p className="text-sm text-gray-600 mt-1 font-mono">
|
||||
Phone: {tenant.display_phone_number}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
No phone number assigned
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{tenant.display_phone_number ? (
|
||||
<button
|
||||
onClick={() => unassignPhoneNumber(tenant.tenant_id)}
|
||||
disabled={assigningPhone === tenant.tenant_id}
|
||||
className="px-4 py-2 bg-red-100 text-red-700 rounded-lg hover:bg-red-200 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||
>
|
||||
{assigningPhone === tenant.tenant_id ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Unassigning...
|
||||
</>
|
||||
) : (
|
||||
'Unassign'
|
||||
)}
|
||||
</button>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
onChange={(e) => {
|
||||
if (e.target.value) {
|
||||
const phone = availablePhones.find(p => p.id === e.target.value);
|
||||
if (phone) {
|
||||
assignPhoneNumber(tenant.tenant_id, phone.id, phone.display_phone_number);
|
||||
}
|
||||
e.target.value = ''; // Reset select
|
||||
}
|
||||
}}
|
||||
disabled={assigningPhone === tenant.tenant_id || availablePhones.length === 0}
|
||||
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<option value="">Assign phone number...</option>
|
||||
{availablePhones.map((phone) => (
|
||||
<option key={phone.id} value={phone.id}>
|
||||
{phone.display_phone_number} - {phone.verified_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{assigningPhone === tenant.tenant_id && (
|
||||
<Loader2 className="w-5 h-5 animate-spin text-blue-600" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Refresh Button */}
|
||||
<div className="mt-6 flex justify-end">
|
||||
<button
|
||||
onClick={fetchData}
|
||||
disabled={loading}
|
||||
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 font-medium"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Refreshing...
|
||||
</>
|
||||
) : (
|
||||
'Refresh Data'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WhatsAppAdminPage;
|
||||
@@ -44,12 +44,6 @@ const NotificationSettingsCard: React.FC<NotificationSettingsCardProps> = ({
|
||||
onChange({ ...settings, [field]: newChannels });
|
||||
};
|
||||
|
||||
const apiVersionOptions = [
|
||||
{ value: 'v18.0', label: 'v18.0' },
|
||||
{ value: 'v19.0', label: 'v19.0' },
|
||||
{ value: 'v20.0', label: 'v20.0' }
|
||||
];
|
||||
|
||||
const languageOptions = [
|
||||
{ value: 'es', label: 'Español' },
|
||||
{ value: 'eu', label: 'Euskara' },
|
||||
@@ -80,45 +74,40 @@ const NotificationSettingsCard: React.FC<NotificationSettingsCardProps> = ({
|
||||
<>
|
||||
<div className="p-4 sm:p-6 bg-[var(--bg-secondary)]">
|
||||
<h5 className="text-sm font-medium text-[var(--text-secondary)] mb-4">
|
||||
WhatsApp Business API Configuration
|
||||
WhatsApp Configuration
|
||||
</h5>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Input
|
||||
label={t('notification.whatsapp_phone_number_id')}
|
||||
value={settings.whatsapp_phone_number_id}
|
||||
onChange={handleChange('whatsapp_phone_number_id')}
|
||||
disabled={disabled}
|
||||
placeholder="123456789012345"
|
||||
helperText={t('notification.whatsapp_phone_number_id_help')}
|
||||
/>
|
||||
|
||||
<Input
|
||||
type="password"
|
||||
label={t('notification.whatsapp_access_token')}
|
||||
value={settings.whatsapp_access_token}
|
||||
onChange={handleChange('whatsapp_access_token')}
|
||||
disabled={disabled}
|
||||
placeholder="EAAxxxxxxxx"
|
||||
helperText={t('notification.whatsapp_access_token_help')}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label={t('notification.whatsapp_business_account_id')}
|
||||
value={settings.whatsapp_business_account_id}
|
||||
onChange={handleChange('whatsapp_business_account_id')}
|
||||
disabled={disabled}
|
||||
placeholder="987654321098765"
|
||||
helperText={t('notification.whatsapp_business_account_id_help')}
|
||||
/>
|
||||
|
||||
<Select
|
||||
label={t('notification.whatsapp_api_version')}
|
||||
options={apiVersionOptions}
|
||||
value={settings.whatsapp_api_version}
|
||||
onChange={handleSelectChange('whatsapp_api_version')}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{/* Display Phone Number */}
|
||||
{settings.whatsapp_display_phone_number ? (
|
||||
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center justify-center w-10 h-10 bg-green-100 dark:bg-green-800 rounded-full">
|
||||
<MessageSquare className="w-5 h-5 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-green-900 dark:text-green-100">
|
||||
WhatsApp Configured
|
||||
</p>
|
||||
<p className="text-xs text-green-700 dark:text-green-300 mt-1">
|
||||
Phone: <span className="font-mono font-semibold">{settings.whatsapp_display_phone_number}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertCircle className="w-4 h-4 text-yellow-600 dark:text-yellow-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-xs text-yellow-700 dark:text-yellow-300">
|
||||
<p className="font-semibold mb-1">No phone number assigned</p>
|
||||
<p>Please contact support to have a WhatsApp phone number assigned to your bakery.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Language Preference */}
|
||||
<div className="mt-4">
|
||||
<Select
|
||||
label={t('notification.whatsapp_default_language')}
|
||||
options={languageOptions}
|
||||
@@ -128,17 +117,13 @@ const NotificationSettingsCard: React.FC<NotificationSettingsCardProps> = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* WhatsApp Setup Info */}
|
||||
{/* WhatsApp Info */}
|
||||
<div className="mt-4 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<div className="flex items-start gap-2">
|
||||
<Info className="w-4 h-4 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-xs text-blue-700 dark:text-blue-300">
|
||||
<p className="font-semibold mb-1">{t('notification.whatsapp_setup_note')}</p>
|
||||
<ul className="list-disc list-inside space-y-1">
|
||||
<li>{t('notification.whatsapp_setup_step1')}</li>
|
||||
<li>{t('notification.whatsapp_setup_step2')}</li>
|
||||
<li>{t('notification.whatsapp_setup_step3')}</li>
|
||||
</ul>
|
||||
<p className="font-semibold mb-1">WhatsApp Notifications Included</p>
|
||||
<p>WhatsApp messaging is included in your subscription. Your notifications will be sent from the phone number shown above to your suppliers and team members.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,8 @@ import {
|
||||
Building2,
|
||||
Cloud,
|
||||
Euro,
|
||||
ChevronRight
|
||||
ChevronRight,
|
||||
Play
|
||||
} from 'lucide-react';
|
||||
|
||||
const LandingPage: React.FC = () => {
|
||||
@@ -90,6 +91,18 @@ const LandingPage: React.FC = () => {
|
||||
</span>
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to={getDemoUrl()} className="w-full sm:w-auto">
|
||||
<Button
|
||||
size="lg"
|
||||
variant="secondary"
|
||||
className="w-full sm:w-auto group px-10 py-5 text-lg font-bold shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-300 rounded-xl"
|
||||
>
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
{t('landing:hero.cta_demo', 'Ver Demo')}
|
||||
<Play className="w-5 h-5 group-hover:scale-110 transition-transform" />
|
||||
</span>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Social Proof - New */}
|
||||
@@ -98,13 +111,13 @@ const LandingPage: React.FC = () => {
|
||||
<div className="flex items-start gap-3 bg-white/60 dark:bg-gray-800/60 backdrop-blur-sm p-4 rounded-xl shadow-sm border border-[var(--border-primary)]">
|
||||
<CheckCircle2 className="w-5 h-5 text-green-600 dark:text-green-400 mt-0.5 flex-shrink-0" />
|
||||
<span className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
<AnimatedCounter value={20} className="inline font-bold" /> panaderías ya ahorran <AnimatedCounter value={1500} prefix="€" className="inline font-bold" />/mes de promedio
|
||||
{t('landing:hero.social_proof.bakeries', '20 panaderías ya ahorran €1,500/mes de promedio')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 bg-white/60 dark:bg-gray-800/60 backdrop-blur-sm p-4 rounded-xl shadow-sm border border-[var(--border-primary)]">
|
||||
<Target className="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" />
|
||||
<span className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Predicciones <AnimatedCounter value={92} suffix="%" className="inline font-bold" /> precisas (vs 60% sistemas genéricos)
|
||||
{t('landing:hero.social_proof.accuracy', 'Predicciones 92% precisas (vs 60% sistemas genéricos)')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 bg-white/60 dark:bg-gray-800/60 backdrop-blur-sm p-4 rounded-xl shadow-sm border border-[var(--border-primary)]">
|
||||
|
||||
Reference in New Issue
Block a user