Improve the production frontend
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user