Clean frontend
This commit is contained in:
@@ -1,404 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Input, Select, Badge } from '../../ui';
|
||||
import type { OnboardingStepProps } from './OnboardingWizard';
|
||||
|
||||
interface CompanyInfo {
|
||||
name: string;
|
||||
type: 'artisan' | 'industrial' | 'chain' | 'mixed';
|
||||
size: 'small' | 'medium' | 'large';
|
||||
locations: number;
|
||||
specialties: string[];
|
||||
address: {
|
||||
street: string;
|
||||
city: string;
|
||||
state: string;
|
||||
postal_code: string;
|
||||
country: string;
|
||||
};
|
||||
contact: {
|
||||
phone: string;
|
||||
email: string;
|
||||
website?: string;
|
||||
};
|
||||
established_year?: number;
|
||||
tax_id?: string;
|
||||
}
|
||||
|
||||
const BAKERY_TYPES = [
|
||||
{ value: 'artisan', label: 'Artesanal', description: 'Producción tradicional y manual' },
|
||||
{ value: 'industrial', label: 'Industrial', description: 'Producción automatizada a gran escala' },
|
||||
{ value: 'chain', label: 'Cadena', description: 'Múltiples ubicaciones con procesos estandarizados' },
|
||||
{ value: 'mixed', label: 'Mixta', description: 'Combinación de métodos artesanales e industriales' },
|
||||
];
|
||||
|
||||
const BAKERY_SIZES = [
|
||||
{ value: 'small', label: 'Pequeña', description: '1-10 empleados' },
|
||||
{ value: 'medium', label: 'Mediana', description: '11-50 empleados' },
|
||||
{ value: 'large', label: 'Grande', description: '50+ empleados' },
|
||||
];
|
||||
|
||||
const COMMON_SPECIALTIES = [
|
||||
'Pan tradicional',
|
||||
'Bollería',
|
||||
'Repostería',
|
||||
'Pan integral',
|
||||
'Pasteles',
|
||||
'Productos sin gluten',
|
||||
'Productos veganos',
|
||||
'Pan artesanal',
|
||||
'Productos de temporada',
|
||||
'Catering',
|
||||
];
|
||||
|
||||
export const CompanyInfoStep: React.FC<OnboardingStepProps> = ({
|
||||
data,
|
||||
onDataChange,
|
||||
}) => {
|
||||
const companyData: CompanyInfo = {
|
||||
name: '',
|
||||
type: 'artisan',
|
||||
size: 'small',
|
||||
locations: 1,
|
||||
specialties: [],
|
||||
address: {
|
||||
street: '',
|
||||
city: '',
|
||||
state: '',
|
||||
postal_code: '',
|
||||
country: 'España',
|
||||
},
|
||||
contact: {
|
||||
phone: '',
|
||||
email: '',
|
||||
website: '',
|
||||
},
|
||||
...data,
|
||||
};
|
||||
|
||||
const handleInputChange = (field: string, value: any) => {
|
||||
onDataChange({
|
||||
...companyData,
|
||||
[field]: value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddressChange = (field: string, value: string) => {
|
||||
onDataChange({
|
||||
...companyData,
|
||||
address: {
|
||||
...companyData.address,
|
||||
[field]: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleContactChange = (field: string, value: string) => {
|
||||
onDataChange({
|
||||
...companyData,
|
||||
contact: {
|
||||
...companyData.contact,
|
||||
[field]: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSpecialtyToggle = (specialty: string) => {
|
||||
const currentSpecialties = companyData.specialties || [];
|
||||
const updatedSpecialties = currentSpecialties.includes(specialty)
|
||||
? currentSpecialties.filter(s => s !== specialty)
|
||||
: [...currentSpecialties, specialty];
|
||||
|
||||
handleInputChange('specialties', updatedSpecialties);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Basic Information */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 border-b pb-2">
|
||||
Información básica
|
||||
</h3>
|
||||
|
||||
<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-gray-700 mb-1">
|
||||
Nombre de la panadería *
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={companyData.name}
|
||||
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||
placeholder="Ej: Panadería San Miguel"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Tipo de panadería *
|
||||
</label>
|
||||
<Select
|
||||
value={companyData.type}
|
||||
onChange={(e) => handleInputChange('type', e.target.value)}
|
||||
className="w-full"
|
||||
>
|
||||
{BAKERY_TYPES.map((type) => (
|
||||
<option key={type.value} value={type.value}>
|
||||
{type.label} - {type.description}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Tamaño de la empresa *
|
||||
</label>
|
||||
<Select
|
||||
value={companyData.size}
|
||||
onChange={(e) => handleInputChange('size', e.target.value)}
|
||||
className="w-full"
|
||||
>
|
||||
{BAKERY_SIZES.map((size) => (
|
||||
<option key={size.value} value={size.value}>
|
||||
{size.label} - {size.description}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Número de ubicaciones
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
value={companyData.locations}
|
||||
onChange={(e) => handleInputChange('locations', parseInt(e.target.value) || 1)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Año de fundación
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1900"
|
||||
max={new Date().getFullYear()}
|
||||
value={companyData.established_year || ''}
|
||||
onChange={(e) => handleInputChange('established_year', parseInt(e.target.value) || undefined)}
|
||||
placeholder="Ej: 1995"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
NIF/CIF
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={companyData.tax_id || ''}
|
||||
onChange={(e) => handleInputChange('tax_id', e.target.value)}
|
||||
placeholder="Ej: B12345678"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Specialties */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 border-b pb-2">
|
||||
Especialidades
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600">
|
||||
Selecciona los productos que produces habitualmente
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
{COMMON_SPECIALTIES.map((specialty) => (
|
||||
<button
|
||||
key={specialty}
|
||||
onClick={() => handleSpecialtyToggle(specialty)}
|
||||
className={`p-3 text-left border rounded-lg transition-colors ${
|
||||
companyData.specialties?.includes(specialty)
|
||||
? 'border-blue-500 bg-blue-50 text-blue-700'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<span className="text-sm font-medium">{specialty}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{companyData.specialties && companyData.specialties.length > 0 && (
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-700 mb-2">
|
||||
Especialidades seleccionadas:
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{companyData.specialties.map((specialty) => (
|
||||
<Badge key={specialty} className="bg-blue-100 text-blue-800">
|
||||
{specialty}
|
||||
<button
|
||||
onClick={() => handleSpecialtyToggle(specialty)}
|
||||
className="ml-1 text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Address */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 border-b pb-2">
|
||||
Dirección principal
|
||||
</h3>
|
||||
|
||||
<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-gray-700 mb-1">
|
||||
Dirección *
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={companyData.address.street}
|
||||
onChange={(e) => handleAddressChange('street', e.target.value)}
|
||||
placeholder="Calle, número, piso, puerta"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Ciudad *
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={companyData.address.city}
|
||||
onChange={(e) => handleAddressChange('city', e.target.value)}
|
||||
placeholder="Ej: Madrid"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Provincia/Estado *
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={companyData.address.state}
|
||||
onChange={(e) => handleAddressChange('state', e.target.value)}
|
||||
placeholder="Ej: Madrid"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Código postal *
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={companyData.address.postal_code}
|
||||
onChange={(e) => handleAddressChange('postal_code', e.target.value)}
|
||||
placeholder="Ej: 28001"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
País *
|
||||
</label>
|
||||
<Select
|
||||
value={companyData.address.country}
|
||||
onChange={(e) => handleAddressChange('country', e.target.value)}
|
||||
className="w-full"
|
||||
>
|
||||
<option value="España">España</option>
|
||||
<option value="Francia">Francia</option>
|
||||
<option value="Portugal">Portugal</option>
|
||||
<option value="Italia">Italia</option>
|
||||
<option value="México">México</option>
|
||||
<option value="Argentina">Argentina</option>
|
||||
<option value="Colombia">Colombia</option>
|
||||
<option value="Otro">Otro</option>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contact Information */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 border-b pb-2">
|
||||
Información de contacto
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Teléfono *
|
||||
</label>
|
||||
<Input
|
||||
type="tel"
|
||||
value={companyData.contact.phone}
|
||||
onChange={(e) => handleContactChange('phone', e.target.value)}
|
||||
placeholder="Ej: +34 911 234 567"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email de contacto *
|
||||
</label>
|
||||
<Input
|
||||
type="email"
|
||||
value={companyData.contact.email}
|
||||
onChange={(e) => handleContactChange('email', e.target.value)}
|
||||
placeholder="contacto@panaderia.com"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Sitio web (opcional)
|
||||
</label>
|
||||
<Input
|
||||
type="url"
|
||||
value={companyData.contact.website || ''}
|
||||
onChange={(e) => handleContactChange('website', e.target.value)}
|
||||
placeholder="https://www.panaderia.com"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 mb-2">Resumen</h4>
|
||||
<div className="text-sm text-gray-600 space-y-1">
|
||||
<p><strong>Panadería:</strong> {companyData.name || 'Sin especificar'}</p>
|
||||
<p><strong>Tipo:</strong> {BAKERY_TYPES.find(t => t.value === companyData.type)?.label}</p>
|
||||
<p><strong>Tamaño:</strong> {BAKERY_SIZES.find(s => s.value === companyData.size)?.label}</p>
|
||||
<p><strong>Especialidades:</strong> {companyData.specialties?.length || 0} seleccionadas</p>
|
||||
<p><strong>Ubicaciones:</strong> {companyData.locations}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompanyInfoStep;
|
||||
@@ -1,255 +0,0 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { Card, Button, Input, Select, Badge } from '../../ui';
|
||||
|
||||
export interface OnboardingStep {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
component: React.ComponentType<OnboardingStepProps>;
|
||||
isCompleted?: boolean;
|
||||
isRequired?: boolean;
|
||||
validation?: (data: any) => string | null;
|
||||
}
|
||||
|
||||
export interface OnboardingStepProps {
|
||||
data: any;
|
||||
onDataChange: (data: any) => void;
|
||||
onNext: () => void;
|
||||
onPrevious: () => void;
|
||||
isFirstStep: boolean;
|
||||
isLastStep: boolean;
|
||||
}
|
||||
|
||||
interface OnboardingWizardProps {
|
||||
steps: OnboardingStep[];
|
||||
onComplete: (data: any) => void;
|
||||
onExit?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const OnboardingWizard: React.FC<OnboardingWizardProps> = ({
|
||||
steps,
|
||||
onComplete,
|
||||
onExit,
|
||||
className = '',
|
||||
}) => {
|
||||
const [currentStepIndex, setCurrentStepIndex] = useState(0);
|
||||
const [stepData, setStepData] = useState<Record<string, any>>({});
|
||||
const [completedSteps, setCompletedSteps] = useState<Set<string>>(new Set());
|
||||
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const currentStep = steps[currentStepIndex];
|
||||
|
||||
const updateStepData = useCallback((stepId: string, data: any) => {
|
||||
setStepData(prev => ({
|
||||
...prev,
|
||||
[stepId]: { ...prev[stepId], ...data }
|
||||
}));
|
||||
|
||||
// Clear validation error for this step
|
||||
setValidationErrors(prev => {
|
||||
const newErrors = { ...prev };
|
||||
delete newErrors[stepId];
|
||||
return newErrors;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const validateCurrentStep = useCallback(() => {
|
||||
const step = currentStep;
|
||||
const data = stepData[step.id] || {};
|
||||
|
||||
if (step.validation) {
|
||||
const error = step.validation(data);
|
||||
if (error) {
|
||||
setValidationErrors(prev => ({
|
||||
...prev,
|
||||
[step.id]: error
|
||||
}));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Mark step as completed
|
||||
setCompletedSteps(prev => new Set(prev).add(step.id));
|
||||
return true;
|
||||
}, [currentStep, stepData]);
|
||||
|
||||
const goToNextStep = useCallback(() => {
|
||||
if (validateCurrentStep()) {
|
||||
if (currentStepIndex < steps.length - 1) {
|
||||
setCurrentStepIndex(currentStepIndex + 1);
|
||||
} else {
|
||||
// All steps completed, call onComplete with all data
|
||||
onComplete(stepData);
|
||||
}
|
||||
}
|
||||
}, [currentStepIndex, steps.length, validateCurrentStep, onComplete, stepData]);
|
||||
|
||||
const goToPreviousStep = useCallback(() => {
|
||||
if (currentStepIndex > 0) {
|
||||
setCurrentStepIndex(currentStepIndex - 1);
|
||||
}
|
||||
}, [currentStepIndex]);
|
||||
|
||||
const goToStep = useCallback((stepIndex: number) => {
|
||||
setCurrentStepIndex(stepIndex);
|
||||
}, []);
|
||||
|
||||
const calculateProgress = () => {
|
||||
return (completedSteps.size / steps.length) * 100;
|
||||
};
|
||||
|
||||
const renderStepIndicator = () => (
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
{steps.map((step, index) => {
|
||||
const isCompleted = completedSteps.has(step.id);
|
||||
const isCurrent = index === currentStepIndex;
|
||||
const hasError = validationErrors[step.id];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={step.id}
|
||||
className="flex items-center flex-1"
|
||||
>
|
||||
<button
|
||||
onClick={() => goToStep(index)}
|
||||
disabled={index > currentStepIndex + 1}
|
||||
className={`flex items-center justify-center w-8 h-8 rounded-full text-sm font-medium transition-colors ${
|
||||
isCompleted
|
||||
? 'bg-green-500 text-white'
|
||||
: isCurrent
|
||||
? hasError
|
||||
? 'bg-red-500 text-white'
|
||||
: 'bg-blue-500 text-white'
|
||||
: 'bg-gray-200 text-gray-600 hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
{isCompleted ? '✓' : index + 1}
|
||||
</button>
|
||||
|
||||
<div className="ml-3 flex-1">
|
||||
<p className={`text-sm font-medium ${isCurrent ? 'text-blue-600' : 'text-gray-600'}`}>
|
||||
{step.title}
|
||||
{step.isRequired && <span className="text-red-500 ml-1">*</span>}
|
||||
</p>
|
||||
{hasError && (
|
||||
<p className="text-xs text-red-600 mt-1">{hasError}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{index < steps.length - 1 && (
|
||||
<div className={`flex-1 h-0.5 mx-4 ${
|
||||
isCompleted ? 'bg-green-500' : 'bg-gray-200'
|
||||
}`} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderProgressBar = () => (
|
||||
<div className="mb-6">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
Progreso del onboarding
|
||||
</span>
|
||||
<span className="text-sm text-gray-500">
|
||||
{completedSteps.size} de {steps.length} completados
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-500 h-2 rounded-full transition-all duration-500"
|
||||
style={{ width: `${calculateProgress()}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!currentStep) {
|
||||
return (
|
||||
<Card className={`p-8 text-center ${className}`}>
|
||||
<p className="text-gray-500">No hay pasos de onboarding configurados.</p>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const StepComponent = currentStep.component;
|
||||
|
||||
return (
|
||||
<div className={`max-w-4xl mx-auto ${className}`}>
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">
|
||||
Configuración inicial
|
||||
</h1>
|
||||
<p className="text-gray-600 mt-1">
|
||||
Completa estos pasos para comenzar a usar la plataforma
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{onExit && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={onExit}
|
||||
className="text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
✕ Salir
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{renderProgressBar()}
|
||||
{renderStepIndicator()}
|
||||
|
||||
{/* Current Step Content */}
|
||||
<Card className="p-6">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
{currentStep.title}
|
||||
</h2>
|
||||
<p className="text-gray-600">
|
||||
{currentStep.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<StepComponent
|
||||
data={stepData[currentStep.id] || {}}
|
||||
onDataChange={(data) => updateStepData(currentStep.id, data)}
|
||||
onNext={goToNextStep}
|
||||
onPrevious={goToPreviousStep}
|
||||
isFirstStep={currentStepIndex === 0}
|
||||
isLastStep={currentStepIndex === steps.length - 1}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex justify-between items-center mt-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={goToPreviousStep}
|
||||
disabled={currentStepIndex === 0}
|
||||
>
|
||||
← Anterior
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-500">
|
||||
Paso {currentStepIndex + 1} de {steps.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={goToNextStep}
|
||||
>
|
||||
{currentStepIndex === steps.length - 1 ? 'Finalizar' : 'Siguiente →'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OnboardingWizard;
|
||||
@@ -1,556 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Card, Button, Input, Select, Badge } from '../../ui';
|
||||
import type { OnboardingStepProps } from './OnboardingWizard';
|
||||
|
||||
interface SystemConfig {
|
||||
timezone: string;
|
||||
currency: string;
|
||||
language: string;
|
||||
date_format: string;
|
||||
number_format: string;
|
||||
working_hours: {
|
||||
start: string;
|
||||
end: string;
|
||||
days: number[];
|
||||
};
|
||||
notifications: {
|
||||
email_enabled: boolean;
|
||||
sms_enabled: boolean;
|
||||
push_enabled: boolean;
|
||||
alert_preferences: string[];
|
||||
};
|
||||
integrations: {
|
||||
pos_system?: string;
|
||||
accounting_software?: string;
|
||||
payment_provider?: string;
|
||||
};
|
||||
features: {
|
||||
inventory_management: boolean;
|
||||
production_planning: boolean;
|
||||
sales_analytics: boolean;
|
||||
customer_management: boolean;
|
||||
financial_reporting: boolean;
|
||||
quality_control: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const TIMEZONES = [
|
||||
{ value: 'Europe/Madrid', label: 'Madrid (GMT+1)' },
|
||||
{ value: 'Europe/London', label: 'London (GMT+0)' },
|
||||
{ value: 'Europe/Paris', label: 'Paris (GMT+1)' },
|
||||
{ value: 'America/New_York', label: 'New York (GMT-5)' },
|
||||
{ value: 'America/Los_Angeles', label: 'Los Angeles (GMT-8)' },
|
||||
{ value: 'America/Mexico_City', label: 'Mexico City (GMT-6)' },
|
||||
{ value: 'America/Buenos_Aires', label: 'Buenos Aires (GMT-3)' },
|
||||
];
|
||||
|
||||
const CURRENCIES = [
|
||||
{ value: 'EUR', label: 'Euro (€)', symbol: '€' },
|
||||
{ value: 'USD', label: 'US Dollar ($)', symbol: '$' },
|
||||
{ value: 'GBP', label: 'British Pound (£)', symbol: '£' },
|
||||
{ value: 'MXN', label: 'Mexican Peso ($)', symbol: '$' },
|
||||
{ value: 'ARS', label: 'Argentine Peso ($)', symbol: '$' },
|
||||
{ value: 'COP', label: 'Colombian Peso ($)', symbol: '$' },
|
||||
];
|
||||
|
||||
const LANGUAGES = [
|
||||
{ value: 'es', label: 'Español' },
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'fr', label: 'Français' },
|
||||
{ value: 'pt', label: 'Português' },
|
||||
{ value: 'it', label: 'Italiano' },
|
||||
];
|
||||
|
||||
const DAYS_OF_WEEK = [
|
||||
{ value: 1, label: 'Lunes', short: 'L' },
|
||||
{ value: 2, label: 'Martes', short: 'M' },
|
||||
{ value: 3, label: 'Miércoles', short: 'X' },
|
||||
{ value: 4, label: 'Jueves', short: 'J' },
|
||||
{ value: 5, label: 'Viernes', short: 'V' },
|
||||
{ value: 6, label: 'Sábado', short: 'S' },
|
||||
{ value: 0, label: 'Domingo', short: 'D' },
|
||||
];
|
||||
|
||||
const ALERT_TYPES = [
|
||||
{ value: 'low_stock', label: 'Stock bajo', description: 'Cuando los ingredientes están por debajo del mínimo' },
|
||||
{ value: 'production_delays', label: 'Retrasos de producción', description: 'Cuando los lotes se retrasan' },
|
||||
{ value: 'quality_issues', label: 'Problemas de calidad', description: 'Cuando se detectan problemas de calidad' },
|
||||
{ value: 'financial_targets', label: 'Objetivos financieros', description: 'Cuando se alcanzan o no se cumplen objetivos' },
|
||||
{ value: 'equipment_maintenance', label: 'Mantenimiento de equipos', description: 'Recordatorios de mantenimiento' },
|
||||
{ value: 'food_safety', label: 'Seguridad alimentaria', description: 'Alertas relacionadas con seguridad alimentaria' },
|
||||
];
|
||||
|
||||
const FEATURES = [
|
||||
{
|
||||
key: 'inventory_management',
|
||||
title: 'Gestión de inventario',
|
||||
description: 'Control de stock, ingredientes y materias primas',
|
||||
recommended: true,
|
||||
},
|
||||
{
|
||||
key: 'production_planning',
|
||||
title: 'Planificación de producción',
|
||||
description: 'Programación de lotes y gestión de recetas',
|
||||
recommended: true,
|
||||
},
|
||||
{
|
||||
key: 'sales_analytics',
|
||||
title: 'Analytics de ventas',
|
||||
description: 'Reportes y análisis de ventas y tendencias',
|
||||
recommended: true,
|
||||
},
|
||||
{
|
||||
key: 'customer_management',
|
||||
title: 'Gestión de clientes',
|
||||
description: 'Base de datos de clientes y programa de fidelización',
|
||||
recommended: false,
|
||||
},
|
||||
{
|
||||
key: 'financial_reporting',
|
||||
title: 'Reportes financieros',
|
||||
description: 'Análisis de costos, márgenes y rentabilidad',
|
||||
recommended: true,
|
||||
},
|
||||
{
|
||||
key: 'quality_control',
|
||||
title: 'Control de calidad',
|
||||
description: 'Seguimiento de calidad y estándares',
|
||||
recommended: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const SystemSetupStep: React.FC<OnboardingStepProps> = ({
|
||||
data,
|
||||
onDataChange,
|
||||
}) => {
|
||||
const systemData: SystemConfig = {
|
||||
timezone: 'Europe/Madrid',
|
||||
currency: 'EUR',
|
||||
language: 'es',
|
||||
date_format: 'DD/MM/YYYY',
|
||||
number_format: 'European',
|
||||
working_hours: {
|
||||
start: '06:00',
|
||||
end: '20:00',
|
||||
days: [1, 2, 3, 4, 5, 6], // Monday to Saturday
|
||||
},
|
||||
notifications: {
|
||||
email_enabled: true,
|
||||
sms_enabled: false,
|
||||
push_enabled: true,
|
||||
alert_preferences: ['low_stock', 'production_delays', 'quality_issues'],
|
||||
},
|
||||
integrations: {},
|
||||
features: {
|
||||
inventory_management: true,
|
||||
production_planning: true,
|
||||
sales_analytics: true,
|
||||
customer_management: false,
|
||||
financial_reporting: true,
|
||||
quality_control: true,
|
||||
},
|
||||
...data,
|
||||
};
|
||||
|
||||
const handleInputChange = (field: string, value: any) => {
|
||||
onDataChange({
|
||||
...systemData,
|
||||
[field]: value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleWorkingHoursChange = (field: string, value: any) => {
|
||||
onDataChange({
|
||||
...systemData,
|
||||
working_hours: {
|
||||
...systemData.working_hours,
|
||||
[field]: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleNotificationChange = (field: string, value: any) => {
|
||||
onDataChange({
|
||||
...systemData,
|
||||
notifications: {
|
||||
...systemData.notifications,
|
||||
[field]: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleIntegrationChange = (field: string, value: string) => {
|
||||
onDataChange({
|
||||
...systemData,
|
||||
integrations: {
|
||||
...systemData.integrations,
|
||||
[field]: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleFeatureToggle = (feature: string) => {
|
||||
onDataChange({
|
||||
...systemData,
|
||||
features: {
|
||||
...systemData.features,
|
||||
[feature]: !systemData.features[feature as keyof typeof systemData.features],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const toggleWorkingDay = (day: number) => {
|
||||
const currentDays = systemData.working_hours.days;
|
||||
const updatedDays = currentDays.includes(day)
|
||||
? currentDays.filter(d => d !== day)
|
||||
: [...currentDays, day].sort();
|
||||
|
||||
handleWorkingHoursChange('days', updatedDays);
|
||||
};
|
||||
|
||||
const toggleAlertPreference = (alert: string) => {
|
||||
const current = systemData.notifications.alert_preferences;
|
||||
const updated = current.includes(alert)
|
||||
? current.filter(a => a !== alert)
|
||||
: [...current, alert];
|
||||
|
||||
handleNotificationChange('alert_preferences', updated);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Regional Settings */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 border-b pb-2">
|
||||
Configuración regional
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Zona horaria *
|
||||
</label>
|
||||
<Select
|
||||
value={systemData.timezone}
|
||||
onChange={(e) => handleInputChange('timezone', e.target.value)}
|
||||
className="w-full"
|
||||
>
|
||||
{TIMEZONES.map((tz) => (
|
||||
<option key={tz.value} value={tz.value}>
|
||||
{tz.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Moneda *
|
||||
</label>
|
||||
<Select
|
||||
value={systemData.currency}
|
||||
onChange={(e) => handleInputChange('currency', e.target.value)}
|
||||
className="w-full"
|
||||
>
|
||||
{CURRENCIES.map((currency) => (
|
||||
<option key={currency.value} value={currency.value}>
|
||||
{currency.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Idioma *
|
||||
</label>
|
||||
<Select
|
||||
value={systemData.language}
|
||||
onChange={(e) => handleInputChange('language', e.target.value)}
|
||||
className="w-full"
|
||||
>
|
||||
{LANGUAGES.map((lang) => (
|
||||
<option key={lang.value} value={lang.value}>
|
||||
{lang.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Formato de fecha *
|
||||
</label>
|
||||
<Select
|
||||
value={systemData.date_format}
|
||||
onChange={(e) => handleInputChange('date_format', e.target.value)}
|
||||
className="w-full"
|
||||
>
|
||||
<option value="DD/MM/YYYY">DD/MM/YYYY</option>
|
||||
<option value="MM/DD/YYYY">MM/DD/YYYY</option>
|
||||
<option value="YYYY-MM-DD">YYYY-MM-DD</option>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Working Hours */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 border-b pb-2">
|
||||
Horario de trabajo
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Hora de apertura *
|
||||
</label>
|
||||
<Input
|
||||
type="time"
|
||||
value={systemData.working_hours.start}
|
||||
onChange={(e) => handleWorkingHoursChange('start', e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Hora de cierre *
|
||||
</label>
|
||||
<Input
|
||||
type="time"
|
||||
value={systemData.working_hours.end}
|
||||
onChange={(e) => handleWorkingHoursChange('end', e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Días de operación *
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{DAYS_OF_WEEK.map((day) => (
|
||||
<button
|
||||
key={day.value}
|
||||
onClick={() => toggleWorkingDay(day.value)}
|
||||
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
systemData.working_hours.days.includes(day.value)
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
{day.short} - {day.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 border-b pb-2">
|
||||
Módulos y características
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600">
|
||||
Selecciona las características que quieres activar. Podrás cambiar esto más tarde.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{FEATURES.map((feature) => (
|
||||
<Card
|
||||
key={feature.key}
|
||||
className={`p-4 cursor-pointer transition-colors ${
|
||||
systemData.features[feature.key as keyof typeof systemData.features]
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
onClick={() => handleFeatureToggle(feature.key)}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h4 className="font-medium text-gray-900">{feature.title}</h4>
|
||||
{feature.recommended && (
|
||||
<Badge className="bg-green-100 text-green-800 text-xs">
|
||||
Recomendado
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">{feature.description}</p>
|
||||
</div>
|
||||
|
||||
<div className={`w-5 h-5 rounded border-2 flex items-center justify-center ${
|
||||
systemData.features[feature.key as keyof typeof systemData.features]
|
||||
? 'border-blue-500 bg-blue-500'
|
||||
: 'border-gray-300'
|
||||
}`}>
|
||||
{systemData.features[feature.key as keyof typeof systemData.features] && (
|
||||
<span className="text-white text-xs">✓</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notifications */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 border-b pb-2">
|
||||
Notificaciones
|
||||
</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={systemData.notifications.email_enabled}
|
||||
onChange={(e) => handleNotificationChange('email_enabled', e.target.checked)}
|
||||
className="mr-2 rounded"
|
||||
/>
|
||||
Notificaciones por email
|
||||
</label>
|
||||
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={systemData.notifications.push_enabled}
|
||||
onChange={(e) => handleNotificationChange('push_enabled', e.target.checked)}
|
||||
className="mr-2 rounded"
|
||||
/>
|
||||
Notificaciones push
|
||||
</label>
|
||||
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={systemData.notifications.sms_enabled}
|
||||
onChange={(e) => handleNotificationChange('sms_enabled', e.target.checked)}
|
||||
className="mr-2 rounded"
|
||||
/>
|
||||
Notificaciones por SMS
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Tipos de alertas que deseas recibir
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
{ALERT_TYPES.map((alert) => (
|
||||
<div
|
||||
key={alert.value}
|
||||
className={`p-3 border rounded-lg cursor-pointer transition-colors ${
|
||||
systemData.notifications.alert_preferences.includes(alert.value)
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
onClick={() => toggleAlertPreference(alert.value)}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h5 className="font-medium text-gray-900">{alert.label}</h5>
|
||||
<p className="text-sm text-gray-600">{alert.description}</p>
|
||||
</div>
|
||||
<div className={`w-5 h-5 rounded border-2 flex items-center justify-center ${
|
||||
systemData.notifications.alert_preferences.includes(alert.value)
|
||||
? 'border-blue-500 bg-blue-500'
|
||||
: 'border-gray-300'
|
||||
}`}>
|
||||
{systemData.notifications.alert_preferences.includes(alert.value) && (
|
||||
<span className="text-white text-xs">✓</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Integrations */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 border-b pb-2">
|
||||
Integraciones (opcional)
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600">
|
||||
Estas integraciones se pueden configurar más tarde desde el panel de administración.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Sistema POS
|
||||
</label>
|
||||
<Select
|
||||
value={systemData.integrations.pos_system || ''}
|
||||
onChange={(e) => handleIntegrationChange('pos_system', e.target.value)}
|
||||
className="w-full"
|
||||
>
|
||||
<option value="">Seleccionar...</option>
|
||||
<option value="square">Square</option>
|
||||
<option value="shopify">Shopify POS</option>
|
||||
<option value="toast">Toast</option>
|
||||
<option value="lightspeed">Lightspeed</option>
|
||||
<option value="other">Otro</option>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Software de contabilidad
|
||||
</label>
|
||||
<Select
|
||||
value={systemData.integrations.accounting_software || ''}
|
||||
onChange={(e) => handleIntegrationChange('accounting_software', e.target.value)}
|
||||
className="w-full"
|
||||
>
|
||||
<option value="">Seleccionar...</option>
|
||||
<option value="sage">Sage</option>
|
||||
<option value="contaplus">ContaPlus</option>
|
||||
<option value="a3">A3 Software</option>
|
||||
<option value="quickbooks">QuickBooks</option>
|
||||
<option value="other">Otro</option>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Proveedor de pagos
|
||||
</label>
|
||||
<Select
|
||||
value={systemData.integrations.payment_provider || ''}
|
||||
onChange={(e) => handleIntegrationChange('payment_provider', e.target.value)}
|
||||
className="w-full"
|
||||
>
|
||||
<option value="">Seleccionar...</option>
|
||||
<option value="stripe">Stripe</option>
|
||||
<option value="paypal">PayPal</option>
|
||||
<option value="redsys">Redsys</option>
|
||||
<option value="bizum">Bizum</option>
|
||||
<option value="other">Otro</option>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 mb-2">Configuración seleccionada</h4>
|
||||
<div className="text-sm text-gray-600 space-y-1">
|
||||
<p><strong>Zona horaria:</strong> {TIMEZONES.find(tz => tz.value === systemData.timezone)?.label}</p>
|
||||
<p><strong>Moneda:</strong> {CURRENCIES.find(c => c.value === systemData.currency)?.label}</p>
|
||||
<p><strong>Horario:</strong> {systemData.working_hours.start} - {systemData.working_hours.end}</p>
|
||||
<p><strong>Días operativos:</strong> {systemData.working_hours.days.length} días por semana</p>
|
||||
<p><strong>Módulos activados:</strong> {Object.values(systemData.features).filter(Boolean).length} de {Object.keys(systemData.features).length}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemSetupStep;
|
||||
Reference in New Issue
Block a user