Improve the frontend and fix TODOs

This commit is contained in:
Urtzi Alfaro
2025-10-24 13:05:04 +02:00
parent 07c33fa578
commit 61376b7a9f
100 changed files with 8284 additions and 3419 deletions

View File

@@ -1,577 +0,0 @@
import React, { useState } from 'react';
import { Store, MapPin, Clock, Phone, Mail, Globe, Save, X, AlertCircle, Loader } from 'lucide-react';
import { Button, Card, Input, Select } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
import { useToast } from '../../../../hooks/ui/useToast';
import { useUpdateTenant } from '../../../../api/hooks/tenant';
import { useCurrentTenant, useTenantActions } from '../../../../stores/tenant.store';
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 InformationPage: React.FC = () => {
const { addToast } = useToast();
const currentTenant = useCurrentTenant();
const { loadUserTenants, setCurrentTenant } = useTenantActions();
const tenantId = currentTenant?.id || '';
// Use the current tenant from the store instead of making additional API calls
// to avoid the 422 validation error on the tenant GET endpoint
const tenant = currentTenant;
const tenantLoading = !currentTenant;
const tenantError = null;
const updateTenantMutation = useUpdateTenant();
const [isLoading, setIsLoading] = useState(false);
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const [config, setConfig] = useState<BakeryConfig>({
name: '',
description: '',
email: '',
phone: '',
website: '',
address: '',
city: '',
postalCode: '',
country: '',
taxId: '',
currency: 'EUR',
timezone: 'Europe/Madrid',
language: 'es'
});
// Load user tenants on component mount to ensure fresh data
React.useEffect(() => {
loadUserTenants();
}, [loadUserTenants]);
// Update config when tenant data is loaded
React.useEffect(() => {
if (tenant) {
setConfig({
name: tenant.name || '',
description: tenant.description || '',
email: tenant.email || '',
phone: tenant.phone || '',
website: tenant.website || '',
address: tenant.address || '',
city: tenant.city || '',
postalCode: tenant.postal_code || '',
country: tenant.country || '',
taxId: '', // Not supported by backend yet
currency: 'EUR', // Default value
timezone: 'Europe/Madrid', // Default value
language: 'es' // Default value
});
setHasUnsavedChanges(false); // Reset unsaved changes when loading fresh data
}
}, [tenant]);
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 daysOfWeek = [
{ key: 'monday', label: 'Lunes' },
{ key: 'tuesday', label: 'Martes' },
{ key: 'wednesday', label: 'Miércoles' },
{ key: 'thursday', label: 'Jueves' },
{ key: 'friday', label: 'Viernes' },
{ key: 'saturday', label: 'Sábado' },
{ key: 'sunday', label: 'Domingo' }
];
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() || !tenantId) return;
setIsLoading(true);
try {
const updateData = {
name: config.name,
description: config.description,
email: config.email,
phone: config.phone,
website: config.website,
address: config.address,
city: config.city,
postal_code: config.postalCode,
country: config.country
};
const updatedTenant = await updateTenantMutation.mutateAsync({
tenantId,
updateData
});
// Update the tenant store with the new data
if (updatedTenant) {
setCurrentTenant(updatedTenant);
// Force reload tenant list to ensure cache consistency
await loadUserTenants();
// Update localStorage to persist the changes
const tenantStorage = localStorage.getItem('tenant-storage');
if (tenantStorage) {
const parsedStorage = JSON.parse(tenantStorage);
if (parsedStorage.state && parsedStorage.state.currentTenant) {
parsedStorage.state.currentTenant = updatedTenant;
localStorage.setItem('tenant-storage', JSON.stringify(parsedStorage));
}
}
}
setHasUnsavedChanges(false);
addToast('Información actualizada correctamente', { type: 'success' });
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error desconocido';
addToast(`Error al actualizar: ${errorMessage}`, { type: 'error' });
} finally {
setIsLoading(false);
}
};
const handleInputChange = (field: keyof BakeryConfig) => (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setConfig(prev => ({ ...prev, [field]: e.target.value }));
setHasUnsavedChanges(true);
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: '' }));
}
};
const handleSelectChange = (field: keyof BakeryConfig) => (value: string) => {
setConfig(prev => ({ ...prev, [field]: value }));
setHasUnsavedChanges(true);
};
const handleHoursChange = (day: string, field: 'open' | 'close' | 'closed', value: string | boolean) => {
setBusinessHours(prev => ({
...prev,
[day]: {
...prev[day],
[field]: value
}
}));
setHasUnsavedChanges(true);
};
if (tenantLoading || !currentTenant) {
return (
<div className="p-6 space-y-6">
<PageHeader
title="Información de la Panadería"
description="Configura los datos básicos y preferencias de tu panadería"
/>
<div className="flex items-center justify-center h-64">
<Loader className="w-8 h-8 animate-spin" />
<span className="ml-2">Cargando información...</span>
</div>
</div>
);
}
if (tenantError) {
return (
<div className="p-6 space-y-6">
<PageHeader
title="Información de la Panadería"
description="Error al cargar la información"
/>
<Card className="p-6">
<div className="text-red-600">
Error al cargar la información: Error desconocido
</div>
</Card>
</div>
);
}
return (
<div className="p-6 space-y-6">
<PageHeader
title="Información de la Panadería"
description="Configura los datos básicos y preferencias de tu panadería"
/>
{/* 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">
{hasUnsavedChanges && (
<div className="flex items-center gap-2 text-sm text-yellow-600">
<AlertCircle className="w-4 h-4" />
Cambios sin guardar
</div>
)}
</div>
</div>
</Card>
{/* Information Sections */}
<div className="space-y-8">
{/* General Information */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-text-primary mb-6 flex items-center">
<Store className="w-5 h-5 mr-2" />
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={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={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={isLoading}
placeholder="+34 912 345 678"
leftIcon={<Phone className="w-4 h-4" />}
/>
<Input
label="Sitio Web"
value={config.website}
onChange={handleInputChange('website')}
disabled={isLoading}
placeholder="https://tu-panaderia.com"
leftIcon={<Globe className="w-4 h-4" />}
className="md:col-span-2 xl:col-span-3"
/>
</div>
<div className="mt-6">
<label className="block text-sm font-medium text-text-secondary mb-2">
Descripción
</label>
<textarea
value={config.description}
onChange={handleInputChange('description')}
disabled={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>
</Card>
{/* Location Information */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-text-primary mb-6 flex items-center">
<MapPin className="w-5 h-5 mr-2" />
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={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={isLoading}
placeholder="Ciudad"
/>
<Input
label="Código Postal"
value={config.postalCode}
onChange={handleInputChange('postalCode')}
disabled={isLoading}
placeholder="28001"
/>
<Input
label="País"
value={config.country}
onChange={handleInputChange('country')}
disabled={isLoading}
placeholder="España"
/>
</div>
</Card>
{/* Business Information */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-text-primary mb-6">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={isLoading}
placeholder="B12345678"
/>
<Select
label="Moneda"
options={currencyOptions}
value={config.currency}
onChange={(value) => handleSelectChange('currency')(value as string)}
disabled={isLoading}
/>
<Select
label="Zona Horaria"
options={timezoneOptions}
value={config.timezone}
onChange={(value) => handleSelectChange('timezone')(value as string)}
disabled={isLoading}
/>
<Select
label="Idioma"
options={languageOptions}
value={config.language}
onChange={(value) => handleSelectChange('language')(value as string)}
disabled={isLoading}
/>
</div>
</Card>
{/* Business Hours */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-text-primary mb-6 flex items-center">
<Clock className="w-5 h-5 mr-2" />
Horarios de Apertura
</h3>
<div className="space-y-4">
{daysOfWeek.map((day) => {
const hours = businessHours[day.key];
return (
<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>
{/* 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={isLoading}
className="rounded border-border-primary"
/>
<span className="text-sm text-text-secondary">Cerrado</span>
</label>
</div>
{/* 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={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={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>
</div>
);
})}
</div>
</Card>
</div>
{/* Floating Save Button */}
{hasUnsavedChanges && (
<div className="fixed bottom-6 right-6 z-50">
<Card className="p-4 shadow-lg">
<div className="flex items-center gap-3">
<div className="flex items-center gap-2 text-sm text-text-secondary">
<AlertCircle className="w-4 h-4 text-yellow-500" />
Tienes cambios sin guardar
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => {
// Reset to original values
if (tenant) {
setConfig({
name: tenant.name || '',
description: tenant.description || '',
email: tenant.email || '',
phone: tenant.phone || '',
website: tenant.website || '',
address: tenant.address || '',
city: tenant.city || '',
postalCode: tenant.postal_code || '',
country: tenant.country || '',
taxId: '',
currency: 'EUR',
timezone: 'Europe/Madrid',
language: 'es'
});
}
setHasUnsavedChanges(false);
}}
disabled={isLoading}
>
<X className="w-4 h-4" />
Descartar
</Button>
<Button
variant="primary"
size="sm"
onClick={handleSaveConfig}
isLoading={isLoading}
loadingText="Guardando..."
>
<Save className="w-4 h-4" />
Guardar
</Button>
</div>
</div>
</Card>
</div>
)}
</div>
);
};
export default InformationPage;