Improve frontend 5
This commit is contained in:
@@ -1,66 +1,76 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Store, MapPin, Clock, Phone, Mail, Globe, Save, RotateCcw } from 'lucide-react';
|
||||
import { Button, Card, Input } from '../../../../components/ui';
|
||||
import { Store, MapPin, Clock, Phone, Mail, Globe, Save, X, Edit3 } from 'lucide-react';
|
||||
import { Button, Card, Input, Select } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { useToast } from '../../../../hooks/ui/useToast';
|
||||
|
||||
interface BakeryConfig {
|
||||
// General Info
|
||||
name: string;
|
||||
description: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
website: string;
|
||||
// Location
|
||||
address: string;
|
||||
city: string;
|
||||
postalCode: string;
|
||||
country: string;
|
||||
// Business
|
||||
taxId: string;
|
||||
currency: string;
|
||||
timezone: string;
|
||||
language: string;
|
||||
}
|
||||
|
||||
interface BusinessHours {
|
||||
[key: string]: {
|
||||
open: string;
|
||||
close: string;
|
||||
closed: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const BakeryConfigPage: React.FC = () => {
|
||||
const [config, setConfig] = useState({
|
||||
general: {
|
||||
name: 'Panadería Artesanal San Miguel',
|
||||
description: 'Panadería tradicional con más de 30 años de experiencia',
|
||||
logo: '',
|
||||
website: 'https://panaderiasanmiguel.com',
|
||||
email: 'info@panaderiasanmiguel.com',
|
||||
phone: '+34 912 345 678'
|
||||
},
|
||||
location: {
|
||||
address: 'Calle Mayor 123',
|
||||
city: 'Madrid',
|
||||
postalCode: '28001',
|
||||
country: 'España',
|
||||
coordinates: {
|
||||
lat: 40.4168,
|
||||
lng: -3.7038
|
||||
}
|
||||
},
|
||||
schedule: {
|
||||
monday: { open: '07:00', close: '20:00', closed: false },
|
||||
tuesday: { open: '07:00', close: '20:00', closed: false },
|
||||
wednesday: { open: '07:00', close: '20:00', closed: false },
|
||||
thursday: { open: '07:00', close: '20:00', closed: false },
|
||||
friday: { open: '07:00', close: '20:00', closed: false },
|
||||
saturday: { open: '08:00', close: '14:00', closed: false },
|
||||
sunday: { open: '09:00', close: '13:00', closed: false }
|
||||
},
|
||||
business: {
|
||||
taxId: 'B12345678',
|
||||
registrationNumber: 'REG-2024-001',
|
||||
licenseNumber: 'LIC-FOOD-2024',
|
||||
currency: 'EUR',
|
||||
timezone: 'Europe/Madrid',
|
||||
language: 'es'
|
||||
},
|
||||
preferences: {
|
||||
enableOnlineOrders: true,
|
||||
enableReservations: false,
|
||||
enableDelivery: true,
|
||||
deliveryRadius: 5,
|
||||
minimumOrderAmount: 15.00,
|
||||
enableLoyaltyProgram: true,
|
||||
autoBackup: true,
|
||||
emailNotifications: true,
|
||||
smsNotifications: false
|
||||
}
|
||||
const { showToast } = useToast();
|
||||
|
||||
const [activeTab, setActiveTab] = useState<'general' | 'location' | 'business' | 'hours'>('general');
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [config, setConfig] = useState<BakeryConfig>({
|
||||
name: 'Panadería Artesanal San Miguel',
|
||||
description: 'Panadería tradicional con más de 30 años de experiencia',
|
||||
email: 'info@panaderiasanmiguel.com',
|
||||
phone: '+34 912 345 678',
|
||||
website: 'https://panaderiasanmiguel.com',
|
||||
address: 'Calle Mayor 123',
|
||||
city: 'Madrid',
|
||||
postalCode: '28001',
|
||||
country: 'España',
|
||||
taxId: 'B12345678',
|
||||
currency: 'EUR',
|
||||
timezone: 'Europe/Madrid',
|
||||
language: 'es'
|
||||
});
|
||||
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('general');
|
||||
const [businessHours, setBusinessHours] = useState<BusinessHours>({
|
||||
monday: { open: '07:00', close: '20:00', closed: false },
|
||||
tuesday: { open: '07:00', close: '20:00', closed: false },
|
||||
wednesday: { open: '07:00', close: '20:00', closed: false },
|
||||
thursday: { open: '07:00', close: '20:00', closed: false },
|
||||
friday: { open: '07:00', close: '20:00', closed: false },
|
||||
saturday: { open: '08:00', close: '14:00', closed: false },
|
||||
sunday: { open: '09:00', close: '13:00', closed: false }
|
||||
});
|
||||
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const tabs = [
|
||||
{ id: 'general', label: 'General', icon: Store },
|
||||
{ id: 'location', label: 'Ubicación', icon: MapPin },
|
||||
{ id: 'schedule', label: 'Horarios', icon: Clock },
|
||||
{ id: 'business', label: 'Empresa', icon: Globe }
|
||||
{ id: 'general' as const, label: 'General', icon: Store },
|
||||
{ id: 'location' as const, label: 'Ubicación', icon: MapPin },
|
||||
{ id: 'business' as const, label: 'Empresa', icon: Globe },
|
||||
{ id: 'hours' as const, label: 'Horarios', icon: Clock }
|
||||
];
|
||||
|
||||
const daysOfWeek = [
|
||||
@@ -73,40 +83,94 @@ const BakeryConfigPage: React.FC = () => {
|
||||
{ key: 'sunday', label: 'Domingo' }
|
||||
];
|
||||
|
||||
const handleInputChange = (section: string, field: string, value: any) => {
|
||||
setConfig(prev => ({
|
||||
const currencyOptions = [
|
||||
{ value: 'EUR', label: 'EUR (€)' },
|
||||
{ value: 'USD', label: 'USD ($)' },
|
||||
{ value: 'GBP', label: 'GBP (£)' }
|
||||
];
|
||||
|
||||
const timezoneOptions = [
|
||||
{ value: 'Europe/Madrid', label: 'Madrid (CET/CEST)' },
|
||||
{ value: 'Atlantic/Canary', label: 'Canarias (WET/WEST)' },
|
||||
{ value: 'Europe/London', label: 'Londres (GMT/BST)' }
|
||||
];
|
||||
|
||||
const languageOptions = [
|
||||
{ value: 'es', label: 'Español' },
|
||||
{ value: 'ca', label: 'Català' },
|
||||
{ value: 'en', label: 'English' }
|
||||
];
|
||||
|
||||
const validateConfig = (): boolean => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
if (!config.name.trim()) {
|
||||
newErrors.name = 'El nombre es requerido';
|
||||
}
|
||||
|
||||
if (!config.email.trim()) {
|
||||
newErrors.email = 'El email es requerido';
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(config.email)) {
|
||||
newErrors.email = 'Email inválido';
|
||||
}
|
||||
|
||||
if (!config.address.trim()) {
|
||||
newErrors.address = 'La dirección es requerida';
|
||||
}
|
||||
|
||||
if (!config.city.trim()) {
|
||||
newErrors.city = 'La ciudad es requerida';
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSaveConfig = async () => {
|
||||
if (!validateConfig()) return;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
setIsEditing(false);
|
||||
showToast({
|
||||
type: 'success',
|
||||
title: 'Configuración actualizada',
|
||||
message: 'Los datos de la panadería han sido guardados correctamente'
|
||||
});
|
||||
} catch (error) {
|
||||
showToast({
|
||||
type: 'error',
|
||||
title: 'Error',
|
||||
message: 'No se pudo actualizar la configuración'
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (field: keyof BakeryConfig) => (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
setConfig(prev => ({ ...prev, [field]: e.target.value }));
|
||||
if (errors[field]) {
|
||||
setErrors(prev => ({ ...prev, [field]: '' }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectChange = (field: keyof BakeryConfig) => (value: string) => {
|
||||
setConfig(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleHoursChange = (day: string, field: 'open' | 'close' | 'closed', value: string | boolean) => {
|
||||
setBusinessHours(prev => ({
|
||||
...prev,
|
||||
[section]: {
|
||||
...prev[section as keyof typeof prev],
|
||||
[day]: {
|
||||
...prev[day],
|
||||
[field]: value
|
||||
}
|
||||
}));
|
||||
setHasChanges(true);
|
||||
};
|
||||
|
||||
const handleScheduleChange = (day: string, field: string, value: any) => {
|
||||
setConfig(prev => ({
|
||||
...prev,
|
||||
schedule: {
|
||||
...prev.schedule,
|
||||
[day]: {
|
||||
...prev.schedule[day as keyof typeof prev.schedule],
|
||||
[field]: value
|
||||
}
|
||||
}
|
||||
}));
|
||||
setHasChanges(true);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
// Handle save logic
|
||||
console.log('Saving bakery config:', config);
|
||||
setHasChanges(false);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
// Reset to defaults
|
||||
setHasChanges(false);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -114,366 +178,302 @@ const BakeryConfigPage: React.FC = () => {
|
||||
<PageHeader
|
||||
title="Configuración de Panadería"
|
||||
description="Configura los datos básicos y preferencias de tu panadería"
|
||||
action={
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" onClick={handleReset}>
|
||||
<RotateCcw className="w-4 h-4 mr-2" />
|
||||
Restaurar
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={!hasChanges}>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
Guardar Cambios
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-6">
|
||||
{/* Sidebar */}
|
||||
<div className="w-full lg:w-64">
|
||||
<Card className="p-4">
|
||||
<nav className="space-y-2">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`w-full flex items-center space-x-3 px-3 py-2 text-left rounded-lg transition-colors ${
|
||||
activeTab === tab.id
|
||||
? 'bg-[var(--color-info)]/10 text-[var(--color-info)]'
|
||||
: 'text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)]'
|
||||
}`}
|
||||
>
|
||||
<tab.icon className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">{tab.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</Card>
|
||||
{/* Bakery Header */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] rounded-lg flex items-center justify-center text-white font-bold text-xl">
|
||||
{config.name.charAt(0)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h1 className="text-2xl font-bold text-text-primary mb-1">
|
||||
{config.name}
|
||||
</h1>
|
||||
<p className="text-text-secondary">{config.email}</p>
|
||||
<p className="text-text-tertiary text-sm">{config.address}, {config.city}</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{!isEditing && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsEditing(true)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Edit3 className="w-4 h-4" />
|
||||
Editar Configuración
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Configuration Tabs */}
|
||||
<Card className="overflow-hidden">
|
||||
{/* Tab Navigation */}
|
||||
<div className="border-b border-border-primary">
|
||||
<nav className="flex">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`flex items-center gap-2 px-6 py-4 text-sm font-medium transition-colors ${
|
||||
activeTab === tab.id
|
||||
? 'text-color-primary border-b-2 border-color-primary bg-color-primary/5'
|
||||
: 'text-text-secondary hover:text-text-primary hover:bg-bg-secondary'
|
||||
}`}
|
||||
>
|
||||
<tab.icon className="w-4 h-4" />
|
||||
<span>{tab.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1">
|
||||
{/* Tab Content */}
|
||||
<div className="p-6">
|
||||
{activeTab === 'general' && (
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6">Información General</h3>
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Nombre de la Panadería
|
||||
</label>
|
||||
<Input
|
||||
value={config.general.name}
|
||||
onChange={(e) => handleInputChange('general', 'name', e.target.value)}
|
||||
placeholder="Nombre de tu panadería"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Sitio Web
|
||||
</label>
|
||||
<Input
|
||||
value={config.general.website}
|
||||
onChange={(e) => handleInputChange('general', 'website', e.target.value)}
|
||||
placeholder="https://tu-panaderia.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Descripción
|
||||
</label>
|
||||
<textarea
|
||||
value={config.general.description}
|
||||
onChange={(e) => handleInputChange('general', 'description', e.target.value)}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
||||
placeholder="Describe tu panadería..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Email de Contacto
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[var(--text-tertiary)] h-4 w-4" />
|
||||
<Input
|
||||
value={config.general.email}
|
||||
onChange={(e) => handleInputChange('general', 'email', e.target.value)}
|
||||
className="pl-10"
|
||||
type="email"
|
||||
placeholder="contacto@panaderia.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Teléfono
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Phone className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[var(--text-tertiary)] h-4 w-4" />
|
||||
<Input
|
||||
value={config.general.phone}
|
||||
onChange={(e) => handleInputChange('general', 'phone', e.target.value)}
|
||||
className="pl-10"
|
||||
type="tel"
|
||||
placeholder="+34 912 345 678"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-lg font-semibold text-text-primary">Información General</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
<Input
|
||||
label="Nombre de la Panadería"
|
||||
value={config.name}
|
||||
onChange={handleInputChange('name')}
|
||||
error={errors.name}
|
||||
disabled={!isEditing || isLoading}
|
||||
placeholder="Nombre de tu panadería"
|
||||
leftIcon={<Store className="w-4 h-4" />}
|
||||
/>
|
||||
|
||||
<Input
|
||||
type="email"
|
||||
label="Email de Contacto"
|
||||
value={config.email}
|
||||
onChange={handleInputChange('email')}
|
||||
error={errors.email}
|
||||
disabled={!isEditing || isLoading}
|
||||
placeholder="contacto@panaderia.com"
|
||||
leftIcon={<Mail className="w-4 h-4" />}
|
||||
/>
|
||||
|
||||
<Input
|
||||
type="tel"
|
||||
label="Teléfono"
|
||||
value={config.phone}
|
||||
onChange={handleInputChange('phone')}
|
||||
error={errors.phone}
|
||||
disabled={!isEditing || isLoading}
|
||||
placeholder="+34 912 345 678"
|
||||
leftIcon={<Phone className="w-4 h-4" />}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Sitio Web"
|
||||
value={config.website}
|
||||
onChange={handleInputChange('website')}
|
||||
disabled={!isEditing || isLoading}
|
||||
placeholder="https://tu-panaderia.com"
|
||||
leftIcon={<Globe className="w-4 h-4" />}
|
||||
className="md:col-span-2 xl:col-span-3"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-2">
|
||||
Descripción
|
||||
</label>
|
||||
<textarea
|
||||
value={config.description}
|
||||
onChange={handleInputChange('description')}
|
||||
disabled={!isEditing || isLoading}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-[var(--border-primary)] rounded-lg resize-none bg-[var(--bg-primary)] text-[var(--text-primary)] disabled:bg-[var(--bg-secondary)] disabled:text-[var(--text-secondary)]"
|
||||
placeholder="Describe tu panadería..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'location' && (
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6">Ubicación</h3>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Dirección
|
||||
</label>
|
||||
<Input
|
||||
value={config.location.address}
|
||||
onChange={(e) => handleInputChange('location', 'address', e.target.value)}
|
||||
placeholder="Calle, número, etc."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Ciudad
|
||||
</label>
|
||||
<Input
|
||||
value={config.location.city}
|
||||
onChange={(e) => handleInputChange('location', 'city', e.target.value)}
|
||||
placeholder="Ciudad"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Código Postal
|
||||
</label>
|
||||
<Input
|
||||
value={config.location.postalCode}
|
||||
onChange={(e) => handleInputChange('location', 'postalCode', e.target.value)}
|
||||
placeholder="28001"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
País
|
||||
</label>
|
||||
<Input
|
||||
value={config.location.country}
|
||||
onChange={(e) => handleInputChange('location', 'country', e.target.value)}
|
||||
placeholder="España"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Latitud
|
||||
</label>
|
||||
<Input
|
||||
value={config.location.coordinates.lat}
|
||||
onChange={(e) => handleInputChange('location', 'coordinates', {
|
||||
...config.location.coordinates,
|
||||
lat: parseFloat(e.target.value) || 0
|
||||
})}
|
||||
type="number"
|
||||
step="0.000001"
|
||||
placeholder="40.4168"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Longitud
|
||||
</label>
|
||||
<Input
|
||||
value={config.location.coordinates.lng}
|
||||
onChange={(e) => handleInputChange('location', 'coordinates', {
|
||||
...config.location.coordinates,
|
||||
lng: parseFloat(e.target.value) || 0
|
||||
})}
|
||||
type="number"
|
||||
step="0.000001"
|
||||
placeholder="-3.7038"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-lg font-semibold text-text-primary">Ubicación</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
<Input
|
||||
label="Dirección"
|
||||
value={config.address}
|
||||
onChange={handleInputChange('address')}
|
||||
error={errors.address}
|
||||
disabled={!isEditing || isLoading}
|
||||
placeholder="Calle, número, etc."
|
||||
leftIcon={<MapPin className="w-4 h-4" />}
|
||||
className="md:col-span-2"
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Ciudad"
|
||||
value={config.city}
|
||||
onChange={handleInputChange('city')}
|
||||
error={errors.city}
|
||||
disabled={!isEditing || isLoading}
|
||||
placeholder="Ciudad"
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Código Postal"
|
||||
value={config.postalCode}
|
||||
onChange={handleInputChange('postalCode')}
|
||||
disabled={!isEditing || isLoading}
|
||||
placeholder="28001"
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="País"
|
||||
value={config.country}
|
||||
onChange={handleInputChange('country')}
|
||||
disabled={!isEditing || isLoading}
|
||||
placeholder="España"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'schedule' && (
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6">Horarios de Apertura</h3>
|
||||
{activeTab === 'business' && (
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-lg font-semibold text-text-primary">Datos de Empresa</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
<Input
|
||||
label="NIF/CIF"
|
||||
value={config.taxId}
|
||||
onChange={handleInputChange('taxId')}
|
||||
disabled={!isEditing || isLoading}
|
||||
placeholder="B12345678"
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="Moneda"
|
||||
options={currencyOptions}
|
||||
value={config.currency}
|
||||
onChange={handleSelectChange('currency')}
|
||||
disabled={!isEditing || isLoading}
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="Zona Horaria"
|
||||
options={timezoneOptions}
|
||||
value={config.timezone}
|
||||
onChange={handleSelectChange('timezone')}
|
||||
disabled={!isEditing || isLoading}
|
||||
leftIcon={<Clock className="w-4 h-4" />}
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="Idioma"
|
||||
options={languageOptions}
|
||||
value={config.language}
|
||||
onChange={handleSelectChange('language')}
|
||||
disabled={!isEditing || isLoading}
|
||||
leftIcon={<Globe className="w-4 h-4" />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'hours' && (
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-lg font-semibold text-text-primary">Horarios de Apertura</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
{daysOfWeek.map((day) => {
|
||||
const schedule = config.schedule[day.key as keyof typeof config.schedule];
|
||||
const hours = businessHours[day.key];
|
||||
return (
|
||||
<div key={day.key} className="flex items-center space-x-4 p-4 border rounded-lg">
|
||||
<div className="w-20">
|
||||
<span className="text-sm font-medium text-[var(--text-secondary)]">{day.label}</span>
|
||||
<div key={day.key} className="grid grid-cols-12 items-center gap-4 p-4 border border-border-primary rounded-lg">
|
||||
{/* Day Name */}
|
||||
<div className="col-span-2">
|
||||
<span className="text-sm font-medium text-text-secondary">{day.label}</span>
|
||||
</div>
|
||||
|
||||
<label className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={schedule.closed}
|
||||
onChange={(e) => handleScheduleChange(day.key, 'closed', e.target.checked)}
|
||||
className="rounded border-[var(--border-secondary)]"
|
||||
/>
|
||||
<span className="text-sm text-[var(--text-secondary)]">Cerrado</span>
|
||||
</label>
|
||||
{/* Closed Checkbox */}
|
||||
<div className="col-span-2">
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={hours.closed}
|
||||
onChange={(e) => handleHoursChange(day.key, 'closed', e.target.checked)}
|
||||
disabled={!isEditing || isLoading}
|
||||
className="rounded border-border-primary"
|
||||
/>
|
||||
<span className="text-sm text-text-secondary">Cerrado</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{!schedule.closed && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-xs text-[var(--text-tertiary)] mb-1">Apertura</label>
|
||||
<input
|
||||
type="time"
|
||||
value={schedule.open}
|
||||
onChange={(e) => handleScheduleChange(day.key, 'open', e.target.value)}
|
||||
className="px-3 py-1 border border-[var(--border-secondary)] rounded-md text-sm"
|
||||
/>
|
||||
{/* Time Inputs */}
|
||||
<div className="col-span-8 flex items-center gap-6">
|
||||
{!hours.closed ? (
|
||||
<>
|
||||
<div className="flex-1">
|
||||
<label className="block text-xs text-text-tertiary mb-1">Apertura</label>
|
||||
<input
|
||||
type="time"
|
||||
value={hours.open}
|
||||
onChange={(e) => handleHoursChange(day.key, 'open', e.target.value)}
|
||||
disabled={!isEditing || isLoading}
|
||||
className="w-full px-3 py-2 border border-[var(--border-primary)] rounded-lg text-sm bg-[var(--bg-primary)] text-[var(--text-primary)] disabled:bg-[var(--bg-secondary)] disabled:text-[var(--text-secondary)]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<label className="block text-xs text-text-tertiary mb-1">Cierre</label>
|
||||
<input
|
||||
type="time"
|
||||
value={hours.close}
|
||||
onChange={(e) => handleHoursChange(day.key, 'close', e.target.value)}
|
||||
disabled={!isEditing || isLoading}
|
||||
className="w-full px-3 py-2 border border-[var(--border-primary)] rounded-lg text-sm bg-[var(--bg-primary)] text-[var(--text-primary)] disabled:bg-[var(--bg-secondary)] disabled:text-[var(--text-secondary)]"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-sm text-text-tertiary italic">
|
||||
Cerrado todo el día
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-[var(--text-tertiary)] mb-1">Cierre</label>
|
||||
<input
|
||||
type="time"
|
||||
value={schedule.close}
|
||||
onChange={(e) => handleScheduleChange(day.key, 'close', e.target.value)}
|
||||
className="px-3 py-1 border border-[var(--border-secondary)] rounded-md text-sm"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{activeTab === 'business' && (
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6">Datos de Empresa</h3>
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
NIF/CIF
|
||||
</label>
|
||||
<Input
|
||||
value={config.business.taxId}
|
||||
onChange={(e) => handleInputChange('business', 'taxId', e.target.value)}
|
||||
placeholder="B12345678"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Número de Registro
|
||||
</label>
|
||||
<Input
|
||||
value={config.business.registrationNumber}
|
||||
onChange={(e) => handleInputChange('business', 'registrationNumber', e.target.value)}
|
||||
placeholder="REG-2024-001"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Licencia Sanitaria
|
||||
</label>
|
||||
<Input
|
||||
value={config.business.licenseNumber}
|
||||
onChange={(e) => handleInputChange('business', 'licenseNumber', e.target.value)}
|
||||
placeholder="LIC-FOOD-2024"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Moneda
|
||||
</label>
|
||||
<select
|
||||
value={config.business.currency}
|
||||
onChange={(e) => handleInputChange('business', 'currency', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
||||
>
|
||||
<option value="EUR">EUR (€)</option>
|
||||
<option value="USD">USD ($)</option>
|
||||
<option value="GBP">GBP (£)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Zona Horaria
|
||||
</label>
|
||||
<select
|
||||
value={config.business.timezone}
|
||||
onChange={(e) => handleInputChange('business', 'timezone', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
||||
>
|
||||
<option value="Europe/Madrid">Madrid (GMT+1)</option>
|
||||
<option value="Europe/London">Londres (GMT)</option>
|
||||
<option value="America/New_York">Nueva York (GMT-5)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Idioma
|
||||
</label>
|
||||
<select
|
||||
value={config.business.language}
|
||||
onChange={(e) => handleInputChange('business', 'language', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
||||
>
|
||||
<option value="es">Español</option>
|
||||
<option value="en">English</option>
|
||||
<option value="fr">Français</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Save Changes Banner */}
|
||||
{hasChanges && (
|
||||
<div className="fixed bottom-6 left-1/2 transform -translate-x-1/2 bg-blue-600 text-white px-6 py-3 rounded-lg shadow-lg flex items-center space-x-4">
|
||||
<span className="text-sm">Tienes cambios sin guardar</span>
|
||||
<div className="flex space-x-2">
|
||||
<Button size="sm" variant="outline" className="text-[var(--color-info)] bg-white" onClick={handleReset}>
|
||||
Descartar
|
||||
|
||||
{/* Save Actions */}
|
||||
{isEditing && (
|
||||
<div className="flex gap-3 px-6 py-4 bg-bg-secondary border-t border-border-primary">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsEditing(false)}
|
||||
disabled={isLoading}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button size="sm" className="bg-blue-700 hover:bg-blue-800" onClick={handleSave}>
|
||||
Guardar
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSaveConfig}
|
||||
isLoading={isLoading}
|
||||
loadingText="Guardando..."
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
Guardar Configuración
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,49 +1,173 @@
|
||||
import React, { useState } from 'react';
|
||||
import { User, Mail, Phone, MapPin, Building, Shield, Activity, Settings, Edit3, Lock, Bell, Download } from 'lucide-react';
|
||||
import { Button, Card, Badge, Avatar, Input, ProgressBar } from '../../../../components/ui';
|
||||
import { User, Mail, Phone, Lock, Globe, Clock, Camera, Save, X } from 'lucide-react';
|
||||
import { Button, Card, Avatar, Input, Select } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { ProfileSettings } from '../../../../components/domain/auth';
|
||||
import { useAuthUser } from '../../../../stores/auth.store';
|
||||
import { useToast } from '../../../../hooks/ui/useToast';
|
||||
|
||||
interface ProfileFormData {
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
language: string;
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
interface PasswordData {
|
||||
currentPassword: string;
|
||||
newPassword: string;
|
||||
confirmPassword: string;
|
||||
}
|
||||
|
||||
const ProfilePage: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState('profile');
|
||||
const user = useAuthUser();
|
||||
const { showToast } = useToast();
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [userInfo, setUserInfo] = useState({
|
||||
name: 'María González',
|
||||
email: 'maria.gonzalez@panaderia.com',
|
||||
phone: '+34 123 456 789',
|
||||
address: 'Calle Mayor 123, Madrid',
|
||||
bakery: 'Panadería La Tradicional',
|
||||
role: 'Propietario'
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showPasswordForm, setShowPasswordForm] = useState(false);
|
||||
|
||||
const [profileData, setProfileData] = useState<ProfileFormData>({
|
||||
first_name: 'María',
|
||||
last_name: 'González Pérez',
|
||||
email: 'admin@bakery.com',
|
||||
phone: '+34 612 345 678',
|
||||
language: 'es',
|
||||
timezone: 'Europe/Madrid'
|
||||
});
|
||||
|
||||
const mockProfileStats = {
|
||||
profileCompletion: 85,
|
||||
securityScore: 94,
|
||||
lastLogin: '2 horas',
|
||||
activeSessions: 2,
|
||||
twoFactorEnabled: false,
|
||||
passwordLastChanged: '2 meses'
|
||||
const [passwordData, setPasswordData] = useState<PasswordData>({
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
});
|
||||
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const languageOptions = [
|
||||
{ value: 'es', label: 'Español' },
|
||||
{ value: 'ca', label: 'Català' },
|
||||
{ value: 'en', label: 'English' }
|
||||
];
|
||||
|
||||
const timezoneOptions = [
|
||||
{ value: 'Europe/Madrid', label: 'Madrid (CET/CEST)' },
|
||||
{ value: 'Atlantic/Canary', label: 'Canarias (WET/WEST)' },
|
||||
{ value: 'Europe/London', label: 'Londres (GMT/BST)' }
|
||||
];
|
||||
|
||||
const validateProfile = (): boolean => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
if (!profileData.first_name.trim()) {
|
||||
newErrors.first_name = 'El nombre es requerido';
|
||||
}
|
||||
|
||||
if (!profileData.last_name.trim()) {
|
||||
newErrors.last_name = 'Los apellidos son requeridos';
|
||||
}
|
||||
|
||||
if (!profileData.email.trim()) {
|
||||
newErrors.email = 'El email es requerido';
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(profileData.email)) {
|
||||
newErrors.email = 'Email inválido';
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
setIsEditing(false);
|
||||
console.log('Profile updated:', userInfo);
|
||||
const validatePassword = (): boolean => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
if (!passwordData.currentPassword) {
|
||||
newErrors.currentPassword = 'Contraseña actual requerida';
|
||||
}
|
||||
|
||||
if (!passwordData.newPassword) {
|
||||
newErrors.newPassword = 'Nueva contraseña requerida';
|
||||
} else if (passwordData.newPassword.length < 8) {
|
||||
newErrors.newPassword = 'Mínimo 8 caracteres';
|
||||
}
|
||||
|
||||
if (passwordData.newPassword !== passwordData.confirmPassword) {
|
||||
newErrors.confirmPassword = 'Las contraseñas no coinciden';
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsEditing(false);
|
||||
const handleSaveProfile = async () => {
|
||||
if (!validateProfile()) return;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
setIsEditing(false);
|
||||
showToast({
|
||||
type: 'success',
|
||||
title: 'Perfil actualizado',
|
||||
message: 'Tu información ha sido guardada correctamente'
|
||||
});
|
||||
} catch (error) {
|
||||
showToast({
|
||||
type: 'error',
|
||||
title: 'Error',
|
||||
message: 'No se pudo actualizar tu perfil'
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnable2FA = () => {
|
||||
console.log('Enabling 2FA');
|
||||
const handleChangePassword = async () => {
|
||||
if (!validatePassword()) return;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
setShowPasswordForm(false);
|
||||
setPasswordData({ currentPassword: '', newPassword: '', confirmPassword: '' });
|
||||
showToast({
|
||||
type: 'success',
|
||||
title: 'Contraseña actualizada',
|
||||
message: 'Tu contraseña ha sido cambiada correctamente'
|
||||
});
|
||||
} catch (error) {
|
||||
showToast({
|
||||
type: 'error',
|
||||
title: 'Error',
|
||||
message: 'No se pudo cambiar tu contraseña'
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangePassword = () => {
|
||||
console.log('Change password');
|
||||
const handleInputChange = (field: keyof ProfileFormData) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setProfileData(prev => ({ ...prev, [field]: e.target.value }));
|
||||
if (errors[field]) {
|
||||
setErrors(prev => ({ ...prev, [field]: '' }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleManageSessions = () => {
|
||||
console.log('Manage sessions');
|
||||
const handleSelectChange = (field: keyof ProfileFormData) => (value: string) => {
|
||||
setProfileData(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handlePasswordChange = (field: keyof PasswordData) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPasswordData(prev => ({ ...prev, [field]: e.target.value }));
|
||||
if (errors[field]) {
|
||||
setErrors(prev => ({ ...prev, [field]: '' }));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -51,329 +175,199 @@ const ProfilePage: React.FC = () => {
|
||||
<PageHeader
|
||||
title="Mi Perfil"
|
||||
description="Gestiona tu información personal y configuración de cuenta"
|
||||
action={
|
||||
<Button onClick={() => setIsEditing(!isEditing)}>
|
||||
<Edit3 className="w-4 h-4 mr-2" />
|
||||
{isEditing ? 'Guardar Cambios' : 'Editar Perfil'}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Profile Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-4">
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Perfil Completado</p>
|
||||
<p className="text-2xl font-bold text-[var(--color-success)]">{mockProfileStats.profileCompletion}%</p>
|
||||
</div>
|
||||
<User className="h-8 w-8 text-[var(--color-success)]" />
|
||||
{/* Profile Header */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="relative">
|
||||
<Avatar
|
||||
src="https://images.unsplash.com/photo-1494790108755-2616b612b372?w=150&h=150&fit=crop&crop=face"
|
||||
name={`${profileData.first_name} ${profileData.last_name}`}
|
||||
size="xl"
|
||||
className="w-20 h-20"
|
||||
/>
|
||||
<button className="absolute -bottom-1 -right-1 bg-color-primary text-white rounded-full p-2 shadow-lg hover:bg-color-primary-dark transition-colors">
|
||||
<Camera className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Seguridad</p>
|
||||
<p className="text-2xl font-bold text-[var(--color-info)]">{mockProfileStats.securityScore}%</p>
|
||||
</div>
|
||||
<Shield className="h-8 w-8 text-[var(--color-info)]" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Último Acceso</p>
|
||||
<p className="text-2xl font-bold text-[var(--text-primary)]">{mockProfileStats.lastLogin}</p>
|
||||
</div>
|
||||
<Activity className="h-8 w-8 text-[var(--color-primary)]" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Sesiones</p>
|
||||
<p className="text-2xl font-bold text-purple-600">{mockProfileStats.activeSessions}</p>
|
||||
</div>
|
||||
<div className="h-8 w-8 bg-purple-100 rounded-full flex items-center justify-center">
|
||||
<Settings className="h-5 w-5 text-purple-600" />
|
||||
<div className="flex-1">
|
||||
<h1 className="text-2xl font-bold text-text-primary mb-1">
|
||||
{profileData.first_name} {profileData.last_name}
|
||||
</h1>
|
||||
<p className="text-text-secondary">{profileData.email}</p>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span className="text-sm text-text-tertiary">En línea</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">2FA</p>
|
||||
<p className="text-lg font-bold text-[var(--color-warning)]">{mockProfileStats.twoFactorEnabled ? 'Activo' : 'Pendiente'}</p>
|
||||
</div>
|
||||
<Lock className="h-8 w-8 text-[var(--color-warning)]" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Contraseña</p>
|
||||
<p className="text-lg font-bold text-indigo-600">{mockProfileStats.passwordLastChanged}</p>
|
||||
</div>
|
||||
<div className="h-8 w-8 bg-indigo-100 rounded-full flex items-center justify-center">
|
||||
<Shield className="h-5 w-5 text-indigo-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Tabs Navigation */}
|
||||
<div className="border-b border-[var(--border-primary)]">
|
||||
<nav className="-mb-px flex space-x-8">
|
||||
<button
|
||||
onClick={() => setActiveTab('profile')}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'profile'
|
||||
? 'border-orange-500 text-[var(--color-primary)]'
|
||||
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
|
||||
}`}
|
||||
>
|
||||
Información Personal
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('security')}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'security'
|
||||
? 'border-orange-500 text-[var(--color-primary)]'
|
||||
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
|
||||
}`}
|
||||
>
|
||||
Seguridad
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('activity')}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'activity'
|
||||
? 'border-orange-500 text-[var(--color-primary)]'
|
||||
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
|
||||
}`}
|
||||
>
|
||||
Actividad
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
{activeTab === 'profile' && (
|
||||
<Card>
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h3 className="text-lg font-medium text-[var(--text-primary)]">Información Personal</h3>
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" size="sm">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Exportar Datos
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Avatar and Basic Info */}
|
||||
<div className="flex items-center gap-6 mb-8">
|
||||
<Avatar
|
||||
src="/api/placeholder/120/120"
|
||||
alt={userInfo.name}
|
||||
size="lg"
|
||||
className="w-20 h-20"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-xl font-semibold text-[var(--text-primary)]">{userInfo.name}</h2>
|
||||
<p className="text-[var(--text-secondary)]">{userInfo.role}</p>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<Badge variant="success">Verificado</Badge>
|
||||
<Badge variant="info">Premium</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Form Fields */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||
<User className="w-4 h-4 inline mr-2" />
|
||||
Nombre Completo
|
||||
</label>
|
||||
<Input
|
||||
value={userInfo.name}
|
||||
onChange={(e) => setUserInfo({...userInfo, name: e.target.value})}
|
||||
disabled={!isEditing}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||
<Mail className="w-4 h-4 inline mr-2" />
|
||||
Email
|
||||
</label>
|
||||
<Input
|
||||
value={userInfo.email}
|
||||
onChange={(e) => setUserInfo({...userInfo, email: e.target.value})}
|
||||
disabled={!isEditing}
|
||||
type="email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||
<Phone className="w-4 h-4 inline mr-2" />
|
||||
Teléfono
|
||||
</label>
|
||||
<Input
|
||||
value={userInfo.phone}
|
||||
onChange={(e) => setUserInfo({...userInfo, phone: e.target.value})}
|
||||
disabled={!isEditing}
|
||||
type="tel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||
<Building className="w-4 h-4 inline mr-2" />
|
||||
Panadería
|
||||
</label>
|
||||
<Input
|
||||
value={userInfo.bakery}
|
||||
onChange={(e) => setUserInfo({...userInfo, bakery: e.target.value})}
|
||||
disabled={!isEditing}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||
<MapPin className="w-4 h-4 inline mr-2" />
|
||||
Dirección
|
||||
</label>
|
||||
<Input
|
||||
value={userInfo.address}
|
||||
onChange={(e) => setUserInfo({...userInfo, address: e.target.value})}
|
||||
disabled={!isEditing}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
{isEditing && (
|
||||
<div className="flex gap-3 pt-6 mt-6 border-t border-[var(--border-primary)]">
|
||||
<Button onClick={handleSave}>Guardar Cambios</Button>
|
||||
<Button variant="outline" onClick={handleCancel}>Cancelar</Button>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{!isEditing && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsEditing(true)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<User className="w-4 h-4" />
|
||||
Editar Perfil
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowPasswordForm(!showPasswordForm)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Lock className="w-4 h-4" />
|
||||
Cambiar Contraseña
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{activeTab === 'security' && (
|
||||
<Card>
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h3 className="text-lg font-medium text-[var(--text-primary)]">Configuración de Seguridad</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between p-4 border border-[var(--border-primary)] rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<Shield className="w-5 h-5 text-[var(--color-info)]" />
|
||||
<div>
|
||||
<p className="font-medium text-[var(--text-primary)]">Autenticación de Dos Factores</p>
|
||||
<p className="text-sm text-[var(--text-secondary)]">Protege tu cuenta con 2FA</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge variant={mockProfileStats.twoFactorEnabled ? "success" : "warning"}>
|
||||
{mockProfileStats.twoFactorEnabled ? "Activo" : "Pendiente"}
|
||||
</Badge>
|
||||
<Button variant="outline" size="sm" onClick={handleEnable2FA}>
|
||||
{mockProfileStats.twoFactorEnabled ? "Desactivar" : "Activar"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 border border-[var(--border-primary)] rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<Lock className="w-5 h-5 text-[var(--color-primary)]" />
|
||||
<div>
|
||||
<p className="font-medium text-[var(--text-primary)]">Contraseña</p>
|
||||
<p className="text-sm text-[var(--text-secondary)]">Actualizada hace {mockProfileStats.passwordLastChanged}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={handleChangePassword}>
|
||||
Cambiar
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 border border-[var(--border-primary)] rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<Settings className="w-5 h-5 text-purple-600" />
|
||||
<div>
|
||||
<p className="font-medium text-[var(--text-primary)]">Sesiones Activas</p>
|
||||
<p className="text-sm text-[var(--text-secondary)]">{mockProfileStats.activeSessions} dispositivos conectados</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={handleManageSessions}>
|
||||
Gestionar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/* Profile Form */}
|
||||
<Card className="p-6">
|
||||
<h2 className="text-lg font-semibold mb-4">Información Personal</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
<Input
|
||||
label="Nombre"
|
||||
value={profileData.first_name}
|
||||
onChange={handleInputChange('first_name')}
|
||||
error={errors.first_name}
|
||||
disabled={!isEditing || isLoading}
|
||||
leftIcon={<User className="w-4 h-4" />}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Apellidos"
|
||||
value={profileData.last_name}
|
||||
onChange={handleInputChange('last_name')}
|
||||
error={errors.last_name}
|
||||
disabled={!isEditing || isLoading}
|
||||
/>
|
||||
|
||||
<Input
|
||||
type="email"
|
||||
label="Correo Electrónico"
|
||||
value={profileData.email}
|
||||
onChange={handleInputChange('email')}
|
||||
error={errors.email}
|
||||
disabled={!isEditing || isLoading}
|
||||
leftIcon={<Mail className="w-4 h-4" />}
|
||||
/>
|
||||
|
||||
<Input
|
||||
type="tel"
|
||||
label="Teléfono"
|
||||
value={profileData.phone}
|
||||
onChange={handleInputChange('phone')}
|
||||
error={errors.phone}
|
||||
disabled={!isEditing || isLoading}
|
||||
placeholder="+34 600 000 000"
|
||||
leftIcon={<Phone className="w-4 h-4" />}
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="Idioma"
|
||||
options={languageOptions}
|
||||
value={profileData.language}
|
||||
onChange={handleSelectChange('language')}
|
||||
disabled={!isEditing || isLoading}
|
||||
leftIcon={<Globe className="w-4 h-4" />}
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="Zona Horaria"
|
||||
options={timezoneOptions}
|
||||
value={profileData.timezone}
|
||||
onChange={handleSelectChange('timezone')}
|
||||
disabled={!isEditing || isLoading}
|
||||
leftIcon={<Clock className="w-4 h-4" />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isEditing && (
|
||||
<div className="flex gap-3 mt-6 pt-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsEditing(false)}
|
||||
disabled={isLoading}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSaveProfile}
|
||||
isLoading={isLoading}
|
||||
loadingText="Guardando..."
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
Guardar Cambios
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{activeTab === 'activity' && (
|
||||
<Card>
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h3 className="text-lg font-medium text-[var(--text-primary)]">Actividad Reciente</h3>
|
||||
</div>
|
||||
{/* Password Change Form */}
|
||||
{showPasswordForm && (
|
||||
<Card className="p-6">
|
||||
<h2 className="text-lg font-semibold mb-4">Cambiar Contraseña</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 max-w-4xl">
|
||||
<Input
|
||||
type="password"
|
||||
label="Contraseña Actual"
|
||||
value={passwordData.currentPassword}
|
||||
onChange={handlePasswordChange('currentPassword')}
|
||||
error={errors.currentPassword}
|
||||
disabled={isLoading}
|
||||
leftIcon={<Lock className="w-4 h-4" />}
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-4 p-4 border border-[var(--border-primary)] rounded-lg">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<Activity className="w-5 h-5 text-green-500" />
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-[var(--text-primary)]">Inicio de sesión</p>
|
||||
<p className="text-sm text-[var(--text-secondary)]">Hace 2 horas desde Chrome en Madrid, España</p>
|
||||
</div>
|
||||
<span className="text-xs text-[var(--text-tertiary)]">Hoy 14:30</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 p-4 border border-[var(--border-primary)] rounded-lg">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
||||
<User className="w-5 h-5 text-blue-500" />
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-[var(--text-primary)]">Perfil actualizado</p>
|
||||
<p className="text-sm text-[var(--text-secondary)]">Se modificó la información de contacto</p>
|
||||
</div>
|
||||
<span className="text-xs text-[var(--text-tertiary)]">Ayer 09:15</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 p-4 border border-[var(--border-primary)] rounded-lg">
|
||||
<div className="w-2 h-2 bg-orange-500 rounded-full"></div>
|
||||
<Shield className="w-5 h-5 text-orange-500" />
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-[var(--text-primary)]">Contraseña cambiada</p>
|
||||
<p className="text-sm text-[var(--text-secondary)]">Contraseña actualizada exitosamente</p>
|
||||
</div>
|
||||
<span className="text-xs text-[var(--text-tertiary)]">Hace 2 meses</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 p-4 border border-[var(--border-primary)] rounded-lg">
|
||||
<div className="w-2 h-2 bg-purple-500 rounded-full"></div>
|
||||
<Bell className="w-5 h-5 text-purple-500" />
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-[var(--text-primary)]">Configuración de notificaciones</p>
|
||||
<p className="text-sm text-[var(--text-secondary)]">Se habilitaron las notificaciones por email</p>
|
||||
</div>
|
||||
<span className="text-xs text-[var(--text-tertiary)]">Hace 1 semana</span>
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
type="password"
|
||||
label="Nueva Contraseña"
|
||||
value={passwordData.newPassword}
|
||||
onChange={handlePasswordChange('newPassword')}
|
||||
error={errors.newPassword}
|
||||
disabled={isLoading}
|
||||
leftIcon={<Lock className="w-4 h-4" />}
|
||||
/>
|
||||
|
||||
<Input
|
||||
type="password"
|
||||
label="Confirmar Nueva Contraseña"
|
||||
value={passwordData.confirmPassword}
|
||||
onChange={handlePasswordChange('confirmPassword')}
|
||||
error={errors.confirmPassword}
|
||||
disabled={isLoading}
|
||||
leftIcon={<Lock className="w-4 h-4" />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-6 mt-6 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setShowPasswordForm(false);
|
||||
setPasswordData({ currentPassword: '', newPassword: '', confirmPassword: '' });
|
||||
setErrors({});
|
||||
}}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleChangePassword}
|
||||
isLoading={isLoading}
|
||||
loadingText="Cambiando..."
|
||||
>
|
||||
Cambiar Contraseña
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuthActions, useAuthError, useAuthLoading, useIsAuthenticated } from '../../stores';
|
||||
import { Button, Input, Card } from '../../components/ui';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useIsAuthenticated, useAuthLoading } from '../../stores';
|
||||
import { LoginForm } from '../../components/domain/auth';
|
||||
import { PublicLayout } from '../../components/layout';
|
||||
|
||||
const LoginPage: React.FC = () => {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [rememberMe, setRememberMe] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const { login } = useAuthActions();
|
||||
const error = useAuthError();
|
||||
const loading = useAuthLoading();
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
|
||||
@@ -28,15 +22,12 @@ const LoginPage: React.FC = () => {
|
||||
}
|
||||
}, [isAuthenticated, loading, navigate, from]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!email || !password) return;
|
||||
const handleLoginSuccess = () => {
|
||||
navigate(from, { replace: true });
|
||||
};
|
||||
|
||||
try {
|
||||
await login(email, password);
|
||||
} catch (err) {
|
||||
// Error is handled by the store
|
||||
}
|
||||
const handleRegisterClick = () => {
|
||||
navigate('/register');
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -49,151 +40,11 @@ const LoginPage: React.FC = () => {
|
||||
variant: "minimal"
|
||||
}}
|
||||
>
|
||||
<div className="w-full max-w-md mx-auto space-y-8">
|
||||
<div>
|
||||
<div className="flex justify-center">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] rounded-lg flex items-center justify-center text-white font-bold text-lg">
|
||||
PI
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-[var(--text-primary)]">
|
||||
Inicia sesión en tu cuenta
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-[var(--text-secondary)]">
|
||||
O{' '}
|
||||
<Link
|
||||
to="/register"
|
||||
className="font-medium text-[var(--color-primary)] hover:text-[var(--color-primary-light)]"
|
||||
>
|
||||
regístrate para comenzar tu prueba gratuita
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card className="p-8">
|
||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||
{error && (
|
||||
<div className="bg-[var(--color-error)]/10 border border-[var(--color-error)]/20 rounded-md p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5 text-[var(--color-error)]" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-[var(--color-error)]">
|
||||
Error de autenticación
|
||||
</h3>
|
||||
<div className="mt-2 text-sm text-[var(--color-error)]">
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="sr-only">
|
||||
Correo electrónico
|
||||
</label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
placeholder="Correo electrónico"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="sr-only">
|
||||
Contraseña
|
||||
</label>
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
placeholder="Contraseña"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="remember-me"
|
||||
name="remember-me"
|
||||
type="checkbox"
|
||||
className="h-4 w-4 text-[var(--color-primary)] focus:ring-[var(--color-primary)] border-[var(--border-primary)] rounded"
|
||||
checked={rememberMe}
|
||||
onChange={(e) => setRememberMe(e.target.checked)}
|
||||
/>
|
||||
<label htmlFor="remember-me" className="ml-2 block text-sm text-[var(--text-primary)]">
|
||||
Recordarme
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="text-sm">
|
||||
<a href="#" className="font-medium text-[var(--color-primary)] hover:text-[var(--color-primary-light)]">
|
||||
¿Olvidaste tu contraseña?
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full flex justify-center"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Iniciando sesión...' : 'Iniciar sesión'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-[var(--border-primary)]" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-2 bg-[var(--bg-primary)] text-[var(--text-tertiary)]">Demo</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
// TODO: Handle demo login
|
||||
console.log('Demo login');
|
||||
}}
|
||||
>
|
||||
Usar cuenta de demo
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center text-xs text-[var(--text-tertiary)]">
|
||||
Al iniciar sesión, aceptas nuestros{' '}
|
||||
<a href="#" className="text-[var(--color-primary)] hover:text-[var(--color-primary-light)]">
|
||||
Términos de Servicio
|
||||
</a>
|
||||
{' '}y{' '}
|
||||
<a href="#" className="text-[var(--color-primary)] hover:text-[var(--color-primary-light)]">
|
||||
Política de Privacidad
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<LoginForm
|
||||
onSuccess={handleLoginSuccess}
|
||||
onRegisterClick={handleRegisterClick}
|
||||
className="mx-auto"
|
||||
/>
|
||||
</PublicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,371 +1,34 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Button, Input, Card, Select } from '../../components/ui';
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { RegisterForm } from '../../components/domain/auth';
|
||||
import { PublicLayout } from '../../components/layout';
|
||||
|
||||
const RegisterPage: React.FC = () => {
|
||||
const [step, setStep] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
// Personal info
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
|
||||
// Company info
|
||||
companyName: '',
|
||||
companyType: '',
|
||||
employeeCount: '',
|
||||
|
||||
// Account info
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
acceptTerms: false,
|
||||
acceptMarketing: false,
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleInputChange = (field: string, value: string | boolean) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
const handleRegistrationSuccess = () => {
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
const handleNextStep = () => {
|
||||
setStep(prev => prev + 1);
|
||||
const handleLoginClick = () => {
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
const handlePrevStep = () => {
|
||||
setStep(prev => prev - 1);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
// Redirect to onboarding
|
||||
navigate('/onboarding');
|
||||
} catch (error) {
|
||||
console.error('Registration failed:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const isStep1Valid = formData.firstName && formData.lastName && formData.email && formData.phone;
|
||||
const isStep2Valid = formData.companyName && formData.companyType && formData.employeeCount;
|
||||
const isStep3Valid = formData.password && formData.confirmPassword &&
|
||||
formData.password === formData.confirmPassword && formData.acceptTerms;
|
||||
|
||||
return (
|
||||
<PublicLayout
|
||||
variant="centered"
|
||||
maxWidth="md"
|
||||
maxWidth="xl"
|
||||
headerProps={{
|
||||
showThemeToggle: true,
|
||||
showAuthButtons: false,
|
||||
variant: "minimal"
|
||||
}}
|
||||
>
|
||||
<div className="w-full max-w-md mx-auto space-y-8">
|
||||
<div>
|
||||
<div className="flex justify-center">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] rounded-lg flex items-center justify-center text-white font-bold text-lg">
|
||||
PI
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-[var(--text-primary)]">
|
||||
Crea tu cuenta
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-[var(--text-secondary)]">
|
||||
O{' '}
|
||||
<Link
|
||||
to="/login"
|
||||
className="font-medium text-[var(--color-primary)] hover:text-[var(--color-primary-light)]"
|
||||
>
|
||||
inicia sesión si ya tienes una cuenta
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card className="p-8">
|
||||
{/* Progress indicator */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-start justify-between">
|
||||
{[
|
||||
{ step: 1, label: 'Datos personales' },
|
||||
{ step: 2, label: 'Información empresarial' },
|
||||
{ step: 3, label: 'Crear cuenta' }
|
||||
].map((stepInfo) => (
|
||||
<div key={stepInfo.step} className="flex flex-col items-center">
|
||||
<div
|
||||
className={`flex items-center justify-center w-8 h-8 rounded-full ${
|
||||
step >= stepInfo.step
|
||||
? 'bg-[var(--color-primary)] text-white'
|
||||
: 'bg-[var(--bg-quaternary)] text-[var(--text-tertiary)]'
|
||||
}`}
|
||||
>
|
||||
{stepInfo.step}
|
||||
</div>
|
||||
<span className="mt-2 text-xs text-[var(--text-secondary)] text-center max-w-[80px]">
|
||||
{stepInfo.label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{step === 1 && (
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label htmlFor="firstName" className="block text-sm font-medium text-[var(--text-primary)]">
|
||||
Nombre *
|
||||
</label>
|
||||
<Input
|
||||
id="firstName"
|
||||
type="text"
|
||||
required
|
||||
value={formData.firstName}
|
||||
onChange={(e) => handleInputChange('firstName', e.target.value)}
|
||||
placeholder="Tu nombre"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="lastName" className="block text-sm font-medium text-[var(--text-primary)]">
|
||||
Apellido *
|
||||
</label>
|
||||
<Input
|
||||
id="lastName"
|
||||
type="text"
|
||||
required
|
||||
value={formData.lastName}
|
||||
onChange={(e) => handleInputChange('lastName', e.target.value)}
|
||||
placeholder="Tu apellido"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-[var(--text-primary)]">
|
||||
Correo electrónico *
|
||||
</label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
required
|
||||
value={formData.email}
|
||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||
placeholder="tu@email.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="phone" className="block text-sm font-medium text-[var(--text-primary)]">
|
||||
Teléfono *
|
||||
</label>
|
||||
<Input
|
||||
id="phone"
|
||||
type="tel"
|
||||
required
|
||||
value={formData.phone}
|
||||
onChange={(e) => handleInputChange('phone', e.target.value)}
|
||||
placeholder="+34 600 000 000"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleNextStep}
|
||||
disabled={!isStep1Valid}
|
||||
className="w-full"
|
||||
>
|
||||
Continuar
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{step === 2 && (
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="companyName" className="block text-sm font-medium text-[var(--text-primary)]">
|
||||
Nombre de la panadería *
|
||||
</label>
|
||||
<Input
|
||||
id="companyName"
|
||||
type="text"
|
||||
required
|
||||
value={formData.companyName}
|
||||
onChange={(e) => handleInputChange('companyName', e.target.value)}
|
||||
placeholder="Panadería San Miguel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="companyType" className="block text-sm font-medium text-[var(--text-primary)]">
|
||||
Tipo de negocio *
|
||||
</label>
|
||||
<Select
|
||||
value={formData.companyType}
|
||||
onChange={(value) => handleInputChange('companyType', value as string)}
|
||||
placeholder="Selecciona el tipo"
|
||||
options={[
|
||||
{ value: "traditional", label: "Panadería tradicional" },
|
||||
{ value: "artisan", label: "Panadería artesanal" },
|
||||
{ value: "industrial", label: "Panadería industrial" },
|
||||
{ value: "bakery-cafe", label: "Panadería-cafetería" },
|
||||
{ value: "specialty", label: "Panadería especializada" }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="employeeCount" className="block text-sm font-medium text-[var(--text-primary)]">
|
||||
Número de empleados *
|
||||
</label>
|
||||
<Select
|
||||
value={formData.employeeCount}
|
||||
onChange={(value) => handleInputChange('employeeCount', value as string)}
|
||||
placeholder="Selecciona el rango"
|
||||
options={[
|
||||
{ value: "1", label: "Solo yo" },
|
||||
{ value: "2-5", label: "2-5 empleados" },
|
||||
{ value: "6-15", label: "6-15 empleados" },
|
||||
{ value: "16-50", label: "16-50 empleados" },
|
||||
{ value: "51+", label: "Más de 50 empleados" }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handlePrevStep}
|
||||
className="flex-1"
|
||||
>
|
||||
Atrás
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleNextStep}
|
||||
disabled={!isStep2Valid}
|
||||
className="flex-1"
|
||||
>
|
||||
Continuar
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{step === 3 && (
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-[var(--text-primary)]">
|
||||
Contraseña *
|
||||
</label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
required
|
||||
value={formData.password}
|
||||
onChange={(e) => handleInputChange('password', e.target.value)}
|
||||
placeholder="Mínimo 8 caracteres"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="confirmPassword" className="block text-sm font-medium text-[var(--text-primary)]">
|
||||
Confirmar contraseña *
|
||||
</label>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
required
|
||||
value={formData.confirmPassword}
|
||||
onChange={(e) => handleInputChange('confirmPassword', e.target.value)}
|
||||
placeholder="Repite la contraseña"
|
||||
/>
|
||||
{formData.confirmPassword && formData.password !== formData.confirmPassword && (
|
||||
<p className="mt-1 text-sm text-[var(--color-error)]">Las contraseñas no coinciden</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start">
|
||||
<input
|
||||
id="acceptTerms"
|
||||
type="checkbox"
|
||||
className="h-4 w-4 text-[var(--color-primary)] focus:ring-[var(--color-primary)] border-[var(--border-secondary)] rounded mt-0.5"
|
||||
checked={formData.acceptTerms}
|
||||
onChange={(e) => handleInputChange('acceptTerms', e.target.checked)}
|
||||
/>
|
||||
<label htmlFor="acceptTerms" className="ml-2 block text-sm text-[var(--text-primary)]">
|
||||
Acepto los{' '}
|
||||
<a href="#" className="text-[var(--color-primary)] hover:text-[var(--color-primary-light)]">
|
||||
Términos de Servicio
|
||||
</a>{' '}
|
||||
y la{' '}
|
||||
<a href="#" className="text-[var(--color-primary)] hover:text-[var(--color-primary-light)]">
|
||||
Política de Privacidad
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start">
|
||||
<input
|
||||
id="acceptMarketing"
|
||||
type="checkbox"
|
||||
className="h-4 w-4 text-[var(--color-primary)] focus:ring-[var(--color-primary)] border-[var(--border-secondary)] rounded mt-0.5"
|
||||
checked={formData.acceptMarketing}
|
||||
onChange={(e) => handleInputChange('acceptMarketing', e.target.checked)}
|
||||
/>
|
||||
<label htmlFor="acceptMarketing" className="ml-2 block text-sm text-[var(--text-primary)]">
|
||||
Quiero recibir newsletters y novedades sobre el producto (opcional)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handlePrevStep}
|
||||
className="flex-1"
|
||||
>
|
||||
Atrás
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!isStep3Valid || loading}
|
||||
className="flex-1"
|
||||
>
|
||||
{loading ? 'Creando cuenta...' : 'Crear cuenta'}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center text-xs text-[var(--text-secondary)]">
|
||||
¿Necesitas ayuda?{' '}
|
||||
<a href="#" className="text-[var(--color-primary)] hover:text-[var(--color-primary-light)]">
|
||||
Contáctanos
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<RegisterForm
|
||||
onSuccess={handleRegistrationSuccess}
|
||||
onLoginClick={handleLoginClick}
|
||||
className="mx-auto"
|
||||
/>
|
||||
</PublicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user