Improve onboarding

This commit is contained in:
Urtzi Alfaro
2025-12-18 13:26:32 +01:00
parent f76b3f8e6b
commit f10a2b92ea
42 changed files with 2175 additions and 984 deletions

View File

@@ -1,11 +1,8 @@
import React, { useState } from 'react';
import React, { useState, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { PageHeader } from '../../../../components/layout';
import { Button } from '../../../../components/ui/Button';
import { Card, CardHeader, CardBody } from '../../../../components/ui/Card';
import { Badge } from '../../../../components/ui/Badge';
import { Tooltip } from '../../../../components/ui/Tooltip';
import { Button, StatsGrid, StatusCard, getStatusColor, SearchAndFilter, EmptyState } from '../../../../components/ui';
import { useTenant } from '../../../../stores/tenant.store';
import { useAuthUser } from '../../../../stores/auth.store';
import {
@@ -13,12 +10,6 @@ import {
Building2,
Settings,
Users,
Calendar,
MapPin,
Phone,
Mail,
Globe,
MoreHorizontal,
ArrowRight,
Crown,
Shield,
@@ -31,6 +22,8 @@ const OrganizationsPage: React.FC = () => {
const user = useAuthUser();
const { currentTenant, availableTenants, switchTenant } = useTenant();
const [isLoading, setIsLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [roleFilter, setRoleFilter] = useState('');
const handleAddNewOrganization = () => {
navigate('/app/onboarding?new=true');
@@ -44,30 +37,14 @@ const OrganizationsPage: React.FC = () => {
setIsLoading(false);
};
const handleManageTenant = (tenantId: string) => {
// Navigate to tenant settings
const handleManageTenant = () => {
navigate(`/app/database/bakery-config`);
};
const handleManageTeam = (tenantId: string) => {
// Navigate to team management
const handleManageTeam = () => {
navigate(`/app/database/team`);
};
const getRoleIcon = (ownerId: string) => {
if (user?.id === ownerId) {
return <Crown className="w-4 h-4 text-[var(--color-warning)]" />;
}
return <Shield className="w-4 h-4 text-[var(--color-primary)]" />;
};
const getRoleLabel = (ownerId: string) => {
if (user?.id === ownerId) {
return 'Propietario';
}
return 'Miembro';
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('es-ES', {
year: 'numeric',
@@ -76,208 +53,197 @@ const OrganizationsPage: React.FC = () => {
});
};
const isOwner = (ownerId: string) => user?.id === ownerId;
// Filter organizations based on search and role
const filteredTenants = useMemo(() => {
let filtered = availableTenants || [];
if (searchTerm) {
const searchLower = searchTerm.toLowerCase();
filtered = filtered.filter(tenant =>
tenant.name.toLowerCase().includes(searchLower) ||
tenant.business_type?.toLowerCase().includes(searchLower) ||
tenant.city?.toLowerCase().includes(searchLower)
);
}
if (roleFilter) {
filtered = filtered.filter(tenant => {
if (roleFilter === 'owner') return isOwner(tenant.owner_id);
if (roleFilter === 'member') return !isOwner(tenant.owner_id);
return true;
});
}
return filtered;
}, [availableTenants, searchTerm, roleFilter, user?.id]);
// Calculate stats
const stats = useMemo(() => [
{
title: 'Organizaciones Totales',
value: availableTenants?.length || 0,
variant: 'default' as const,
icon: Building2,
},
{
title: 'Como Propietario',
value: availableTenants?.filter(t => isOwner(t.owner_id)).length || 0,
variant: 'warning' as const,
icon: Crown,
},
{
title: 'Como Miembro',
value: availableTenants?.filter(t => !isOwner(t.owner_id)).length || 0,
variant: 'info' as const,
icon: Shield,
},
], [availableTenants, user?.id]);
return (
<div className="space-y-6 p-4 sm:p-6">
<div className="space-y-6">
<PageHeader
title={t('settings:organization.title', 'Mis Organizaciones')}
description={t('settings:organization.description', 'Gestiona tus panaderías y negocios')}
actions={
<Button
onClick={handleAddNewOrganization}
variant="primary"
size="lg"
className="flex items-center gap-2"
>
<Plus className="w-4 h-4" />
Nueva Organización
</Button>
}
title="Organizaciones"
description="Gestiona tus organizaciones y configuraciones"
actions={[
{
id: "add-organization",
label: "Nueva Organización",
variant: "primary" as const,
icon: Plus,
onClick: handleAddNewOrganization,
tooltip: "Crear nueva organización",
size: "md"
}
]}
/>
{/* Statistics */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card>
<CardBody className="text-center">
<Building2 className="w-8 h-8 text-[var(--color-primary)] mx-auto mb-2" />
<div className="text-2xl font-bold text-[var(--text-primary)]">
{availableTenants?.length || 0}
</div>
<div className="text-sm text-[var(--text-secondary)]">Organizaciones Totales</div>
</CardBody>
</Card>
{/* Stats Grid */}
<StatsGrid
stats={stats}
columns={3}
/>
<Card>
<CardBody className="text-center">
<Crown className="w-8 h-8 text-[var(--color-warning)] mx-auto mb-2" />
<div className="text-2xl font-bold text-[var(--text-primary)]">
{availableTenants?.filter(t => t.owner_id === user?.id).length || 0}
</div>
<div className="text-sm text-[var(--text-secondary)]">Propietario</div>
</CardBody>
</Card>
{/* Search and Filter Controls */}
<SearchAndFilter
searchValue={searchTerm}
onSearchChange={setSearchTerm}
searchPlaceholder="Buscar por nombre, tipo o ciudad..."
filters={[
{
key: 'role',
label: 'Rol',
type: 'dropdown',
value: roleFilter,
onChange: (value) => setRoleFilter(value as string),
placeholder: 'Todos los roles',
options: [
{ value: 'owner', label: 'Propietario' },
{ value: 'member', label: 'Miembro' }
]
}
]}
/>
<Card>
<CardBody className="text-center">
<Shield className="w-8 h-8 text-[var(--color-primary)] mx-auto mb-2" />
<div className="text-2xl font-bold text-[var(--text-primary)]">
{availableTenants?.filter(t => t.owner_id !== user?.id).length || 0}
</div>
<div className="text-sm text-[var(--text-secondary)]">Miembro</div>
</CardBody>
</Card>
</div>
{/* Organizations Grid */}
{filteredTenants && filteredTenants.length > 0 ? (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredTenants.map((tenant) => {
const isActive = currentTenant?.id === tenant.id;
const isOwnerRole = isOwner(tenant.owner_id);
{/* Organizations List */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">Tus Organizaciones</h3>
const statusConfig = {
color: isActive ? getStatusColor('completed') : getStatusColor('default'),
text: isActive ? 'Activa' : 'Inactiva',
icon: isOwnerRole ? Crown : Shield,
isCritical: false,
isHighlight: isActive
};
{availableTenants && availableTenants.length > 0 ? (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{availableTenants.map((tenant) => (
<Card
return (
<StatusCard
key={tenant.id}
className={`transition-all duration-200 hover:shadow-lg ${
currentTenant?.id === tenant.id
? 'ring-2 ring-[var(--color-primary)]/20 bg-[var(--color-primary)]/5'
: 'hover:border-[var(--color-primary)]/30'
}`}
>
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-4">
<div className="flex items-start gap-3 flex-1 min-w-0">
<div className="w-12 h-12 bg-[var(--color-primary)]/10 rounded-lg flex items-center justify-center flex-shrink-0">
<Building2 className="w-6 h-6 text-[var(--color-primary)]" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h4 className="font-semibold text-[var(--text-primary)] truncate">
{tenant.name}
</h4>
{currentTenant?.id === tenant.id && (
<Badge variant="primary" size="sm">Activa</Badge>
)}
</div>
<div className="flex items-center gap-2 text-sm text-[var(--text-secondary)]">
{getRoleIcon(tenant.owner_id)}
<span>{getRoleLabel(tenant.owner_id)}</span>
</div>
</div>
</div>
<div className="flex gap-1">
<Tooltip content="Configurar organización">
<Button
variant="ghost"
size="sm"
onClick={() => handleManageTenant(tenant.id)}
className="w-8 h-8 p-0"
>
<Settings className="w-4 h-4" />
</Button>
</Tooltip>
{user?.id === tenant.owner_id && (
<Tooltip content="Gestionar equipo">
<Button
variant="ghost"
size="sm"
onClick={() => handleManageTeam(tenant.id)}
className="w-8 h-8 p-0"
>
<Users className="w-4 h-4" />
</Button>
</Tooltip>
)}
</div>
</CardHeader>
<CardBody className="pt-0">
{/* Organization details */}
<div className="space-y-2 mb-4">
{tenant.business_type && (
<div className="text-sm text-[var(--text-secondary)]">
<Badge variant="outline" size="sm">{tenant.business_type}</Badge>
</div>
)}
{tenant.address && (
<div className="flex items-center gap-2 text-sm text-[var(--text-secondary)]">
<MapPin className="w-4 h-4 flex-shrink-0" />
<span className="truncate">{tenant.address}</span>
</div>
)}
{tenant.city && (
<div className="flex items-center gap-2 text-sm text-[var(--text-secondary)]">
<MapPin className="w-4 h-4 flex-shrink-0" />
<span>{tenant.city}</span>
</div>
)}
{tenant.phone && (
<div className="flex items-center gap-2 text-sm text-[var(--text-secondary)]">
<Phone className="w-4 h-4 flex-shrink-0" />
<span>{tenant.phone}</span>
</div>
)}
<div className="flex items-center gap-2 text-sm text-[var(--text-secondary)]">
<Calendar className="w-4 h-4 flex-shrink-0" />
<span>Creada el {formatDate(tenant.created_at)}</span>
</div>
</div>
{/* Actions */}
<div className="flex gap-2">
{currentTenant?.id !== tenant.id ? (
<Button
onClick={() => handleSwitchToTenant(tenant.id)}
variant="primary"
size="sm"
disabled={isLoading}
className="flex items-center gap-2"
>
<ArrowRight className="w-4 h-4" />
Cambiar a esta organización
</Button>
) : (
<Button
onClick={() => navigate('/app/dashboard')}
variant="outline"
size="sm"
className="flex items-center gap-2"
>
<Eye className="w-4 h-4" />
Ver dashboard
</Button>
)}
</div>
</CardBody>
</Card>
))}
</div>
) : (
<Card>
<CardBody className="text-center py-12">
<Building2 className="w-16 h-16 text-[var(--text-tertiary)] mx-auto mb-4" />
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
No tienes organizaciones
</h3>
<p className="text-[var(--text-secondary)] mb-6">
Crea tu primera organización para comenzar a usar Bakery IA
</p>
<Button
onClick={handleAddNewOrganization}
variant="primary"
size="lg"
className="flex items-center gap-2"
>
<Plus className="w-4 h-4" />
Crear Primera Organización
</Button>
</CardBody>
</Card>
)}
</div>
id={tenant.id}
statusIndicator={statusConfig}
title={tenant.name}
subtitle={tenant.business_type || 'Organización'}
primaryValue={isOwnerRole ? 'Propietario' : 'Miembro'}
primaryValueLabel=""
secondaryInfo={
tenant.city ? {
label: 'Ubicación',
value: tenant.city
} : undefined
}
metadata={[
`Creada ${formatDate(tenant.created_at)}`,
...(tenant.address ? [tenant.address] : []),
...(tenant.phone ? [tenant.phone] : [])
]}
onClick={() => {
if (!isActive) {
handleSwitchToTenant(tenant.id);
}
}}
actions={[
// Primary action - Switch or View Dashboard
{
label: isActive ? 'Ver Dashboard' : 'Cambiar',
icon: isActive ? Eye : ArrowRight,
variant: isActive ? 'outline' : 'primary',
priority: 'primary',
onClick: () => {
if (isActive) {
navigate('/app/dashboard');
} else {
handleSwitchToTenant(tenant.id);
}
}
},
// Settings action
{
label: 'Configuración',
icon: Settings,
priority: 'secondary',
onClick: () => {
if (!isActive) {
handleSwitchToTenant(tenant.id);
}
handleManageTenant();
}
},
// Team management - only for owners
...(isOwnerRole ? [{
label: 'Equipo',
icon: Users,
priority: 'secondary' as const,
highlighted: true,
onClick: () => {
if (!isActive) {
handleSwitchToTenant(tenant.id);
}
handleManageTeam();
}
}] : [])
]}
/>
);
})}
</div>
) : (
<EmptyState
icon={Building2}
title="No se encontraron organizaciones"
description={searchTerm || roleFilter
? "Intenta ajustar los filtros de búsqueda"
: "Crea tu primera organización para comenzar a usar Bakery IA"
}
actionLabel="Nueva Organización"
actionIcon={Plus}
onAction={handleAddNewOrganization}
/>
)}
</div>
);
};

View File

@@ -86,80 +86,20 @@ const SubscriptionPage: React.FC = () => {
subscriptionService.fetchAvailablePlans()
]);
// FIX: Handle demo mode or missing subscription data
// CRITICAL: No more mock data - show real errors instead
if (!usage || !usage.usage) {
// If no usage data, likely a demo tenant - create mock data
const mockUsage: UsageSummary = {
plan: 'starter',
status: 'active',
billing_cycle: 'monthly',
monthly_price: 0,
next_billing_date: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
usage: {
users: {
current: 1,
limit: 5,
unlimited: false,
usage_percentage: 20
},
locations: {
current: 1,
limit: 1,
unlimited: false,
usage_percentage: 100
},
products: {
current: 0,
limit: 50,
unlimited: false,
usage_percentage: 0
},
recipes: {
current: 0,
limit: 50,
unlimited: false,
usage_percentage: 0
},
suppliers: {
current: 0,
limit: 20,
unlimited: false,
usage_percentage: 0
},
training_jobs_today: {
current: 0,
limit: 1,
unlimited: false,
usage_percentage: 0
},
forecasts_today: {
current: 0,
limit: 10,
unlimited: false,
usage_percentage: 0
},
api_calls_this_hour: {
current: 0,
limit: 100,
unlimited: false,
usage_percentage: 0
},
file_storage_used_gb: {
current: 0,
limit: 5,
unlimited: false,
usage_percentage: 0
}
}
};
setUsageSummary(mockUsage);
} else {
setUsageSummary(usage);
throw new Error('No subscription found. Please contact support or create a new subscription.');
}
setUsageSummary(usage);
setAvailablePlans(plans);
} catch (error) {
console.error('Error loading subscription data:', error);
showToast.error("No se pudo cargar la información de suscripción");
showToast.error(
error instanceof Error && error.message.includes('No subscription')
? error.message
: "No se pudo cargar la información de suscripción. Por favor, contacte con soporte."
);
} finally {
setSubscriptionLoading(false);
}