Improve the frontend modals
This commit is contained in:
@@ -23,10 +23,9 @@ import { Button, Card, Avatar, Input, Select } from '../../../../components/ui';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '../../../../components/ui/Tabs';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { useToast } from '../../../../hooks/ui/useToast';
|
||||
import { useAuthUser, useAuthStore, useAuthActions } from '../../../../stores/auth.store';
|
||||
import { useAuthUser, useAuthActions } from '../../../../stores/auth.store';
|
||||
import { useAuthProfile, useUpdateProfile, useChangePassword } from '../../../../api/hooks/auth';
|
||||
import { useCurrentTenant } from '../../../../stores';
|
||||
import { subscriptionService } from '../../../../api';
|
||||
|
||||
// Import the communication preferences component
|
||||
import CommunicationPreferences, { type NotificationPreferences } from './CommunicationPreferences';
|
||||
@@ -52,7 +51,6 @@ const NewProfileSettingsPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { addToast } = useToast();
|
||||
const user = useAuthUser();
|
||||
const token = useAuthStore((state) => state.token);
|
||||
const { logout } = useAuthActions();
|
||||
const currentTenant = useCurrentTenant();
|
||||
|
||||
@@ -72,7 +70,6 @@ const NewProfileSettingsPage: React.FC = () => {
|
||||
const [deletePassword, setDeletePassword] = useState('');
|
||||
const [deleteReason, setDeleteReason] = useState('');
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [subscriptionStatus, setSubscriptionStatus] = useState<any>(null);
|
||||
|
||||
const [profileData, setProfileData] = useState<ProfileFormData>({
|
||||
first_name: '',
|
||||
@@ -106,22 +103,8 @@ const NewProfileSettingsPage: React.FC = () => {
|
||||
}
|
||||
}, [profile]);
|
||||
|
||||
// Load subscription status
|
||||
React.useEffect(() => {
|
||||
const loadSubscriptionStatus = async () => {
|
||||
const tenantId = currentTenant?.id || user?.tenant_id;
|
||||
if (tenantId) {
|
||||
try {
|
||||
const status = await subscriptionService.getSubscriptionStatus(tenantId);
|
||||
setSubscriptionStatus(status);
|
||||
} catch (error) {
|
||||
console.error('Failed to load subscription status:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadSubscriptionStatus();
|
||||
}, [currentTenant, user]);
|
||||
// Subscription status is not needed on the profile page
|
||||
// It's already shown in the subscription tab of the main ProfilePage
|
||||
|
||||
const languageOptions = [
|
||||
{ value: 'es', label: 'Español' },
|
||||
@@ -249,17 +232,11 @@ const NewProfileSettingsPage: React.FC = () => {
|
||||
const handleDataExport = async () => {
|
||||
setIsExporting(true);
|
||||
try {
|
||||
const response = await fetch('/api/v1/users/me/export', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
const { authService } = await import('../../../../api');
|
||||
const exportData = await authService.exportMyData();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to export data');
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
// Convert to blob and download
|
||||
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
@@ -290,23 +267,8 @@ const NewProfileSettingsPage: React.FC = () => {
|
||||
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
const response = await fetch('/api/v1/users/me/delete/request', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
confirm_email: deleteConfirmEmail,
|
||||
password: deletePassword,
|
||||
reason: deleteReason
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.detail || 'Failed to delete account');
|
||||
}
|
||||
const { authService } = await import('../../../../api');
|
||||
await authService.deleteAccount(deleteConfirmEmail, deletePassword, deleteReason);
|
||||
|
||||
addToast(t('common.success'), { type: 'success' });
|
||||
|
||||
@@ -717,22 +679,6 @@ const NewProfileSettingsPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{subscriptionStatus && subscriptionStatus.status === 'active' && (
|
||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertCircle className="w-5 h-5 text-yellow-600 dark:text-yellow-500 flex-shrink-0 mt-0.5" />
|
||||
<div className="text-sm">
|
||||
<p className="font-semibold text-yellow-900 dark:text-yellow-100 mb-1">
|
||||
Suscripción Activa Detectada
|
||||
</p>
|
||||
<p className="text-yellow-800 dark:text-yellow-200">
|
||||
Tienes una suscripción activa que se cancelará
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Input
|
||||
label="Confirma tu email"
|
||||
type="email"
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Users, Plus, Search, Shield, Trash2, Crown, UserCheck } from 'lucide-react';
|
||||
import { Button, StatusCard, getStatusColor, StatsGrid, SearchAndFilter, type FilterConfig } from '../../../../components/ui';
|
||||
import { Users, Plus, Search, Shield, Trash2, Crown, UserCheck, Eye, Activity } from 'lucide-react';
|
||||
import { Button, StatusCard, getStatusColor, StatsGrid, SearchAndFilter, type FilterConfig, EmptyState, EditViewModal } from '../../../../components/ui';
|
||||
import AddTeamMemberModal from '../../../../components/domain/team/AddTeamMemberModal';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { useTeamMembers, useAddTeamMember, useAddTeamMemberWithUserCreation, useRemoveTeamMember, useUpdateMemberRole, useTenantAccess } from '../../../../api/hooks/tenant';
|
||||
import { useUserActivity } from '../../../../api/hooks/user';
|
||||
import { userService } from '../../../../api/services/user';
|
||||
import { useAuthUser } from '../../../../stores/auth.store';
|
||||
import { useCurrentTenant, useCurrentTenantAccess } from '../../../../stores/tenant.store';
|
||||
import { useToast } from '../../../../hooks/ui/useToast';
|
||||
import { TENANT_ROLES } from '../../../../types/roles';
|
||||
import { TENANT_ROLES, type TenantRole } from '../../../../types/roles';
|
||||
import { subscriptionService } from '../../../../api/services/subscription';
|
||||
|
||||
const TeamPage: React.FC = () => {
|
||||
@@ -38,7 +40,18 @@ const TeamPage: React.FC = () => {
|
||||
const [selectedRole, setSelectedRole] = useState('all');
|
||||
const [showAddForm, setShowAddForm] = useState(false);
|
||||
const [selectedUserToAdd, setSelectedUserToAdd] = useState('');
|
||||
const [selectedRoleToAdd, setSelectedRoleToAdd] = useState<string>(TENANT_ROLES.MEMBER);
|
||||
const [selectedRoleToAdd, setSelectedRoleToAdd] = useState<TenantRole>(TENANT_ROLES.MEMBER);
|
||||
|
||||
// Modal state for team member details
|
||||
const [selectedMember, setSelectedMember] = useState<any>(null);
|
||||
const [showMemberModal, setShowMemberModal] = useState(false);
|
||||
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
|
||||
const [memberFormData, setMemberFormData] = useState<any>({});
|
||||
|
||||
// Modal state for activity view
|
||||
const [showActivityModal, setShowActivityModal] = useState(false);
|
||||
const [selectedMemberActivity, setSelectedMemberActivity] = useState<any>(null);
|
||||
const [activityLoading, setActivityLoading] = useState(false);
|
||||
|
||||
|
||||
// Enhanced team members that includes owner information
|
||||
@@ -96,6 +109,21 @@ const TeamPage: React.FC = () => {
|
||||
owners: enhancedTeamMembers.filter(m => m.role === TENANT_ROLES.OWNER).length,
|
||||
admins: enhancedTeamMembers.filter(m => m.role === TENANT_ROLES.ADMIN).length,
|
||||
members: enhancedTeamMembers.filter(m => m.role === TENANT_ROLES.MEMBER).length,
|
||||
uniqueRoles: new Set(enhancedTeamMembers.map(m => m.role)).size,
|
||||
averageDaysInTeam: (() => {
|
||||
// Only calculate for non-owner members to avoid skewing the average
|
||||
// Owners are added as placeholders with tenant creation date which skews the average
|
||||
const nonOwnerMembers = enhancedTeamMembers.filter(m => m.role !== TENANT_ROLES.OWNER);
|
||||
if (nonOwnerMembers.length === 0) return 0;
|
||||
|
||||
const totalDays = nonOwnerMembers.reduce((sum, m) => {
|
||||
const joinedDate = m.joined_at ? new Date(m.joined_at) : new Date();
|
||||
const days = Math.floor((Date.now() - joinedDate.getTime()) / (1000 * 60 * 24));
|
||||
return sum + days;
|
||||
}, 0);
|
||||
|
||||
return Math.round(totalDays / nonOwnerMembers.length);
|
||||
})()
|
||||
};
|
||||
|
||||
|
||||
@@ -151,21 +179,24 @@ const TeamPage: React.FC = () => {
|
||||
};
|
||||
};
|
||||
|
||||
const getMemberActions = (member: any) => {
|
||||
const getMemberActions = (member: any) => {
|
||||
const actions = [];
|
||||
|
||||
// Primary action - View details (always available)
|
||||
// This will be implemented in the future to show detailed member info modal
|
||||
// For now, we can comment it out as there's no modal yet
|
||||
// actions.push({
|
||||
// label: 'Ver Detalles',
|
||||
// icon: Eye,
|
||||
// priority: 'primary' as const,
|
||||
// onClick: () => {
|
||||
// // TODO: Implement member details modal
|
||||
// console.log('View member details:', member.user_id);
|
||||
// },
|
||||
// });
|
||||
// Primary action - View profile details
|
||||
actions.push({
|
||||
label: 'Ver Perfil',
|
||||
icon: Eye,
|
||||
onClick: () => handleViewMemberDetails(member),
|
||||
priority: 'primary' as const
|
||||
});
|
||||
|
||||
// Secondary action - View activity
|
||||
actions.push({
|
||||
label: 'Ver Actividad',
|
||||
icon: Activity,
|
||||
onClick: () => handleViewActivity(member),
|
||||
priority: 'secondary' as const
|
||||
});
|
||||
|
||||
// Contextual role change actions (only for non-owners and if user can manage team)
|
||||
if (canManageTeam && member.role !== TENANT_ROLES.OWNER) {
|
||||
@@ -204,7 +235,7 @@ const TeamPage: React.FC = () => {
|
||||
// Remove member action (only for owners and non-owner members)
|
||||
if (isOwner && member.role !== TENANT_ROLES.OWNER) {
|
||||
actions.push({
|
||||
label: 'Remover',
|
||||
label: 'Remover Miembro',
|
||||
icon: Trash2,
|
||||
onClick: () => {
|
||||
if (confirm('¿Estás seguro de que deseas remover este miembro?')) {
|
||||
@@ -217,6 +248,72 @@ const TeamPage: React.FC = () => {
|
||||
}
|
||||
|
||||
return actions;
|
||||
};
|
||||
|
||||
const handleViewMemberDetails = (member: any) => {
|
||||
setSelectedMember(member);
|
||||
setMemberFormData({
|
||||
full_name: member.user?.full_name || member.user_full_name || '',
|
||||
email: member.user?.email || member.user_email || '',
|
||||
phone: member.user?.phone || '',
|
||||
role: member.role,
|
||||
language: member.user?.language || 'es',
|
||||
timezone: member.user?.timezone || 'Europe/Madrid',
|
||||
is_active: member.is_active,
|
||||
joined_at: member.joined_at
|
||||
});
|
||||
setModalMode('view');
|
||||
setShowMemberModal(true);
|
||||
};
|
||||
|
||||
const handleEditMember = () => {
|
||||
setModalMode('edit');
|
||||
};
|
||||
|
||||
const handleSaveMember = async () => {
|
||||
// TODO: Implement member update logic
|
||||
console.log('Saving member:', memberFormData);
|
||||
setShowMemberModal(false);
|
||||
};
|
||||
|
||||
const handleFieldChange = (sectionIndex: number, fieldIndex: number, value: string | number) => {
|
||||
const fieldMap: Record<number, string> = {
|
||||
0: 'full_name',
|
||||
1: 'email',
|
||||
2: 'phone',
|
||||
3: 'role',
|
||||
4: 'language',
|
||||
5: 'timezone',
|
||||
6: 'is_active'
|
||||
};
|
||||
|
||||
const fieldName = fieldMap[fieldIndex];
|
||||
if (fieldName) {
|
||||
// Convert string boolean values back to actual booleans for 'is_active' field
|
||||
const processedValue = fieldName === 'is_active' ? value === 'true' : value;
|
||||
|
||||
setMemberFormData((prev: any) => ({
|
||||
...prev,
|
||||
[fieldName]: processedValue
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewActivity = async (member: any) => {
|
||||
const userId = member.user_id;
|
||||
if (!userId) return;
|
||||
|
||||
try {
|
||||
setActivityLoading(true);
|
||||
const activityData = await userService.getUserActivity(userId);
|
||||
setSelectedMemberActivity(activityData);
|
||||
setShowActivityModal(true);
|
||||
} catch (error) {
|
||||
console.error('Error fetching user activity:', error);
|
||||
addToast('Error al cargar la actividad del usuario', { type: 'error' });
|
||||
} finally {
|
||||
setActivityLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredMembers = enhancedTeamMembers.filter(member => {
|
||||
@@ -311,6 +408,7 @@ const TeamPage: React.FC = () => {
|
||||
canManageTeam ? [{
|
||||
id: 'add-member',
|
||||
label: 'Agregar Miembro',
|
||||
variant: "primary" as const,
|
||||
icon: Plus,
|
||||
onClick: () => setShowAddForm(true)
|
||||
}] : undefined
|
||||
@@ -343,9 +441,21 @@ const TeamPage: React.FC = () => {
|
||||
value: teamStats.owners,
|
||||
icon: Crown,
|
||||
variant: "purple"
|
||||
},
|
||||
{
|
||||
title: "Roles Únicos",
|
||||
value: teamStats.uniqueRoles,
|
||||
icon: Users,
|
||||
variant: "info"
|
||||
},
|
||||
{
|
||||
title: "Días Promedio",
|
||||
value: teamStats.averageDaysInTeam,
|
||||
icon: UserCheck,
|
||||
variant: "info"
|
||||
}
|
||||
]}
|
||||
columns={4}
|
||||
columns={3}
|
||||
gap="md"
|
||||
/>
|
||||
|
||||
@@ -414,29 +524,18 @@ const TeamPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{filteredMembers.length === 0 && (
|
||||
<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'
|
||||
<EmptyState
|
||||
icon={Users}
|
||||
title="No se encontraron miembros"
|
||||
description={
|
||||
searchTerm || selectedRole !== 'all'
|
||||
? "No hay miembros que coincidan con los filtros seleccionados"
|
||||
: "Este tenant aún no tiene miembros del equipo"
|
||||
}
|
||||
</p>
|
||||
{canManageTeam && (
|
||||
<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>
|
||||
}
|
||||
actionLabel={canManageTeam ? "Agregar Primer Miembro" : undefined}
|
||||
actionIcon={canManageTeam ? Plus : undefined}
|
||||
onAction={canManageTeam ? () => setShowAddForm(true) : undefined}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Add Member Modal - Using StatusModal */}
|
||||
@@ -452,7 +551,7 @@ const TeamPage: React.FC = () => {
|
||||
|
||||
try {
|
||||
// Check subscription limits before adding member
|
||||
const usageCheck = await subscriptionService.checkUsageLimit(tenantId, 'users', 1);
|
||||
const usageCheck = await subscriptionService.checkQuotaLimit(tenantId, 'users', 1);
|
||||
|
||||
if (!usageCheck.allowed) {
|
||||
const errorMessage = usageCheck.message ||
|
||||
@@ -461,6 +560,10 @@ const TeamPage: React.FC = () => {
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
// The AddTeamMemberModal returns a string role, but it's always one of the tenant roles
|
||||
// Since the modal only allows MEMBER, ADMIN, VIEWER (no OWNER), we can safely cast it
|
||||
const role = userData.role as typeof TENANT_ROLES.MEMBER | typeof TENANT_ROLES.ADMIN | typeof TENANT_ROLES.VIEWER;
|
||||
|
||||
// Use appropriate mutation based on whether we're creating a user
|
||||
if (userData.createUser) {
|
||||
await addMemberWithUserMutation.mutateAsync({
|
||||
@@ -471,7 +574,7 @@ const TeamPage: React.FC = () => {
|
||||
full_name: userData.fullName!,
|
||||
password: userData.password!,
|
||||
phone: userData.phone,
|
||||
role: userData.role,
|
||||
role,
|
||||
language: 'es',
|
||||
timezone: 'Europe/Madrid'
|
||||
}
|
||||
@@ -481,7 +584,7 @@ const TeamPage: React.FC = () => {
|
||||
await addMemberMutation.mutateAsync({
|
||||
tenantId,
|
||||
userId: userData.userId!,
|
||||
role: userData.role,
|
||||
role,
|
||||
});
|
||||
addToast('Miembro agregado exitosamente', { type: 'success' });
|
||||
}
|
||||
@@ -503,8 +606,197 @@ const TeamPage: React.FC = () => {
|
||||
}}
|
||||
availableUsers={[]}
|
||||
/>
|
||||
|
||||
{/* Team Member Details Modal */}
|
||||
<EditViewModal
|
||||
isOpen={showMemberModal}
|
||||
onClose={() => setShowMemberModal(false)}
|
||||
mode={modalMode}
|
||||
onModeChange={setModalMode}
|
||||
title={memberFormData.full_name || 'Miembro del Equipo'}
|
||||
subtitle={memberFormData.email}
|
||||
statusIndicator={selectedMember ? getMemberStatusConfig(selectedMember) : undefined}
|
||||
sections={[
|
||||
{
|
||||
title: 'Información Personal',
|
||||
icon: Users,
|
||||
fields: [
|
||||
{
|
||||
label: 'Nombre Completo',
|
||||
value: memberFormData.full_name,
|
||||
type: 'text',
|
||||
editable: true,
|
||||
required: true,
|
||||
placeholder: 'Introduce el nombre completo',
|
||||
span: 2
|
||||
},
|
||||
{
|
||||
label: 'Email',
|
||||
value: memberFormData.email,
|
||||
type: 'email',
|
||||
editable: true,
|
||||
required: true,
|
||||
placeholder: 'Introduce el email',
|
||||
span: 2
|
||||
},
|
||||
{
|
||||
label: 'Teléfono',
|
||||
value: memberFormData.phone,
|
||||
type: 'tel',
|
||||
editable: true,
|
||||
placeholder: 'Introduce el teléfono',
|
||||
span: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Configuración de Cuenta',
|
||||
icon: Shield,
|
||||
fields: [
|
||||
{
|
||||
label: 'Rol',
|
||||
value: getRoleLabel(memberFormData.role),
|
||||
type: 'select',
|
||||
editable: modalMode === 'edit',
|
||||
options: [
|
||||
{ label: 'Miembro - Acceso estándar', value: TENANT_ROLES.MEMBER },
|
||||
{ label: 'Administrador - Gestión de equipo', value: TENANT_ROLES.ADMIN },
|
||||
{ label: 'Observador - Solo lectura', value: TENANT_ROLES.VIEWER }
|
||||
],
|
||||
span: 2
|
||||
},
|
||||
{
|
||||
label: 'Idioma',
|
||||
value: memberFormData.language?.toUpperCase() || 'ES',
|
||||
type: 'select',
|
||||
editable: modalMode === 'edit',
|
||||
options: [
|
||||
{ label: 'Español', value: 'es' },
|
||||
{ label: 'English', value: 'en' },
|
||||
{ label: 'Euskera', value: 'eu' }
|
||||
],
|
||||
span: 2
|
||||
},
|
||||
{
|
||||
label: 'Zona Horaria',
|
||||
value: memberFormData.timezone || 'Europe/Madrid',
|
||||
type: 'select',
|
||||
editable: modalMode === 'edit',
|
||||
options: [
|
||||
{ label: 'Madrid (CET)', value: 'Europe/Madrid' },
|
||||
{ label: 'London (GMT)', value: 'Europe/London' },
|
||||
{ label: 'New York (EST)', value: 'America/New_York' }
|
||||
],
|
||||
span: 2
|
||||
},
|
||||
{
|
||||
label: 'Estado',
|
||||
value: memberFormData.is_active ? 'Activo' : 'Inactivo',
|
||||
type: 'select',
|
||||
editable: modalMode === 'edit',
|
||||
options: [
|
||||
{ label: 'Activo', value: 'true' },
|
||||
{ label: 'Inactivo', value: 'false' }
|
||||
],
|
||||
span: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Detalles del Equipo',
|
||||
icon: UserCheck,
|
||||
fields: [
|
||||
{
|
||||
label: 'Fecha de Ingreso',
|
||||
value: memberFormData.joined_at,
|
||||
type: 'date',
|
||||
editable: false,
|
||||
span: 2
|
||||
},
|
||||
{
|
||||
label: 'Días en el Equipo',
|
||||
value: selectedMember ? Math.floor((Date.now() - new Date(selectedMember.joined_at).getTime()) / (1000 * 60 * 60 * 24)) : 0,
|
||||
type: 'number',
|
||||
editable: false,
|
||||
span: 2
|
||||
},
|
||||
{
|
||||
label: 'ID de Usuario',
|
||||
value: selectedMember?.user_id || 'N/A',
|
||||
type: 'text',
|
||||
editable: false,
|
||||
span: 2
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
onFieldChange={handleFieldChange}
|
||||
onEdit={handleEditMember}
|
||||
onSave={handleSaveMember}
|
||||
size="lg"
|
||||
/>
|
||||
|
||||
{/* Activity Modal */}
|
||||
<EditViewModal
|
||||
isOpen={showActivityModal}
|
||||
onClose={() => setShowActivityModal(false)}
|
||||
mode="view"
|
||||
title="Actividad del Usuario"
|
||||
subtitle={selectedMemberActivity?.user_id ? `ID: ${selectedMemberActivity.user_id}` : ''}
|
||||
sections={[
|
||||
{
|
||||
title: 'Información Básica',
|
||||
icon: Activity,
|
||||
fields: [
|
||||
{
|
||||
label: 'Estado de Cuenta',
|
||||
value: selectedMemberActivity?.is_active ? 'Activa' : 'Inactiva',
|
||||
type: 'text',
|
||||
span: 2
|
||||
},
|
||||
{
|
||||
label: 'Estado de Verificación',
|
||||
value: selectedMemberActivity?.is_verified ? 'Verificada' : 'No verificada',
|
||||
type: 'text',
|
||||
span: 2
|
||||
},
|
||||
{
|
||||
label: 'Fecha de Creación',
|
||||
value: selectedMemberActivity?.account_created ? new Date(selectedMemberActivity.account_created).toLocaleDateString('es-ES') : 'N/A',
|
||||
type: 'text',
|
||||
span: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Actividad Reciente',
|
||||
icon: Activity,
|
||||
fields: [
|
||||
{
|
||||
label: 'Último Inicio de Sesión',
|
||||
value: selectedMemberActivity?.last_login ? new Date(selectedMemberActivity.last_login).toLocaleString('es-ES') : 'Nunca',
|
||||
type: 'text',
|
||||
span: 2
|
||||
},
|
||||
{
|
||||
label: 'Última Actividad',
|
||||
value: selectedMemberActivity?.last_activity ? new Date(selectedMemberActivity.last_activity).toLocaleString('es-ES') : 'N/A',
|
||||
type: 'text',
|
||||
span: 2
|
||||
},
|
||||
{
|
||||
label: 'Sesiones Activas',
|
||||
value: selectedMemberActivity?.active_sessions || 0,
|
||||
type: 'number',
|
||||
span: 2
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamPage;
|
||||
export default TeamPage;
|
||||
|
||||
Reference in New Issue
Block a user