Improve the production frontend

This commit is contained in:
Urtzi Alfaro
2025-09-21 07:45:19 +02:00
parent 5e941f5f03
commit 13ca3e90b4
21 changed files with 1416 additions and 1357 deletions

View File

@@ -1,6 +1,7 @@
import React, { useState, useMemo } from 'react';
import { Users, Plus, Search, Mail, Phone, Shield, Trash2, Crown, X, UserCheck } from 'lucide-react';
import { Button, Card, Badge, Input, StatusCard, getStatusColor } from '../../../../components/ui';
import { Button, Card, Badge, Input, StatusCard, getStatusColor, StatsGrid } from '../../../../components/ui';
import AddTeamMemberModal from '../../../../components/domain/team/AddTeamMemberModal';
import { PageHeader } from '../../../../components/layout';
import { useTeamMembers, useAddTeamMember, useRemoveTeamMember, useUpdateMemberRole } from '../../../../api/hooks/tenant';
import { useAllUsers } from '../../../../api/hooks/user';
@@ -285,55 +286,36 @@ const TeamPage: React.FC = () => {
/>
{/* Team Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Total Equipo</p>
<p className="text-3xl font-bold text-[var(--text-primary)]">{teamStats.total}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
<Users className="h-6 w-6 text-[var(--color-info)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Activos</p>
<p className="text-3xl font-bold text-[var(--color-success)]">{teamStats.active}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
<UserCheck className="h-6 w-6 text-[var(--color-success)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Administradores</p>
<p className="text-3xl font-bold text-[var(--color-primary)]">{teamStats.admins}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center">
<Shield className="h-6 w-6 text-[var(--color-primary)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Propietarios</p>
<p className="text-3xl font-bold text-purple-600">{teamStats.owners}</p>
</div>
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
<Crown className="h-6 w-6 text-purple-600" />
</div>
</div>
</Card>
</div>
<StatsGrid
stats={[
{
title: "Total Equipo",
value: teamStats.total,
icon: Users,
variant: "info"
},
{
title: "Activos",
value: teamStats.active,
icon: UserCheck,
variant: "success"
},
{
title: "Administradores",
value: teamStats.admins,
icon: Shield,
variant: "info"
},
{
title: "Propietarios",
value: teamStats.owners,
icon: Crown,
variant: "purple"
}
]}
columns={4}
gap="md"
/>
{/* Filters and Search */}
<Card className="p-6">
@@ -368,6 +350,21 @@ const TeamPage: React.FC = () => {
</div>
</Card>
{/* Add Member Button */}
{canManageTeam && availableUsers.length > 0 && filteredMembers.length > 0 && (
<div className="flex justify-end">
<Button
onClick={() => setShowAddForm(true)}
variant="primary"
size="md"
className="font-medium px-4 py-2 shadow-sm hover:shadow-md transition-all duration-200"
>
<Plus className="w-4 h-4 mr-2 flex-shrink-0" />
<span>Agregar Miembro</span>
</Button>
</div>
)}
{/* Team Members List - Responsive grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4 lg:gap-6">
{filteredMembers.map((member) => (
@@ -398,124 +395,58 @@ const TeamPage: React.FC = () => {
</div>
{filteredMembers.length === 0 && (
<StatusCard
id="empty-state"
statusIndicator={{
color: getStatusColor('pending'),
text: searchTerm || selectedRole !== 'all' ? 'Sin coincidencias' : 'Equipo vacío',
icon: Users,
isCritical: false,
isHighlight: false
}}
title="No se encontraron miembros"
subtitle={searchTerm || selectedRole !== 'all'
? "No hay miembros que coincidan con los filtros seleccionados"
: "Este tenant aún no tiene miembros del equipo"
}
primaryValue="0"
primaryValueLabel="Miembros"
actions={canManageTeam && availableUsers.length > 0 ? [{
label: 'Agregar Primer Miembro',
icon: Plus,
onClick: () => setShowAddForm(true),
priority: 'primary' as const,
}] : []}
className="col-span-full"
/>
)}
{/* Add Member Modal */}
{showAddForm && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<Card className="p-6 max-w-md w-full mx-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">Agregar Miembro al Equipo</h3>
<Button
size="sm"
variant="ghost"
onClick={() => setShowAddForm(false)}
>
<X className="w-4 h-4" />
</Button>
</div>
<div className="space-y-4">
{/* User Selection */}
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
Usuario
</label>
<select
value={selectedUserToAdd}
onChange={(e) => setSelectedUserToAdd(e.target.value)}
className="w-full px-3 py-2 border border-border-secondary rounded-lg bg-bg-primary focus:outline-none focus:ring-2 focus:ring-color-primary focus:ring-opacity-20"
required
>
<option value="">Seleccionar usuario...</option>
{availableUsers.map(user => (
<option key={user.id} value={user.id}>
{user.full_name} ({user.email})
</option>
))}
</select>
{availableUsers.length === 0 && (
<p className="text-sm text-[var(--text-tertiary)] mt-1">
No hay usuarios disponibles para agregar
</p>
)}
</div>
{/* Role Selection */}
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
Rol
</label>
<select
value={selectedRoleToAdd}
onChange={(e) => setSelectedRoleToAdd(e.target.value)}
className="w-full px-3 py-2 border border-border-secondary rounded-lg bg-bg-primary focus:outline-none focus:ring-2 focus:ring-color-primary focus:ring-opacity-20"
>
<option value={TENANT_ROLES.MEMBER}>Miembro - Acceso estándar</option>
<option value={TENANT_ROLES.ADMIN}>Administrador - Gestión de equipo</option>
<option value={TENANT_ROLES.VIEWER}>Observador - Solo lectura</option>
</select>
</div>
{/* Role Description */}
<div className="p-3 bg-bg-secondary rounded-lg">
<p className="text-xs text-[var(--text-secondary)]">
{selectedRoleToAdd === TENANT_ROLES.ADMIN &&
'Los administradores pueden gestionar miembros del equipo y configuraciones.'}
{selectedRoleToAdd === TENANT_ROLES.MEMBER &&
'Los miembros tienen acceso completo para trabajar con datos y funcionalidades.'}
{selectedRoleToAdd === TENANT_ROLES.VIEWER &&
'Los observadores solo pueden ver datos, sin realizar cambios.'}
</p>
</div>
</div>
<div className="flex space-x-2 mt-6">
<Button
onClick={handleAddMember}
disabled={!selectedUserToAdd || addMemberMutation.isPending}
className="flex-1"
>
{addMemberMutation.isPending ? 'Agregando...' : 'Agregar Miembro'}
</Button>
<Button
variant="outline"
onClick={() => {
setShowAddForm(false);
setSelectedUserToAdd('');
setSelectedRoleToAdd(TENANT_ROLES.MEMBER);
}}
>
Cancelar
</Button>
</div>
</Card>
<div className="text-center py-12">
<Users className="mx-auto h-12 w-12 text-[var(--text-tertiary)] mb-4" />
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
No se encontraron miembros
</h3>
<p className="text-[var(--text-secondary)] mb-4">
{searchTerm || selectedRole !== 'all'
? "No hay miembros que coincidan con los filtros seleccionados"
: "Este tenant aún no tiene miembros del equipo"
}
</p>
{canManageTeam && availableUsers.length > 0 && (
<Button
onClick={() => setShowAddForm(true)}
variant="primary"
size="md"
className="font-medium px-6 py-3 shadow-sm hover:shadow-md transition-all duration-200"
>
<Plus className="w-4 h-4 mr-2 flex-shrink-0" />
<span>Agregar Primer Miembro</span>
</Button>
)}
</div>
)}
{/* Add Member Modal - Using StatusModal */}
<AddTeamMemberModal
isOpen={showAddForm}
onClose={() => {
setShowAddForm(false);
setSelectedUserToAdd('');
setSelectedRoleToAdd(TENANT_ROLES.MEMBER);
}}
onAddMember={async (userData) => {
if (!tenantId) return Promise.reject('No tenant ID available');
return addMemberMutation.mutateAsync({
tenantId,
userId: userData.userId,
role: userData.role,
}).then(() => {
addToast('Miembro agregado exitosamente', { type: 'success' });
setShowAddForm(false);
setSelectedUserToAdd('');
setSelectedRoleToAdd(TENANT_ROLES.MEMBER);
}).catch((error) => {
addToast('Error al agregar miembro', { type: 'error' });
throw error;
});
}}
availableUsers={availableUsers}
/>
</div>
);
};