🚀 Initial commit: Versão atual do TrackSteel APP

This commit is contained in:
2026-03-18 21:17:53 +00:00
commit bde410c9ad
633 changed files with 108150 additions and 0 deletions
+190
View File
@@ -0,0 +1,190 @@
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from '@/components/ui/table';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Plus, Edit, Trash2 } from 'lucide-react';
import { UserFunction } from '@/hooks/useUserManagement';
interface FunctionsManagerProps {
functions: UserFunction[];
onCreate: (data: { name: string; description?: string }) => void;
onUpdate: (id: string, data: { name: string; description?: string }) => void;
onDelete: (id: string) => void;
}
export function FunctionsManager({ functions, onCreate, onUpdate, onDelete }: FunctionsManagerProps) {
const [showModal, setShowModal] = useState(false);
const [editingFunction, setEditingFunction] = useState<UserFunction | null>(null);
const [formData, setFormData] = useState({ name: '', description: '' });
const handleOpenModal = (func?: UserFunction) => {
if (func) {
setEditingFunction(func);
setFormData({ name: func.name, description: func.description || '' });
} else {
setEditingFunction(null);
setFormData({ name: '', description: '' });
}
setShowModal(true);
};
const handleCloseModal = () => {
setShowModal(false);
setEditingFunction(null);
setFormData({ name: '', description: '' });
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (editingFunction) {
onUpdate(editingFunction.id, formData);
} else {
onCreate(formData);
}
handleCloseModal();
};
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h3 className="text-lg font-semibold text-foreground">Gerenciar Funções</h3>
<p className="text-sm text-muted-foreground">
Gerencie as funções disponíveis no sistema
</p>
</div>
<Button
onClick={() => handleOpenModal()}
className="bg-green-600 hover:bg-green-700"
>
<Plus className="w-4 h-4 mr-2" />
Nova Função
</Button>
</div>
<Card>
<CardContent className="p-0">
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Nome</TableHead>
<TableHead className="hidden sm:table-cell">Descrição</TableHead>
<TableHead className="hidden md:table-cell">Criado em</TableHead>
<TableHead>Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{functions.map((func) => (
<TableRow key={func.id}>
<TableCell className="min-w-0">
<div className="font-medium text-xs sm:text-sm">{func.name}</div>
{/* Mobile: Show description and date on small screens */}
<div className="sm:hidden space-y-1 mt-1">
<div className="text-xs text-muted-foreground">
{func.description || 'Sem descrição'}
</div>
<div className="text-xs text-muted-foreground md:hidden">
Criado: {new Date(func.created_at).toLocaleDateString('pt-BR')}
</div>
</div>
</TableCell>
<TableCell className="hidden sm:table-cell">
<span className="text-xs text-muted-foreground">{func.description || '-'}</span>
</TableCell>
<TableCell className="hidden md:table-cell">
<span className="text-xs text-muted-foreground">{new Date(func.created_at).toLocaleDateString('pt-BR')}</span>
</TableCell>
<TableCell>
<div className="flex gap-1">
<Button
size="sm"
variant="ghost"
onClick={() => handleOpenModal(func)}
className="text-blue-600 hover:text-blue-700 hover:bg-blue-50 dark:text-blue-400 dark:hover:text-blue-300 dark:hover:bg-blue-900/50 p-1 h-6 w-6"
>
<Edit className="w-3 h-3" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => onDelete(func.id)}
className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:text-red-400 dark:hover:text-red-300 dark:hover:bg-red-900/50 p-1 h-6 w-6"
>
<Trash2 className="w-3 h-3" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
{/* Modal */}
{showModal && (
<Dialog open={true} onOpenChange={handleCloseModal}>
<DialogContent>
<DialogHeader>
<DialogTitle>
{editingFunction ? 'Editar Função' : 'Nova Função'}
</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<Label>Nome</Label>
<Input
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
placeholder="Digite o nome da função"
required
/>
</div>
<div>
<Label>Descrição</Label>
<Textarea
value={formData.description}
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
placeholder="Digite a descrição da função"
rows={3}
/>
</div>
<div className="flex gap-2 pt-4">
<Button type="submit">
Salvar
</Button>
<Button type="button" variant="ghost" onClick={handleCloseModal}>
Cancelar
</Button>
</div>
</form>
</DialogContent>
</Dialog>
)}
</div>
);
}
@@ -0,0 +1,90 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Mail, Clock, User } from 'lucide-react';
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { format } from 'date-fns';
import { ptBR } from 'date-fns/locale';
export const PasswordResetRequests = () => {
const { data: passwordResetRequests = [], isLoading } = useQuery({
queryKey: ['password-reset-requests'],
queryFn: async () => {
const { data, error } = await supabase
.from('password_reset_requests')
.select('*')
.order('requested_at', { ascending: false });
if (error) throw error;
return data || [];
},
});
if (isLoading) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Mail className="h-5 w-5" />
Solicitações de Redefinição de Senha
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-center py-4">
<div className="text-sm text-muted-foreground">Carregando...</div>
</div>
</CardContent>
</Card>
);
}
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Mail className="h-5 w-5" />
Solicitações de Redefinição de Senha
</CardTitle>
</CardHeader>
<CardContent>
{passwordResetRequests.length === 0 ? (
<div className="text-center py-4 text-sm text-muted-foreground">
Nenhuma solicitação de redefinição de senha encontrada.
</div>
) : (
<div className="space-y-3">
{passwordResetRequests.map((request) => (
<div
key={request.id}
className="flex items-center justify-between p-3 border rounded-lg"
>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<User className="h-4 w-4 text-muted-foreground" />
<div>
<div className="font-medium text-sm">{request.email}</div>
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Clock className="h-3 w-3" />
{format(new Date(request.requested_at), "dd/MM/yyyy 'às' HH:mm", {
locale: ptBR,
})}
</div>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Badge
variant={request.status === 'pending' ? 'default' : 'secondary'}
>
{request.status === 'pending' ? 'Pendente' : request.status}
</Badge>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
);
};
@@ -0,0 +1,95 @@
import { Button } from '@/components/ui/button';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from '@/components/ui/table';
import { Check, X } from 'lucide-react';
import { UserProfile } from '@/hooks/useUserManagement';
import { UserAvatar } from '@/components/ui/user-avatar';
interface PendingUsersTableProps {
users: UserProfile[];
onApprove: (userId: string) => void;
onReject: (userId: string) => void;
}
export function PendingUsersTable({ users, onApprove, onReject }: PendingUsersTableProps) {
if (users.length === 0) {
return (
<div className="text-center py-8 text-slate-400">
Nenhuma solicitação pendente
</div>
);
}
return (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow className="border-slate-700">
<TableHead className="text-slate-300">Usuário</TableHead>
<TableHead className="text-slate-300 hidden sm:table-cell">Data da Solicitação</TableHead>
<TableHead className="text-slate-300">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map((user) => (
<TableRow key={user.id} className="border-slate-700">
<TableCell className="min-w-0">
<div className="flex items-center gap-2">
<UserAvatar
imageUrl={user.profile_image_url}
name={user.full_name}
email={user.email}
size="sm"
/>
<div className="min-w-0 flex-1">
<div className="text-white font-medium truncate text-xs sm:text-sm">
{user.full_name || 'Nome não informado'}
</div>
<div className="text-slate-400 text-xs truncate">{user.email}</div>
{/* Mobile: Show date on small screens */}
<div className="sm:hidden text-xs text-slate-300 mt-1">
Solicitação: {new Date(user.requested_at).toLocaleDateString('pt-BR')}
</div>
</div>
</div>
</TableCell>
<TableCell className="text-slate-300 hidden sm:table-cell">
<span className="text-xs">{new Date(user.requested_at).toLocaleDateString('pt-BR')}</span>
</TableCell>
<TableCell>
<div className="flex gap-1 flex-col sm:flex-row">
<Button
size="sm"
onClick={() => onApprove(user.id)}
className="bg-green-600 hover:bg-green-700 text-white h-6 text-xs px-2"
>
<Check className="w-3 h-3 mr-1" />
<span className="hidden sm:inline">Aprovar</span>
<span className="sm:hidden">✓</span>
</Button>
<Button
size="sm"
variant="destructive"
onClick={() => onReject(user.id)}
className="bg-red-600 hover:bg-red-700 h-6 text-xs px-2"
>
<X className="w-3 h-3 mr-1" />
<span className="hidden sm:inline">Rejeitar</span>
<span className="sm:hidden">✗</span>
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
+195
View File
@@ -0,0 +1,195 @@
import React, { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Plus } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
import { Privilege, FUNCTIONAL_PERMISSIONS } from './privileges/types';
import { PrivilegeForm } from './privileges/PrivilegeForm';
import { PrivilegeCard } from './privileges/PrivilegeCard';
interface PrivilegesManagerProps {
privileges: Privilege[];
onCreate: (data: { name: string; description?: string; permissions: Record<string, boolean> }) => Promise<void>;
onUpdate: (id: string, data: { name: string; description?: string; permissions: Record<string, boolean> }) => Promise<void>;
onDelete: (id: string) => Promise<void>;
}
export function PrivilegesManager({ privileges, onCreate, onUpdate, onDelete }: PrivilegesManagerProps) {
const [editingPrivilege, setEditingPrivilege] = useState<Privilege | null>(null);
const [showForm, setShowForm] = useState(false);
const [formData, setFormData] = useState({
name: '',
description: '',
permissions: {} as Record<string, boolean>
});
const { toast } = useToast();
const resetForm = () => {
setFormData({
name: '',
description: '',
permissions: {}
});
setEditingPrivilege(null);
setShowForm(false);
};
// Function to clean permissions and keep only valid functional ones
const cleanPermissions = (permissions: Record<string, boolean>): Record<string, boolean> => {
const cleanedPermissions: Record<string, boolean> = {};
// Keep only valid functional permissions
FUNCTIONAL_PERMISSIONS.forEach(perm => {
cleanedPermissions[perm.key] = permissions[perm.key] || false;
});
return cleanedPermissions;
};
const handleEdit = (privilege: Privilege) => {
console.log('Editing privilege:', privilege.name);
setEditingPrivilege(privilege);
// Clean old permissions and keep only functional ones
const cleanedPermissions = cleanPermissions(privilege.permissions || {});
setFormData({
name: privilege.name,
description: privilege.description || '',
permissions: cleanedPermissions
});
setShowForm(true);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Ensure only one functional permission is active
const activePermissions = Object.entries(formData.permissions).filter(([, value]) => value);
if (activePermissions.length > 1) {
toast({
title: "Erro de validação",
description: "Apenas uma permissão funcional pode ser selecionada por privilégio.",
variant: "destructive"
});
return;
}
if (activePermissions.length === 0) {
toast({
title: "Erro de validação",
description: "Pelo menos uma permissão funcional deve ser selecionada.",
variant: "destructive"
});
return;
}
console.log('Submitting privilege:', {
name: formData.name,
functionalPermissions: formData.permissions
});
try {
if (editingPrivilege) {
await onUpdate(editingPrivilege.id, formData);
toast({ title: "Privilégio atualizado com sucesso!" });
} else {
await onCreate(formData);
toast({ title: "Privilégio criado com sucesso!" });
}
resetForm();
} catch (error) {
console.error('Error saving privilege:', error);
toast({
title: "Erro ao salvar privilégio",
description: "Verifique os dados e tente novamente.",
variant: "destructive"
});
}
};
const handleDelete = async (id: string) => {
if (window.confirm('Tem certeza que deseja excluir este privilégio?')) {
try {
await onDelete(id);
toast({ title: "Privilégio excluído com sucesso!" });
} catch (error) {
console.error('Error deleting privilege:', error);
toast({
title: "Erro ao excluir privilégio",
variant: "destructive"
});
}
}
};
const handlePermissionChange = (permissionKey: string, checked: boolean) => {
if (checked) {
// Implement radio button behavior - only one functional permission at a time
const newPermissions = FUNCTIONAL_PERMISSIONS.reduce((acc, perm) => {
acc[perm.key] = perm.key === permissionKey;
return acc;
}, {} as Record<string, boolean>);
setFormData(prev => ({
...prev,
permissions: newPermissions
}));
} else {
setFormData(prev => ({
...prev,
permissions: {
...prev.permissions,
[permissionKey]: false
}
}));
}
};
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h3 className="text-lg font-semibold">Privilégios do Sistema</h3>
<p className="text-sm text-muted-foreground">
Gerencie os privilégios funcionais dos usuários (sistema simplificado)
</p>
</div>
<Button onClick={() => setShowForm(true)}>
<Plus className="w-4 h-4 mr-2" />
Novo Privilégio
</Button>
</div>
{showForm && (
<Card>
<CardHeader>
<CardTitle>Privilégios</CardTitle>
</CardHeader>
<CardContent>
<PrivilegeForm
formData={formData}
onFormDataChange={setFormData}
onPermissionChange={handlePermissionChange}
onSubmit={handleSubmit}
onCancel={resetForm}
isEditing={!!editingPrivilege}
/>
</CardContent>
</Card>
)}
<div className="grid gap-4">
{privileges.map((privilege) => (
<PrivilegeCard
key={privilege.id}
privilege={privilege}
onEdit={handleEdit}
onDelete={handleDelete}
/>
))}
</div>
</div>
);
}
+517
View File
@@ -0,0 +1,517 @@
import { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Badge } from '@/components/ui/badge';
import { Download, FileText, BarChart3, Users, Clock, Activity, AlertCircle } from 'lucide-react';
import { format } from 'date-fns';
import { ptBR } from 'date-fns/locale';
import { useSessionLogs } from '@/hooks/useSessionLogs';
import { UserProfile } from '@/hooks/useUserManagement';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, LineChart, Line } from 'recharts';
import { toast } from 'sonner';
interface SessionLogsManagerProps {
users: UserProfile[];
}
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884D8'];
export function SessionLogsManager({ users }: SessionLogsManagerProps) {
const {
sessionLogs,
sessionStats,
loading,
fetchSessionLogs,
fetchSessionStats,
exportReport
} = useSessionLogs();
const [filters, setFilters] = useState({
userId: '',
period: 'week' as 'day' | 'week' | 'month',
startDate: '',
endDate: ''
});
const [activeView, setActiveView] = useState<'logs' | 'dashboard'>('logs');
useEffect(() => {
const loadData = async () => {
try {
await fetchSessionLogs();
await fetchSessionStats({ period: filters.period });
} catch (error) {
console.error('Erro ao carregar dados dos logs de sessão:', error);
toast.error('Erro ao carregar logs de sessão');
}
};
loadData();
}, []);
const handleFilterChange = (key: string, value: string) => {
setFilters(prev => ({ ...prev, [key]: value }));
};
const applyFilters = async () => {
try {
const filterParams = {
userId: filters.userId || undefined,
startDate: filters.startDate || undefined,
endDate: filters.endDate || undefined
};
await fetchSessionLogs(filterParams);
await fetchSessionStats({
userId: filters.userId || undefined,
period: filters.period
});
toast.success('Filtros aplicados com sucesso');
} catch (error) {
console.error('Erro ao aplicar filtros:', error);
toast.error('Erro ao aplicar filtros');
}
};
const resetFilters = async () => {
try {
setFilters({
userId: '',
period: 'week',
startDate: '',
endDate: ''
});
await fetchSessionLogs();
await fetchSessionStats({ period: 'week' });
toast.success('Filtros limpos com sucesso');
} catch (error) {
console.error('Erro ao limpar filtros:', error);
toast.error('Erro ao limpar filtros');
}
};
const getUserName = (userId: string) => {
const user = users.find(u => u.id === userId);
return user?.full_name || user?.email || 'Usuário desconhecido';
};
const formatDuration = (minutes: number | null) => {
if (!minutes) return '0min';
const hours = Math.floor(minutes / 60);
const mins = Math.round(minutes % 60);
return hours > 0 ? `${hours}h ${mins}min` : `${mins}min`;
};
// Dados para gráficos
const getDashboardData = () => {
if (!sessionLogs.length) {
return { userSessions: [], dailyActivity: [], timeRangeData: [] };
}
// Sessões por usuário
const userSessions = users.map(user => {
const userLogs = sessionLogs.filter(log => log.user_id === user.id);
return {
name: user.full_name?.split(' ')[0] || user.email.split('@')[0],
sessions: userLogs.length,
totalTime: userLogs.reduce((acc, log) => acc + (log.duration_minutes || 0), 0)
};
}).filter(data => data.sessions > 0);
// Atividade por dia (últimos 7 dias)
const last7Days = Array.from({ length: 7 }, (_, i) => {
const date = new Date();
date.setDate(date.getDate() - i);
return date.toISOString().split('T')[0];
}).reverse();
const dailyActivity = last7Days.map(date => {
const dayLogs = sessionLogs.filter(log =>
log.session_start.split('T')[0] === date
);
return {
date: format(new Date(date), 'dd/MM', { locale: ptBR }),
sessions: dayLogs.length,
users: new Set(dayLogs.map(log => log.user_id)).size
};
});
// Duração média por período do dia
const timeRanges = [
{ name: 'Manhã (6-12h)', start: 6, end: 12 },
{ name: 'Tarde (12-18h)', start: 12, end: 18 },
{ name: 'Noite (18-24h)', start: 18, end: 24 },
{ name: 'Madrugada (0-6h)', start: 0, end: 6 }
];
const timeRangeData = timeRanges.map(range => {
const rangeLogs = sessionLogs.filter(log => {
const hour = new Date(log.session_start).getHours();
return hour >= range.start && hour < range.end;
});
const avgDuration = rangeLogs.length > 0
? rangeLogs.reduce((acc, log) => acc + (log.duration_minutes || 0), 0) / rangeLogs.length
: 0;
return {
name: range.name,
avgDuration: Math.round(avgDuration),
sessions: rangeLogs.length
};
});
return { userSessions, dailyActivity, timeRangeData };
};
const { userSessions, dailyActivity, timeRangeData } = getDashboardData();
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
<p className="text-muted-foreground">Carregando logs de sessão...</p>
</div>
</div>
);
}
return (
<div className="space-y-6">
{/* Estatísticas Rápidas */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardContent className="p-4">
<div className="flex items-center space-x-2">
<Users className="h-4 w-4 text-blue-500" />
<div>
<p className="text-sm text-muted-foreground">Total de Usuários</p>
<p className="text-2xl font-bold">{sessionStats?.total_users || 0}</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center space-x-2">
<Activity className="h-4 w-4 text-green-500" />
<div>
<p className="text-sm text-muted-foreground">Sessões Ativas</p>
<p className="text-2xl font-bold">{sessionStats?.active_sessions || 0}</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center space-x-2">
<BarChart3 className="h-4 w-4 text-orange-500" />
<div>
<p className="text-sm text-muted-foreground">Total de Sessões</p>
<p className="text-2xl font-bold">{sessionStats?.total_sessions || 0}</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center space-x-2">
<Clock className="h-4 w-4 text-purple-500" />
<div>
<p className="text-sm text-muted-foreground">Duração Média</p>
<p className="text-2xl font-bold">{formatDuration(sessionStats?.average_duration || 0)}</p>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Navegação entre Views */}
<div className="flex space-x-2">
<Button
variant={activeView === 'logs' ? 'default' : 'outline'}
onClick={() => setActiveView('logs')}
>
<FileText className="h-4 w-4 mr-2" />
Logs de Sessão
</Button>
<Button
variant={activeView === 'dashboard' ? 'default' : 'outline'}
onClick={() => setActiveView('dashboard')}
>
<BarChart3 className="h-4 w-4 mr-2" />
Dashboard
</Button>
</div>
{/* Filtros */}
<Card>
<CardHeader>
<CardTitle>Filtros</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
<div>
<Label htmlFor="user-select">Usuário</Label>
<Select value={filters.userId} onValueChange={(value) => handleFilterChange('userId', value)}>
<SelectTrigger>
<SelectValue placeholder="Todos os usuários" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">Todos os usuários</SelectItem>
{users.map(user => (
<SelectItem key={user.id} value={user.id}>
{user.full_name || user.email}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="period-select">Período</Label>
<Select value={filters.period} onValueChange={(value) => handleFilterChange('period', value)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="day">Último dia</SelectItem>
<SelectItem value="week">Última semana</SelectItem>
<SelectItem value="month">Último mês</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="start-date">Data Início</Label>
<Input
id="start-date"
type="date"
value={filters.startDate}
onChange={(e) => handleFilterChange('startDate', e.target.value)}
/>
</div>
<div>
<Label htmlFor="end-date">Data Fim</Label>
<Input
id="end-date"
type="date"
value={filters.endDate}
onChange={(e) => handleFilterChange('endDate', e.target.value)}
/>
</div>
<div className="flex items-end space-x-2">
<Button onClick={applyFilters}>Aplicar</Button>
<Button variant="outline" onClick={resetFilters}>Limpar</Button>
</div>
</div>
</CardContent>
</Card>
{/* Botões de Exportação */}
<div className="flex space-x-2">
<Button
variant="outline"
onClick={() => exportReport('csv', {
userId: filters.userId || undefined,
startDate: filters.startDate || undefined,
endDate: filters.endDate || undefined
})}
>
<Download className="h-4 w-4 mr-2" />
Exportar CSV
</Button>
<Button
variant="outline"
onClick={() => exportReport('json', {
userId: filters.userId || undefined,
startDate: filters.startDate || undefined,
endDate: filters.endDate || undefined
})}
>
<Download className="h-4 w-4 mr-2" />
Exportar JSON
</Button>
</div>
{/* Conteúdo Principal */}
{activeView === 'logs' ? (
// Tabela de Logs
<Card>
<CardHeader>
<CardTitle>Logs de Sessão</CardTitle>
</CardHeader>
<CardContent>
{sessionLogs.length === 0 ? (
<div className="text-center py-8">
<AlertCircle className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<p className="text-muted-foreground">Nenhum log de sessão encontrado</p>
</div>
) : (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Usuário</TableHead>
<TableHead>Início da Sessão</TableHead>
<TableHead>Fim da Sessão</TableHead>
<TableHead>Duração</TableHead>
<TableHead>Status</TableHead>
<TableHead>User Agent</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sessionLogs.map((log) => (
<TableRow key={log.id}>
<TableCell>{getUserName(log.user_id)}</TableCell>
<TableCell>
{format(new Date(log.session_start), 'dd/MM/yyyy HH:mm', { locale: ptBR })}
</TableCell>
<TableCell>
{log.session_end
? format(new Date(log.session_end), 'dd/MM/yyyy HH:mm', { locale: ptBR })
: '-'
}
</TableCell>
<TableCell>{formatDuration(log.duration_minutes)}</TableCell>
<TableCell>
<Badge variant={log.is_active ? 'default' : 'secondary'}>
{log.is_active ? 'Ativa' : 'Finalizada'}
</Badge>
</TableCell>
<TableCell className="text-xs text-muted-foreground max-w-[200px] truncate">
{log.user_agent}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</CardContent>
</Card>
) : (
// Dashboard com Gráficos
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Sessões por Usuário */}
<Card>
<CardHeader>
<CardTitle>Sessões por Usuário</CardTitle>
</CardHeader>
<CardContent>
{userSessions.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<BarChart data={userSessions}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Bar dataKey="sessions" fill="#8884d8" />
</BarChart>
</ResponsiveContainer>
) : (
<div className="h-[300px] flex items-center justify-center text-muted-foreground">
Nenhum dado disponível
</div>
)}
</CardContent>
</Card>
{/* Atividade Diária */}
<Card>
<CardHeader>
<CardTitle>Atividade dos Últimos 7 Dias</CardTitle>
</CardHeader>
<CardContent>
{dailyActivity.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<LineChart data={dailyActivity}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="sessions" stroke="#8884d8" strokeWidth={2} />
<Line type="monotone" dataKey="users" stroke="#82ca9d" strokeWidth={2} />
</LineChart>
</ResponsiveContainer>
) : (
<div className="h-[300px] flex items-center justify-center text-muted-foreground">
Nenhum dado disponível
</div>
)}
</CardContent>
</Card>
{/* Tempo por Usuário */}
<Card>
<CardHeader>
<CardTitle>Tempo Total por Usuário</CardTitle>
</CardHeader>
<CardContent>
{userSessions.length > 0 && userSessions.some(u => u.totalTime > 0) ? (
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={userSessions.filter(u => u.totalTime > 0)}
cx="50%"
cy="50%"
labelLine={false}
label={({ name, totalTime }) => `${name}: ${formatDuration(totalTime)}`}
outerRadius={80}
fill="#8884d8"
dataKey="totalTime"
>
{userSessions.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip formatter={(value) => formatDuration(value as number)} />
</PieChart>
</ResponsiveContainer>
) : (
<div className="h-[300px] flex items-center justify-center text-muted-foreground">
Nenhum dado disponível
</div>
)}
</CardContent>
</Card>
{/* Atividade por Período do Dia */}
<Card>
<CardHeader>
<CardTitle>Duração Média por Período</CardTitle>
</CardHeader>
<CardContent>
{timeRangeData.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<BarChart data={timeRangeData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip formatter={(value) => `${value} min`} />
<Bar dataKey="avgDuration" fill="#82ca9d" />
</BarChart>
</ResponsiveContainer>
) : (
<div className="h-[300px] flex items-center justify-center text-muted-foreground">
Nenhum dado disponível
</div>
)}
</CardContent>
</Card>
</div>
)}
</div>
);
}
+319
View File
@@ -0,0 +1,319 @@
import { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Badge } from '@/components/ui/badge';
import { Download, Users, Activity, Clock, BarChart3, RefreshCw } from 'lucide-react';
import { format } from 'date-fns';
import { ptBR } from 'date-fns/locale';
import { useSessionLogsSimple } from '@/hooks/useSessionLogsSimple';
import { UserProfile } from '@/hooks/useUserManagement';
interface SessionLogsSimpleProps {
users: UserProfile[];
}
export function SessionLogsSimple({ users }: SessionLogsSimpleProps) {
const {
sessionLogs,
onlineUsers,
loading,
fetchSessionLogs,
fetchOnlineUsers,
exportToCsv
} = useSessionLogsSimple();
const [filters, setFilters] = useState({
userId: '',
startDate: '',
endDate: ''
});
// Carregar dados iniciais
useEffect(() => {
fetchSessionLogs();
fetchOnlineUsers();
// Atualizar usuários online a cada 30 segundos
const interval = setInterval(fetchOnlineUsers, 30000);
return () => clearInterval(interval);
}, []);
const handleFilterChange = (key: string, value: string) => {
setFilters(prev => ({ ...prev, [key]: value }));
};
const applyFilters = () => {
const filterParams = {
userId: filters.userId || undefined,
startDate: filters.startDate || undefined,
endDate: filters.endDate || undefined
};
fetchSessionLogs(filterParams);
};
const resetFilters = () => {
setFilters({ userId: '', startDate: '', endDate: '' });
fetchSessionLogs();
};
const getUserName = (userId: string) => {
const user = users.find(u => u.id === userId);
return user?.full_name || user?.email || 'Usuário desconhecido';
};
const formatDuration = (minutes: number | null) => {
if (!minutes) return '0min';
const hours = Math.floor(minutes / 60);
const mins = Math.round(minutes % 60);
return hours > 0 ? `${hours}h ${mins}min` : `${mins}min`;
};
const formatDate = (dateString: string) => {
try {
return format(new Date(dateString), 'dd/MM/yyyy HH:mm', { locale: ptBR });
} catch {
return dateString;
}
};
// Estatísticas simples
const stats = {
totalSessions: sessionLogs.length,
activeSessions: sessionLogs.filter(log => log.is_active).length,
onlineUsers: onlineUsers.length,
avgDuration: sessionLogs.length > 0
? Math.round(sessionLogs.reduce((acc, log) => acc + (log.duration_minutes || 0), 0) / sessionLogs.length)
: 0
};
return (
<div className="space-y-6">
{/* Estatísticas */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardContent className="p-4">
<div className="flex items-center space-x-2">
<Users className="h-4 w-4 text-blue-500" />
<div>
<p className="text-sm text-muted-foreground">Usuários Online</p>
<p className="text-2xl font-bold">{stats.onlineUsers}</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center space-x-2">
<Activity className="h-4 w-4 text-green-500" />
<div>
<p className="text-sm text-muted-foreground">Sessões Ativas</p>
<p className="text-2xl font-bold">{stats.activeSessions}</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center space-x-2">
<BarChart3 className="h-4 w-4 text-orange-500" />
<div>
<p className="text-sm text-muted-foreground">Total de Sessões</p>
<p className="text-2xl font-bold">{stats.totalSessions}</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center space-x-2">
<Clock className="h-4 w-4 text-purple-500" />
<div>
<p className="text-sm text-muted-foreground">Duração Média</p>
<p className="text-2xl font-bold">{formatDuration(stats.avgDuration)}</p>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Usuários Online */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Users className="h-5 w-5" />
Usuários Online Agora
<Button
variant="outline"
size="sm"
onClick={fetchOnlineUsers}
disabled={loading}
>
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
</Button>
</CardTitle>
</CardHeader>
<CardContent>
{onlineUsers.length === 0 ? (
<p className="text-center text-muted-foreground py-4">
Nenhum usuário online no momento
</p>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{onlineUsers.map((user) => (
<div key={user.user_id} className="flex items-center space-x-3 p-3 border rounded-lg">
<div className="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center text-white text-sm font-medium">
{user.full_name?.charAt(0) || user.email.charAt(0).toUpperCase()}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">
{user.full_name || user.email}
</p>
<p className="text-xs text-muted-foreground">
Online desde {formatDate(user.session_start)}
</p>
</div>
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
</div>
))}
</div>
)}
</CardContent>
</Card>
{/* Filtros */}
<Card>
<CardHeader>
<CardTitle>Filtros</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<Label htmlFor="user-select">Usuário</Label>
<Select value={filters.userId} onValueChange={(value) => handleFilterChange('userId', value)}>
<SelectTrigger>
<SelectValue placeholder="Todos os usuários" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos os usuários</SelectItem>
{users.map(user => (
<SelectItem key={user.id} value={user.id}>
{user.full_name || user.email}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="start-date">Data Início</Label>
<Input
id="start-date"
type="date"
value={filters.startDate}
onChange={(e) => handleFilterChange('startDate', e.target.value)}
/>
</div>
<div>
<Label htmlFor="end-date">Data Fim</Label>
<Input
id="end-date"
type="date"
value={filters.endDate}
onChange={(e) => handleFilterChange('endDate', e.target.value)}
/>
</div>
<div className="flex items-end space-x-2">
<Button onClick={applyFilters} disabled={loading}>
{loading ? 'Carregando...' : 'Aplicar'}
</Button>
<Button variant="outline" onClick={resetFilters}>
Limpar
</Button>
</div>
</div>
</CardContent>
</Card>
{/* Botão de Exportação */}
<div className="flex space-x-2">
<Button
variant="outline"
onClick={() => exportToCsv({
userId: filters.userId === 'all' ? undefined : filters.userId || undefined,
startDate: filters.startDate || undefined,
endDate: filters.endDate || undefined
})}
disabled={loading}
>
<Download className="h-4 w-4 mr-2" />
Exportar CSV
</Button>
</div>
{/* Tabela de Logs */}
<Card>
<CardHeader>
<CardTitle>Histórico de Sessões</CardTitle>
</CardHeader>
<CardContent>
{loading ? (
<div className="text-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
<p className="text-muted-foreground">Carregando logs...</p>
</div>
) : sessionLogs.length === 0 ? (
<div className="text-center py-8">
<p className="text-muted-foreground">Nenhum log de sessão encontrado</p>
</div>
) : (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Usuário</TableHead>
<TableHead>Início da Sessão</TableHead>
<TableHead>Fim da Sessão</TableHead>
<TableHead>Duração</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sessionLogs.slice(0, 50).map((log) => (
<TableRow key={log.id}>
<TableCell>{getUserName(log.user_id)}</TableCell>
<TableCell>{formatDate(log.session_start)}</TableCell>
<TableCell>
{log.session_end ? formatDate(log.session_end) : '-'}
</TableCell>
<TableCell>{formatDuration(log.duration_minutes)}</TableCell>
<TableCell>
<Badge variant={log.is_active ? 'default' : 'secondary'}>
{log.is_active ? 'Ativa' : 'Finalizada'}
</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{sessionLogs.length > 50 && (
<p className="text-center text-sm text-muted-foreground mt-4">
Mostrando os primeiros 50 registros de {sessionLogs.length} total
</p>
)}
</div>
)}
</CardContent>
</Card>
</div>
);
}
+214
View File
@@ -0,0 +1,214 @@
import React, { useState, useEffect } from 'react';
import {
AlertDialog,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Loader2, AlertTriangle, User, Database } from 'lucide-react';
import { UserProfile, UserDependency } from '@/hooks/useUserManagement';
interface UserDeleteDialogProps {
user: UserProfile | null;
isOpen: boolean;
onClose: () => void;
onConfirmDelete: (userId: string, replaceReferences: boolean) => Promise<boolean>;
canDeleteUser: (userId: string) => Promise<boolean>;
getUserDependencies: (userId: string) => Promise<UserDependency[]>;
}
export function UserDeleteDialog({
user,
isOpen,
onClose,
onConfirmDelete,
canDeleteUser,
getUserDependencies
}: UserDeleteDialogProps) {
const [loading, setLoading] = useState(false);
const [canDelete, setCanDelete] = useState(false);
const [dependencies, setDependencies] = useState<UserDependency[]>([]);
const [deleting, setDeleting] = useState(false);
useEffect(() => {
if (isOpen && user) {
loadUserData();
}
}, [isOpen, user]);
const loadUserData = async () => {
if (!user) return;
setLoading(true);
try {
const [canDeleteResult, dependenciesResult] = await Promise.all([
canDeleteUser(user.id),
getUserDependencies(user.id)
]);
setCanDelete(canDeleteResult);
setDependencies(dependenciesResult.filter(dep => dep.count > 0));
} catch (error) {
console.error('Error loading user data:', error);
} finally {
setLoading(false);
}
};
const handleDelete = async (replaceReferences: boolean = false) => {
if (!user) return;
setDeleting(true);
try {
const success = await onConfirmDelete(user.id, replaceReferences);
if (success) {
onClose();
}
} finally {
setDeleting(false);
}
};
const handleClose = () => {
if (!deleting) {
onClose();
}
};
if (!user) return null;
const criticalDependencies = dependencies.filter(dep =>
['ficha_tecnica_contratos', 'ordens_fabricacao', 'pecas', 'apontamentos_producao'].includes(dep.table_name)
);
const nonCriticalDependencies = dependencies.filter(dep =>
!['ficha_tecnica_contratos', 'ordens_fabricacao', 'pecas', 'apontamentos_producao'].includes(dep.table_name)
);
return (
<AlertDialog open={isOpen} onOpenChange={handleClose}>
<AlertDialogContent className="max-w-2xl">
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2">
<User className="h-5 w-5" />
Excluir usuário
</AlertDialogTitle>
<AlertDialogDescription>
Análise dos vínculos do usuário <strong>{user.full_name || user.email}</strong>
</AlertDialogDescription>
</AlertDialogHeader>
<div className="space-y-4">
{loading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin" />
<span className="ml-2">Verificando vínculos...</span>
</div>
) : (
<>
{dependencies.length === 0 ? (
<div className="text-center py-4 text-green-600">
<Database className="h-8 w-8 mx-auto mb-2" />
<p>Este usuário não possui vínculos no sistema e pode ser excluído com segurança.</p>
</div>
) : (
<div className="space-y-4">
<div className="flex items-center gap-2 text-orange-600">
<AlertTriangle className="h-5 w-5" />
<span className="font-medium">Vínculos encontrados:</span>
</div>
{criticalDependencies.length > 0 && (
<div className="space-y-2">
<h4 className="font-medium text-red-600">Vínculos críticos (impedem exclusão):</h4>
<div className="grid grid-cols-1 gap-2">
{criticalDependencies.map((dep, index) => (
<div key={index} className="flex items-center justify-between p-2 bg-red-50 rounded-lg border border-red-200">
<span className="text-sm">{dep.description}</span>
<Badge variant="destructive">{dep.count}</Badge>
</div>
))}
</div>
</div>
)}
{nonCriticalDependencies.length > 0 && (
<div className="space-y-2">
<h4 className="font-medium text-orange-600">Vínculos não críticos (podem ser transferidos):</h4>
<div className="grid grid-cols-1 gap-2">
{nonCriticalDependencies.map((dep, index) => (
<div key={index} className="flex items-center justify-between p-2 bg-orange-50 rounded-lg border border-orange-200">
<span className="text-sm">{dep.description}</span>
<Badge variant="secondary">{dep.count}</Badge>
</div>
))}
</div>
</div>
)}
{!canDelete && nonCriticalDependencies.length > 0 && (
<div className="p-4 bg-blue-50 rounded-lg border border-blue-200">
<p className="text-sm text-blue-800">
<strong>Opção disponível:</strong> Os vínculos não críticos podem ser transferidos para um usuário especial "UserDel"
para preservar a integridade do banco de dados e permitir a exclusão do usuário.
</p>
</div>
)}
</div>
)}
</>
)}
</div>
<AlertDialogFooter>
<Button
variant="outline"
onClick={handleClose}
disabled={deleting}
>
Cancelar
</Button>
{canDelete && (
<Button
variant="destructive"
onClick={() => handleDelete(false)}
disabled={deleting}
>
{deleting ? (
<>
<Loader2 className="h-4 w-4 animate-spin mr-2" />
Excluindo...
</>
) : (
'Excluir usuário'
)}
</Button>
)}
{!canDelete && nonCriticalDependencies.length > 0 && criticalDependencies.length === 0 && (
<Button
variant="destructive"
onClick={() => handleDelete(true)}
disabled={deleting}
>
{deleting ? (
<>
<Loader2 className="h-4 w-4 animate-spin mr-2" />
Processando...
</>
) : (
'Transferir vínculos e excluir'
)}
</Button>
)}
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
+313
View File
@@ -0,0 +1,313 @@
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { UserProfile, UserFunction, UserPrivilege } from '@/hooks/useUserManagement';
import { AvatarUpload } from '@/components/ui/avatar-upload';
import { useProfileImage } from '@/hooks/useProfileImage';
import { usePasswordManagement } from '@/hooks/usePasswordManagement';
import { Eye, EyeOff } from 'lucide-react';
interface UserModalProps {
user: UserProfile | null;
functions: UserFunction[];
privileges: UserPrivilege[];
onSave?: (userId: string, data: Partial<UserProfile>) => void;
onCreate?: (data: { email: string; full_name?: string; function_id?: string; privilege_id?: string }) => void;
onClose: () => void;
readOnly?: boolean;
}
export function UserModal({ user, functions, privileges, onSave, onCreate, onClose, readOnly = false }: UserModalProps) {
const { updateProfileImage, removeProfileImage, updating } = useProfileImage();
const { changeUserPassword, isChangingPassword } = usePasswordManagement();
const [showPassword, setShowPassword] = useState(false);
const [formData, setFormData] = useState({
email: '',
full_name: '',
function_id: '',
privilege_id: '',
status: 'active' as 'pending' | 'active' | 'inactive' | 'rejected',
profile_image_url: null as string | null,
password: ''
});
const isCreateMode = !user;
const isReadOnly = readOnly || (!onSave && !onCreate);
useEffect(() => {
if (user) {
setFormData({
email: user.email || '',
full_name: user.full_name || '',
function_id: user.function_id || '',
privilege_id: user.privilege_id || '',
status: user.status,
profile_image_url: user.profile_image_url,
password: '' // Não mostramos a senha atual por segurança
});
} else {
// Reset form for create mode
setFormData({
email: '',
full_name: '',
function_id: '',
privilege_id: '',
status: 'active',
profile_image_url: null,
password: 'usuario' // Senha padrão para novos usuários
});
}
}, [user]);
const handleImageUpload = async (imageUrl: string) => {
if (!user || isReadOnly) return;
const success = await updateProfileImage(user.id, imageUrl);
if (success) {
setFormData(prev => ({ ...prev, profile_image_url: imageUrl }));
}
};
const handleImageRemove = async () => {
if (!user || isReadOnly) return;
const success = await removeProfileImage(user.id);
if (success) {
setFormData(prev => ({ ...prev, profile_image_url: null }));
}
};
const handlePasswordChange = async () => {
if (!user || isReadOnly || !formData.password.trim()) return;
await changeUserPassword(user.id, formData.password);
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (isReadOnly) return;
if (isCreateMode && onCreate) {
// Create new user
onCreate({
email: formData.email,
full_name: formData.full_name || undefined,
function_id: formData.function_id || undefined,
privilege_id: formData.privilege_id || undefined
});
} else if (user && onSave) {
// Update existing user
const updateData: Partial<UserProfile> = {
full_name: formData.full_name,
function_id: formData.function_id || null,
privilege_id: formData.privilege_id || null,
status: formData.status,
profile_image_url: formData.profile_image_url
};
onSave(user.id, updateData);
}
onClose();
};
return (
<Dialog open={true} onOpenChange={onClose}>
<DialogContent className="bg-slate-800 border-slate-700 text-white max-w-md">
<DialogHeader>
<DialogTitle>
{isReadOnly ? 'Visualizar Usuário' : isCreateMode ? 'Novo Usuário' : 'Editar Usuário'}
</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Profile Image Upload - Only for edit mode */}
{user && (
<div>
<Label className="text-slate-300 text-sm font-medium">Foto de Perfil</Label>
<div className="mt-2">
<AvatarUpload
currentImageUrl={formData.profile_image_url}
onImageUpload={isReadOnly ? undefined : handleImageUpload}
onImageRemove={isReadOnly ? undefined : handleImageRemove}
/>
</div>
</div>
)}
<div>
<Label className="text-slate-300">Email</Label>
<Input
type="email"
value={formData.email}
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
disabled={!isCreateMode || isReadOnly}
className="bg-slate-700 border-slate-600 text-white mt-1"
placeholder="Digite o email"
required={!isReadOnly}
readOnly={isReadOnly}
/>
</div>
{/* Campo de Senha */}
<div>
<Label className="text-slate-300">Senha</Label>
<div className="relative mt-1">
<Input
type={showPassword ? "text" : "password"}
value={formData.password}
onChange={(e) => setFormData(prev => ({ ...prev, password: e.target.value }))}
className="bg-slate-700 border-slate-600 text-slate-400 pr-10"
placeholder={isCreateMode ? "usuario" : "Digite a nova senha"}
disabled={isReadOnly}
readOnly={isReadOnly}
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
onClick={() => setShowPassword(!showPassword)}
disabled={isReadOnly}
>
{showPassword ? (
<EyeOff className="h-4 w-4 text-slate-400" />
) : (
<Eye className="h-4 w-4 text-slate-400" />
)}
</Button>
</div>
{!isCreateMode && !isReadOnly && (
<Button
type="button"
variant="outline"
size="sm"
className="mt-2 text-xs"
onClick={handlePasswordChange}
disabled={isChangingPassword || !formData.password.trim()}
>
{isChangingPassword ? 'Alterando...' : 'Alterar Senha'}
</Button>
)}
</div>
<div>
<Label className="text-slate-300">Nome Completo</Label>
<Input
value={formData.full_name}
onChange={(e) => setFormData(prev => ({ ...prev, full_name: e.target.value }))}
className="bg-slate-700 border-slate-600 text-white mt-1"
placeholder="Digite o nome completo"
disabled={isReadOnly}
readOnly={isReadOnly}
/>
</div>
<div>
<Label className="text-slate-300">Função</Label>
<Select
value={formData.function_id}
onValueChange={isReadOnly ? undefined : (value) => setFormData(prev => ({ ...prev, function_id: value }))}
disabled={isReadOnly}
>
<SelectTrigger className="bg-slate-700 border-slate-600 text-white mt-1">
<SelectValue placeholder="Selecione uma função" />
</SelectTrigger>
<SelectContent className="bg-slate-700 border-slate-600">
{functions.map((func) => (
<SelectItem key={func.id} value={func.id} className="text-white hover:bg-slate-600">
{func.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-slate-300">Privilégio</Label>
<Select
value={formData.privilege_id}
onValueChange={isReadOnly ? undefined : (value) => setFormData(prev => ({ ...prev, privilege_id: value }))}
disabled={isReadOnly}
>
<SelectTrigger className="bg-slate-700 border-slate-600 text-white mt-1">
<SelectValue placeholder="Selecione um privilégio" />
</SelectTrigger>
<SelectContent className="bg-slate-700 border-slate-600">
{privileges.map((privilege) => (
<SelectItem key={privilege.id} value={privilege.id} className="text-white hover:bg-slate-600">
{privilege.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Status - Only for edit mode */}
{!isCreateMode && (
<div>
<Label className="text-slate-300">Status</Label>
<Select
value={formData.status}
onValueChange={isReadOnly ? undefined : (value) => setFormData(prev => ({ ...prev, status: value as typeof formData.status }))}
disabled={isReadOnly}
>
<SelectTrigger className="bg-slate-700 border-slate-600 text-white mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-slate-700 border-slate-600">
<SelectItem value="active" className="text-white hover:bg-slate-600">Ativo</SelectItem>
<SelectItem value="inactive" className="text-white hover:bg-slate-600">Inativo</SelectItem>
<SelectItem value="pending" className="text-white hover:bg-slate-600">Pendente</SelectItem>
<SelectItem value="rejected" className="text-white hover:bg-slate-600">Rejeitado</SelectItem>
</SelectContent>
</Select>
</div>
)}
{/* Password info for create mode */}
{isCreateMode && !isReadOnly && (
<div className="bg-slate-700 p-3 rounded border border-slate-600">
<p className="text-sm text-slate-300">
<strong>Senha padrão:</strong> usuario
</p>
<p className="text-xs text-slate-400 mt-1">
O usuário poderá alterar a senha após o primeiro login.
</p>
</div>
)}
<div className="flex gap-2 pt-4">
{!isReadOnly && (
<Button
type="submit"
className="bg-blue-600 hover:bg-blue-700"
disabled={updating || (isCreateMode && !formData.email)}
>
{isCreateMode ? 'Criar Usuário' : 'Salvar'}
</Button>
)}
<Button type="button" variant="ghost" onClick={onClose}>
{isReadOnly ? 'Fechar' : 'Cancelar'}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
);
}
+202
View File
@@ -0,0 +1,202 @@
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from '@/components/ui/table';
import { Edit, Power, PowerOff, Trash2 } from 'lucide-react';
import { UserProfile, UserFunction, UserPrivilege, UserDependency } from '@/hooks/useUserManagement';
import { UserAvatar } from '@/components/ui/user-avatar';
import { UserDeleteDialog } from './UserDeleteDialog';
import { useState } from 'react';
interface UsersTableProps {
users: UserProfile[];
functions: UserFunction[];
privileges: UserPrivilege[];
onEditUser: (user: UserProfile) => void;
onToggleStatus: (userId: string, currentStatus: string) => void;
onDeleteUser?: (userId: string, replaceReferences: boolean) => Promise<boolean>;
canDeleteUser?: (userId: string) => Promise<boolean>;
getUserDependencies?: (userId: string) => Promise<UserDependency[]>;
}
export function UsersTable({
users,
functions,
privileges,
onEditUser,
onToggleStatus,
onDeleteUser,
canDeleteUser,
getUserDependencies
}: UsersTableProps) {
const [selectedUserForDeletion, setSelectedUserForDeletion] = useState<UserProfile | null>(null);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const getFunctionName = (functionId: string | null) => {
if (!functionId) return '-';
const func = functions.find(f => f.id === functionId);
return func?.name || '-';
};
const getPrivilegeName = (privilegeId: string | null) => {
if (!privilegeId) return '-';
const privilege = privileges.find(p => p.id === privilegeId);
return privilege?.name || '-';
};
const getStatusBadge = (status: string) => {
const statusConfig = {
active: { label: 'Ativo', className: 'bg-green-600 text-white' },
inactive: { label: 'Inativo', className: 'bg-red-600 text-white' },
rejected: { label: 'Rejeitado', className: 'bg-gray-600 text-white' }
};
const config = statusConfig[status as keyof typeof statusConfig] || statusConfig.inactive;
return (
<Badge className={config.className}>
{config.label}
</Badge>
);
};
const handleDeleteClick = (user: UserProfile) => {
setSelectedUserForDeletion(user);
setIsDeleteDialogOpen(true);
};
const handleCloseDeleteDialog = () => {
setSelectedUserForDeletion(null);
setIsDeleteDialogOpen(false);
};
const handleConfirmDelete = async (userId: string, replaceReferences: boolean = false) => {
if (onDeleteUser) {
return await onDeleteUser(userId, replaceReferences);
}
return false;
};
if (users.length === 0) {
return (
<div className="text-center py-8 text-slate-400">
Nenhum usuário encontrado
</div>
);
}
return (
<>
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow className="border-slate-700">
<TableHead className="text-slate-300">Usuário</TableHead>
<TableHead className="text-slate-300 hidden sm:table-cell">Função</TableHead>
<TableHead className="text-slate-300 hidden md:table-cell">Privilégio</TableHead>
<TableHead className="text-slate-300">Status</TableHead>
<TableHead className="text-slate-300 hidden lg:table-cell">Inclusão</TableHead>
<TableHead className="text-slate-300">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map((user) => (
<TableRow key={user.id} className="border-slate-700">
<TableCell className="min-w-0">
<div className="flex items-center gap-2">
<UserAvatar
imageUrl={user.profile_image_url}
name={user.full_name}
email={user.email}
size="sm"
/>
<div className="min-w-0 flex-1">
<div className="text-white font-medium truncate text-xs sm:text-sm">
{user.full_name || 'Nome não informado'}
</div>
<div className="text-slate-400 text-xs truncate">{user.email}</div>
{/* Mobile: Show function and privilege on small screens */}
<div className="sm:hidden space-y-1 mt-1">
<div className="text-xs text-slate-300">
Função: {getFunctionName(user.function_id)}
</div>
<div className="text-xs text-slate-300 md:hidden">
Privilégio: {getPrivilegeName(user.privilege_id)}
</div>
</div>
</div>
</div>
</TableCell>
<TableCell className="text-slate-300 hidden sm:table-cell">
<span className="text-xs">{getFunctionName(user.function_id)}</span>
</TableCell>
<TableCell className="text-slate-300 hidden md:table-cell">
<span className="text-xs">{getPrivilegeName(user.privilege_id)}</span>
</TableCell>
<TableCell>
{getStatusBadge(user.status)}
</TableCell>
<TableCell className="text-slate-300 hidden lg:table-cell">
<span className="text-xs">{new Date(user.created_at).toLocaleDateString('pt-BR')}</span>
</TableCell>
<TableCell>
<div className="flex gap-1">
<Button
size="sm"
variant="ghost"
onClick={() => onEditUser(user)}
className="text-blue-400 hover:text-blue-300 hover:bg-blue-900/50 p-1 h-6 w-6"
>
<Edit className="w-3 h-3" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => onToggleStatus(user.id, user.status)}
className={`p-1 h-6 w-6 ${user.status === 'active'
? "text-red-400 hover:text-red-300 hover:bg-red-900/50"
: "text-green-400 hover:text-green-300 hover:bg-green-900/50"
}`}
>
{user.status === 'active' ? (
<PowerOff className="w-3 h-3" />
) : (
<Power className="w-3 h-3" />
)}
</Button>
{onDeleteUser && canDeleteUser && getUserDependencies && (
<Button
size="sm"
variant="ghost"
onClick={() => handleDeleteClick(user)}
className="text-red-400 hover:text-red-300 hover:bg-red-900/50 p-1 h-6 w-6"
>
<Trash2 className="w-3 h-3" />
</Button>
)}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<UserDeleteDialog
user={selectedUserForDeletion}
isOpen={isDeleteDialogOpen}
onClose={handleCloseDeleteDialog}
onConfirmDelete={handleConfirmDelete}
canDeleteUser={canDeleteUser || (async () => false)}
getUserDependencies={getUserDependencies || (async () => [])}
/>
</>
);
}
@@ -0,0 +1,86 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Trash2, Edit } from 'lucide-react';
import { Privilege, FUNCTIONAL_PERMISSIONS } from './types';
interface PrivilegeCardProps {
privilege: Privilege;
onEdit: (privilege: Privilege) => void;
onDelete: (id: string) => void;
}
export const PrivilegeCard: React.FC<PrivilegeCardProps> = ({
privilege,
onEdit,
onDelete
}) => {
const getFunctionalPermissionCount = (privilege: Privilege) => {
if (!privilege.permissions) return 0;
// Count only valid functional permissions
const validPermissions = FUNCTIONAL_PERMISSIONS.map(p => p.key);
return validPermissions.filter(key => privilege.permissions[key]).length;
};
const getActiveFunctionalPermissions = (privilege: Privilege) => {
if (!privilege.permissions) return [];
// Show only valid functional permissions
return FUNCTIONAL_PERMISSIONS
.filter(perm => privilege.permissions[perm.key])
.map(perm => perm.label);
};
const functionalCount = getFunctionalPermissionCount(privilege);
const activeFunctionalPermissions = getActiveFunctionalPermissions(privilege);
return (
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h4 className="font-semibold">{privilege.name}</h4>
<div className="flex gap-2">
<Badge variant="secondary">
{functionalCount} permissão funcional
</Badge>
</div>
</div>
{privilege.description && (
<p className="text-sm text-muted-foreground mb-2">
{privilege.description}
</p>
)}
<div className="flex flex-wrap gap-1">
{activeFunctionalPermissions.map((permName) => (
<Badge key={permName} variant="outline" className="text-xs">
{permName}
</Badge>
))}
</div>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => onEdit(privilege)}
>
<Edit className="w-4 h-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => onDelete(privilege.id)}
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
</CardContent>
</Card>
);
};
@@ -0,0 +1,100 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import { Shield } from 'lucide-react';
import { FUNCTIONAL_PERMISSIONS } from './types';
interface PrivilegeFormProps {
formData: {
name: string;
description: string;
permissions: Record<string, boolean>;
};
onFormDataChange: (data: any) => void;
onPermissionChange: (permissionKey: string, checked: boolean) => void;
onSubmit: (e: React.FormEvent) => void;
onCancel: () => void;
isEditing: boolean;
}
export const PrivilegeForm: React.FC<PrivilegeFormProps> = ({
formData,
onFormDataChange,
onPermissionChange,
onSubmit,
onCancel,
isEditing
}) => {
return (
<div className="space-y-6">
<div className="flex items-center gap-2">
<Shield className="w-5 h-5" />
<h3 className="text-lg font-semibold">
{isEditing ? 'Editar Privilégio' : 'Novo Privilégio'}
</h3>
</div>
<form onSubmit={onSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="name">Nome *</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => onFormDataChange(prev => ({ ...prev, name: e.target.value }))}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="description">Descrição</Label>
<Input
id="description"
value={formData.description}
onChange={(e) => onFormDataChange(prev => ({ ...prev, description: e.target.value }))}
/>
</div>
</div>
<div className="space-y-4">
<div>
<Label className="text-base font-medium">Permissões Funcionais</Label>
<p className="text-sm text-muted-foreground mb-3">
Selecione o nível de acesso funcional (apenas uma opção por privilégio)
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{FUNCTIONAL_PERMISSIONS.map((perm) => (
<div key={perm.key} className="flex items-start space-x-3 p-3 border rounded-lg">
<Checkbox
id={perm.key}
checked={formData.permissions[perm.key] || false}
onCheckedChange={(checked) => onPermissionChange(perm.key, checked as boolean)}
/>
<div className="space-y-1">
<Label htmlFor={perm.key} className="font-medium">
{perm.label}
</Label>
<p className="text-sm text-muted-foreground">
{perm.description}
</p>
</div>
</div>
))}
</div>
</div>
</div>
<div className="flex gap-2">
<Button type="submit">
{isEditing ? 'Atualizar' : 'Criar'}
</Button>
<Button type="button" variant="outline" onClick={onCancel}>
Cancelar
</Button>
</div>
</form>
</div>
);
};
+16
View File
@@ -0,0 +1,16 @@
export interface Privilege {
id: string;
name: string;
description?: string;
permissions: Record<string, boolean>;
created_at: string;
updated_at: string;
}
export const FUNCTIONAL_PERMISSIONS = [
{ key: 'can_admin', label: 'Administrador Total', description: 'Acesso completo ao sistema' },
{ key: 'can_create_update_delete', label: 'Criar/Editar/Excluir', description: 'Pode criar, editar e excluir dados' },
{ key: 'can_create_only', label: 'Apenas Criar', description: 'Pode criar e visualizar dados' },
{ key: 'can_view_only', label: 'Apenas Visualizar', description: 'Pode apenas visualizar dados' }
];