Compare commits
5 Commits
fix-ferias
...
fix-notifi
| Author | SHA1 | Date | |
|---|---|---|---|
| 8167a407e7 | |||
|
|
9ff61b325f | ||
| fbec5c46c2 | |||
| d0692c3608 | |||
|
|
bf67faa470 |
@@ -5,7 +5,7 @@ root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
@@ -1 +1 @@
|
||||
nodejs 25.0.0
|
||||
nodejs 22.21.1
|
||||
|
||||
@@ -2,25 +2,38 @@
|
||||
interface Props {
|
||||
title: string;
|
||||
value: string | number;
|
||||
icon?: string;
|
||||
icon?: any; // Componente do lucide-svelte
|
||||
trend?: {
|
||||
value: number;
|
||||
isPositive: boolean;
|
||||
};
|
||||
description?: string;
|
||||
color?: "primary" | "secondary" | "accent" | "success" | "warning" | "error";
|
||||
color?:
|
||||
| "primary"
|
||||
| "secondary"
|
||||
| "accent"
|
||||
| "success"
|
||||
| "warning"
|
||||
| "error"
|
||||
| "info";
|
||||
}
|
||||
|
||||
let { title, value, icon, trend, description, color = "primary" }: Props = $props();
|
||||
let {
|
||||
title,
|
||||
value,
|
||||
icon: Icon,
|
||||
trend,
|
||||
description,
|
||||
color = "primary",
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="stats shadow bg-base-100">
|
||||
<div class="stat">
|
||||
<div class="stat-figure text-{color}">
|
||||
{#if icon}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="inline-block w-8 h-8 stroke-current">
|
||||
{@html icon}
|
||||
</svg>
|
||||
{#if Icon}
|
||||
{@const IconComponent = Icon}
|
||||
<IconComponent class="inline-block w-8 h-8 stroke-current" />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="stat-title">{title}</div>
|
||||
@@ -30,10 +43,9 @@
|
||||
{/if}
|
||||
{#if trend}
|
||||
<div class="stat-desc {trend.isPositive ? 'text-success' : 'text-error'}">
|
||||
{trend.isPositive ? '↗︎' : '↘︎'} {Math.abs(trend.value)}%
|
||||
{trend.isPositive ? "↗︎" : "↘︎"}
|
||||
{Math.abs(trend.value)}%
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -6,17 +6,21 @@
|
||||
let limite = $state(50);
|
||||
|
||||
// Queries com $derived para garantir reatividade
|
||||
const atividades = $derived(useQuery(api.logsAtividades.listarAtividades, { limite }));
|
||||
const logins = $derived(useQuery(api.logsLogin.listarTodosLogins, { limite }));
|
||||
const atividades = $derived(
|
||||
useQuery(api.logsAtividades.listarAtividades, { limite }),
|
||||
);
|
||||
const logins = $derived(
|
||||
useQuery(api.logsLogin.listarTodosLogins, { limite }),
|
||||
);
|
||||
|
||||
function formatarData(timestamp: number) {
|
||||
return new Date(timestamp).toLocaleString('pt-BR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
return new Date(timestamp).toLocaleString("pt-BR", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -27,7 +31,7 @@
|
||||
excluir: "badge-error",
|
||||
bloquear: "badge-error",
|
||||
desbloquear: "badge-success",
|
||||
resetar_senha: "badge-info"
|
||||
resetar_senha: "badge-info",
|
||||
};
|
||||
return colors[acao] || "badge-neutral";
|
||||
}
|
||||
@@ -39,7 +43,7 @@
|
||||
excluir: "Excluir",
|
||||
bloquear: "Bloquear",
|
||||
desbloquear: "Desbloquear",
|
||||
resetar_senha: "Resetar Senha"
|
||||
resetar_senha: "Resetar Senha",
|
||||
};
|
||||
return labels[acao] || acao;
|
||||
}
|
||||
@@ -47,8 +51,12 @@
|
||||
// Estatísticas
|
||||
const totalAtividades = $derived(atividades?.data?.length || 0);
|
||||
const totalLogins = $derived(logins?.data?.length || 0);
|
||||
const loginsSucesso = $derived(logins?.data?.filter(l => l.sucesso).length || 0);
|
||||
const loginsFalha = $derived(logins?.data?.filter(l => !l.sucesso).length || 0);
|
||||
const loginsSucesso = $derived(
|
||||
logins?.data?.filter((l) => l.sucesso).length || 0,
|
||||
);
|
||||
const loginsFalha = $derived(
|
||||
logins?.data?.filter((l) => !l.sucesso).length || 0,
|
||||
);
|
||||
</script>
|
||||
|
||||
<main class="container mx-auto px-4 py-6 max-w-7xl">
|
||||
@@ -64,13 +72,26 @@
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-4 mb-2">
|
||||
<div class="p-3 bg-blue-500/20 rounded-xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8 text-blue-600"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-primary">Auditoria e Logs</h1>
|
||||
<p class="text-base-content/70">Monitoramento completo de atividades e acessos do sistema</p>
|
||||
<p class="text-base-content/70">
|
||||
Monitoramento completo de atividades e acessos do sistema
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -79,8 +100,19 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<div class="stat bg-base-100 shadow-lg rounded-lg">
|
||||
<div class="stat-figure text-primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title text-xs">Atividades</div>
|
||||
@@ -90,8 +122,19 @@
|
||||
|
||||
<div class="stat bg-base-100 shadow-lg rounded-lg">
|
||||
<div class="stat-figure text-success">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title text-xs">Logins Totais</div>
|
||||
@@ -101,44 +144,98 @@
|
||||
|
||||
<div class="stat bg-base-100 shadow-lg rounded-lg">
|
||||
<div class="stat-figure text-success">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title text-xs">Logins Bem-sucedidos</div>
|
||||
<div class="stat-value text-2xl text-success">{loginsSucesso}</div>
|
||||
<div class="stat-desc">{totalLogins > 0 ? Math.round((loginsSucesso / totalLogins) * 100) : 0}% de sucesso</div>
|
||||
<div class="stat-desc">
|
||||
{totalLogins > 0 ? Math.round((loginsSucesso / totalLogins) * 100) : 0}%
|
||||
de sucesso
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat bg-base-100 shadow-lg rounded-lg">
|
||||
<div class="stat-figure text-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title text-xs">Logins Falhados</div>
|
||||
<div class="stat-value text-2xl text-error">{loginsFalha}</div>
|
||||
<div class="stat-desc">{totalLogins > 0 ? Math.round((loginsFalha / totalLogins) * 100) : 0}% de falhas</div>
|
||||
<div class="stat-desc">
|
||||
{totalLogins > 0 ? Math.round((loginsFalha / totalLogins) * 100) : 0}%
|
||||
de falhas
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="tabs tabs-boxed mb-6 bg-base-100 shadow-lg p-1">
|
||||
<button
|
||||
class="tab flex items-center gap-2 {abaAtiva === 'atividades' ? 'tab-active' : ''}"
|
||||
onclick={() => abaAtiva = "atividades"}
|
||||
class="tab flex items-center gap-2 {abaAtiva === 'atividades'
|
||||
? 'tab-active'
|
||||
: ''}"
|
||||
onclick={() => (abaAtiva = "atividades")}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"
|
||||
/>
|
||||
</svg>
|
||||
<span class="font-medium">Atividades no Sistema</span>
|
||||
</button>
|
||||
<button
|
||||
class="tab flex items-center gap-2 {abaAtiva === 'logins' ? 'tab-active' : ''}"
|
||||
onclick={() => abaAtiva = "logins"}
|
||||
class="tab flex items-center gap-2 {abaAtiva === 'logins'
|
||||
? 'tab-active'
|
||||
: ''}"
|
||||
onclick={() => (abaAtiva = "logins")}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"
|
||||
/>
|
||||
</svg>
|
||||
<span class="font-medium">Histórico de Logins</span>
|
||||
</button>
|
||||
@@ -149,10 +246,13 @@
|
||||
<div class="card-body py-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label py-1">
|
||||
<div class="label py-1">
|
||||
<span class="label-text font-medium">Quantidade de registros</span>
|
||||
</label>
|
||||
<select bind:value={limite} class="select select-bordered select-sm w-full max-w-xs">
|
||||
</div>
|
||||
<select
|
||||
bind:value={limite}
|
||||
class="select select-bordered select-sm w-full max-w-xs"
|
||||
>
|
||||
<option value={20}>20 registros</option>
|
||||
<option value={50}>50 registros</option>
|
||||
<option value={100}>100 registros</option>
|
||||
@@ -162,14 +262,36 @@
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-outline btn-primary btn-sm gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||
/>
|
||||
</svg>
|
||||
Exportar CSV
|
||||
</button>
|
||||
<button class="btn btn-outline btn-secondary btn-sm gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
|
||||
/>
|
||||
</svg>
|
||||
Filtros Avançados
|
||||
</button>
|
||||
@@ -184,25 +306,54 @@
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="card-title text-xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 mr-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
|
||||
/>
|
||||
</svg>
|
||||
Atividades Recentes
|
||||
</h2>
|
||||
{#if atividades?.data}
|
||||
<div class="badge badge-outline badge-lg">{atividades.data.length} registro{atividades.data.length !== 1 ? 's' : ''}</div>
|
||||
<div class="badge badge-outline badge-lg">
|
||||
{atividades.data.length} registro{atividades.data.length !== 1
|
||||
? "s"
|
||||
: ""}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if !atividades?.data}
|
||||
<div class="flex flex-col items-center justify-center py-16">
|
||||
<span class="loading loading-spinner loading-lg text-primary mb-4"></span>
|
||||
<span class="loading loading-spinner loading-lg text-primary mb-4"
|
||||
></span>
|
||||
<p class="text-base-content/60">Carregando atividades...</p>
|
||||
</div>
|
||||
{:else if atividades.data.length === 0}
|
||||
<div class="flex flex-col items-center justify-center py-16 text-base-content/60">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mb-4 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
<div
|
||||
class="flex flex-col items-center justify-center py-16 text-base-content/60"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-16 w-16 mb-4 opacity-50"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
<p class="text-lg font-medium">Nenhuma atividade registrada</p>
|
||||
<p class="text-sm mt-1">As atividades do sistema aparecerão aqui</p>
|
||||
@@ -224,32 +375,58 @@
|
||||
<tr class="hover transition-colors">
|
||||
<td>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-base-content/40" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 text-base-content/40"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="font-mono text-xs">{formatarData(atividade.timestamp)}</span>
|
||||
<span class="font-mono text-xs"
|
||||
>{formatarData(atividade.timestamp)}</span
|
||||
>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex flex-col">
|
||||
<div class="font-semibold text-sm">{atividade.usuarioNome || "Sistema"}</div>
|
||||
<div class="font-semibold text-sm">
|
||||
{atividade.usuarioNome || "Sistema"}
|
||||
</div>
|
||||
{#if atividade.usuarioMatricula}
|
||||
<div class="text-xs text-base-content/50 font-mono">{atividade.usuarioMatricula}</div>
|
||||
<div class="text-xs text-base-content/50 font-mono">
|
||||
{atividade.usuarioMatricula}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge {getAcaoColor(atividade.acao)} badge-sm gap-1">
|
||||
<span
|
||||
class="badge {getAcaoColor(
|
||||
atividade.acao,
|
||||
)} badge-sm gap-1"
|
||||
>
|
||||
{getAcaoLabel(atividade.acao)}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="font-medium text-sm">{atividade.recurso}</span>
|
||||
<span class="font-medium text-sm"
|
||||
>{atividade.recurso}</span
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<div class="max-w-md">
|
||||
{#if atividade.detalhes}
|
||||
<div class="text-xs text-base-content/70 truncate" title={atividade.detalhes}>
|
||||
<div
|
||||
class="text-xs text-base-content/70 truncate"
|
||||
title={atividade.detalhes}
|
||||
>
|
||||
{atividade.detalhes}
|
||||
</div>
|
||||
{:else}
|
||||
@@ -270,25 +447,52 @@
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="card-title text-xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 mr-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"
|
||||
/>
|
||||
</svg>
|
||||
Histórico de Logins
|
||||
</h2>
|
||||
{#if logins?.data}
|
||||
<div class="badge badge-outline badge-lg">{logins.data.length} registro{logins.data.length !== 1 ? 's' : ''}</div>
|
||||
<div class="badge badge-outline badge-lg">
|
||||
{logins.data.length} registro{logins.data.length !== 1 ? "s" : ""}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if !logins?.data}
|
||||
<div class="flex flex-col items-center justify-center py-16">
|
||||
<span class="loading loading-spinner loading-lg text-primary mb-4"></span>
|
||||
<span class="loading loading-spinner loading-lg text-primary mb-4"
|
||||
></span>
|
||||
<p class="text-base-content/60">Carregando logins...</p>
|
||||
</div>
|
||||
{:else if logins.data.length === 0}
|
||||
<div class="flex flex-col items-center justify-center py-16 text-base-content/60">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mb-4 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1" />
|
||||
<div
|
||||
class="flex flex-col items-center justify-center py-16 text-base-content/60"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-16 w-16 mb-4 opacity-50"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"
|
||||
/>
|
||||
</svg>
|
||||
<p class="text-lg font-medium">Nenhum login registrado</p>
|
||||
<p class="text-sm mt-1">Os acessos ao sistema aparecerão aqui</p>
|
||||
@@ -312,53 +516,112 @@
|
||||
<tr class="hover transition-colors">
|
||||
<td>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-base-content/40" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 text-base-content/40"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="font-mono text-xs">{formatarData(login.timestamp)}</span>
|
||||
<span class="font-mono text-xs"
|
||||
>{formatarData(login.timestamp)}</span
|
||||
>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-base-content/40" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 text-base-content/40"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-sm font-medium">{login.matriculaOuEmail}</span>
|
||||
<span class="text-sm font-medium"
|
||||
>{login.matriculaOuEmail}</span
|
||||
>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex flex-col gap-1">
|
||||
{#if login.sucesso}
|
||||
<span class="badge badge-success badge-sm gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-3 w-3"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
Sucesso
|
||||
</span>
|
||||
{:else}
|
||||
<span class="badge badge-error badge-sm gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-3 w-3"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
Falhou
|
||||
</span>
|
||||
{#if login.motivoFalha}
|
||||
<div class="text-xs text-error mt-1 font-medium">{login.motivoFalha}</div>
|
||||
<div class="text-xs text-error mt-1 font-medium">
|
||||
{login.motivoFalha}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="font-mono text-xs bg-base-200 px-2 py-1 rounded">{login.ipAddress || "-"}</span>
|
||||
<span
|
||||
class="font-mono text-xs bg-base-200 px-2 py-1 rounded"
|
||||
>{login.ipAddress || "-"}</span
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-xs text-base-content/70">{login.device || "-"}</div>
|
||||
<div class="text-xs text-base-content/70">
|
||||
{login.device || "-"}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-xs text-base-content/70">{login.browser || "-"}</div>
|
||||
<div class="text-xs text-base-content/70">
|
||||
{login.browser || "-"}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-xs text-base-content/70">{login.sistema || "-"}</div>
|
||||
<div class="text-xs text-base-content/70">
|
||||
{login.sistema || "-"}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
@@ -372,13 +635,25 @@
|
||||
|
||||
<!-- Informação -->
|
||||
<div class="alert alert-info shadow-lg mt-6">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="stroke-current shrink-0 w-6 h-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
></path>
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="font-bold">Informação Importante</h3>
|
||||
<div class="text-sm">Os logs são armazenados permanentemente e não podem ser alterados ou excluídos.</div>
|
||||
<div class="text-sm">
|
||||
Os logs são armazenados permanentemente e não podem ser alterados ou
|
||||
excluídos.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -6,19 +6,45 @@
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="p-3 bg-gradient-to-br from-primary/20 to-primary/10 rounded-2xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
|
||||
<div
|
||||
class="p-3 bg-linear-to-br from-primary/20 to-primary/10 rounded-2xl"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-10 w-10 text-primary"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-4xl font-bold text-primary">Monitoramento SGSE</h1>
|
||||
<p class="text-base-content/60 mt-2 text-lg">Sistema de monitoramento técnico em tempo real</p>
|
||||
<p class="text-base-content/60 mt-2 text-lg">
|
||||
Sistema de monitoramento técnico em tempo real
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/ti" class="btn btn-ghost">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 19l-7-7m0 0l7-7m-7 7h18"
|
||||
/>
|
||||
</svg>
|
||||
Voltar
|
||||
</a>
|
||||
@@ -27,4 +53,3 @@
|
||||
<!-- Card de Monitoramento -->
|
||||
<SystemMonitorCardLocal />
|
||||
</div>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,17 @@
|
||||
import { useQuery, useConvexClient } from "convex-svelte";
|
||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||
import StatsCard from "$lib/components/ti/StatsCard.svelte";
|
||||
import {
|
||||
Users,
|
||||
CheckCircle,
|
||||
Ban,
|
||||
Clock,
|
||||
BarChart3,
|
||||
Plus,
|
||||
FolderTree,
|
||||
FileText,
|
||||
Info,
|
||||
} from "lucide-svelte";
|
||||
|
||||
const client = useConvexClient();
|
||||
const usuariosQuery = useQuery(api.usuarios.listar, {});
|
||||
@@ -23,19 +34,19 @@
|
||||
total: 0,
|
||||
ativos: 0,
|
||||
bloqueados: 0,
|
||||
inativos: 0
|
||||
inativos: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const ativos = usuarios.filter(u => u.ativo && !u.bloqueado).length;
|
||||
const bloqueados = usuarios.filter(u => u.bloqueado === true).length;
|
||||
const inativos = usuarios.filter(u => !u.ativo).length;
|
||||
const ativos = usuarios.filter((u) => u.ativo && !u.bloqueado).length;
|
||||
const bloqueados = usuarios.filter((u) => u.bloqueado === true).length;
|
||||
const inativos = usuarios.filter((u) => !u.ativo).length;
|
||||
|
||||
return {
|
||||
total: usuarios.length,
|
||||
ativos,
|
||||
bloqueados,
|
||||
inativos
|
||||
inativos,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
@@ -45,13 +56,15 @@
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="p-3 bg-primary/10 rounded-xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
<BarChart3 class="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-base-content">Dashboard Administrativo TI</h1>
|
||||
<p class="text-base-content/60 mt-1">Painel de controle e monitoramento do sistema</p>
|
||||
<h1 class="text-3xl font-bold text-base-content">
|
||||
Dashboard Administrativo TI
|
||||
</h1>
|
||||
<p class="text-base-content/60 mt-1">
|
||||
Painel de controle e monitoramento do sistema
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,34 +72,38 @@
|
||||
<!-- Stats Cards -->
|
||||
{#if stats}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||
<StatsCard
|
||||
title="Total de Usuários"
|
||||
value={stats.total}
|
||||
icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />'
|
||||
icon={Users}
|
||||
color="primary"
|
||||
/>
|
||||
|
||||
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||
<StatsCard
|
||||
title="Usuários Ativos"
|
||||
value={stats.ativos}
|
||||
description="{stats.total > 0 ? ((stats.ativos / stats.total) * 100).toFixed(1) + '% do total' : '0% do total'}"
|
||||
icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />'
|
||||
description={`${stats.total > 0 ? ((stats.ativos / stats.total) * 100).toFixed(1) + "% do total" : "0% do total"}`}
|
||||
icon={CheckCircle}
|
||||
color="success"
|
||||
/>
|
||||
|
||||
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||
<StatsCard
|
||||
title="Usuários Bloqueados"
|
||||
value={stats.bloqueados}
|
||||
description="Requerem atenção"
|
||||
icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />'
|
||||
icon={Ban}
|
||||
color="error"
|
||||
/>
|
||||
|
||||
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||
<StatsCard
|
||||
title="Usuários Inativos"
|
||||
value={stats.inativos}
|
||||
description="Desativados"
|
||||
icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />'
|
||||
icon={Clock}
|
||||
color="warning"
|
||||
/>
|
||||
</div>
|
||||
@@ -102,23 +119,17 @@
|
||||
<h2 class="card-title text-2xl mb-4">Ações Rápidas</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<a href="/ti/usuarios" class="btn btn-primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
<Plus class="h-5 w-5" />
|
||||
Criar Usuário
|
||||
</a>
|
||||
|
||||
<a href="/ti/perfis" class="btn btn-secondary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
</svg>
|
||||
<FolderTree class="h-5 w-5" />
|
||||
Gerenciar Perfis
|
||||
</a>
|
||||
|
||||
<a href="/ti/auditoria" class="btn btn-accent">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<FileText class="h-5 w-5" />
|
||||
Ver Logs
|
||||
</a>
|
||||
</div>
|
||||
@@ -127,9 +138,10 @@
|
||||
|
||||
<!-- Informação Sistema -->
|
||||
<div class="alert alert-info">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>Sistema de Gestão da Secretaria de Esportes - Versão 2.0 com controle avançado de acesso</span>
|
||||
<Info class="stroke-current shrink-0 w-6 h-6" />
|
||||
<span
|
||||
>Sistema de Gestão da Secretaria de Esportes - Versão 2.0 com controle
|
||||
avançado de acesso</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,21 @@
|
||||
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
||||
import { format } from "date-fns";
|
||||
import { ptBR } from "date-fns/locale";
|
||||
import {
|
||||
Users,
|
||||
Shield,
|
||||
ShieldAlert,
|
||||
Info,
|
||||
Building2,
|
||||
Search,
|
||||
X,
|
||||
Eye,
|
||||
Calendar,
|
||||
Tag,
|
||||
Settings,
|
||||
User,
|
||||
AlertCircle,
|
||||
} from "lucide-svelte";
|
||||
|
||||
type Role = {
|
||||
_id: Id<"roles">;
|
||||
@@ -40,10 +55,10 @@
|
||||
if (carregando) return null;
|
||||
|
||||
const porNivel = {
|
||||
0: roles.filter(r => r.nivel === 0).length,
|
||||
1: roles.filter(r => r.nivel === 1).length,
|
||||
2: roles.filter(r => r.nivel === 2).length,
|
||||
3: roles.filter(r => r.nivel >= 3).length,
|
||||
0: roles.filter((r) => r.nivel === 0).length,
|
||||
1: roles.filter((r) => r.nivel === 1).length,
|
||||
2: roles.filter((r) => r.nivel === 2).length,
|
||||
3: roles.filter((r) => r.nivel >= 3).length,
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -52,7 +67,7 @@
|
||||
nivelAlto: porNivel[1],
|
||||
nivelMedio: porNivel[2],
|
||||
nivelBaixo: porNivel[3],
|
||||
comSetor: roles.filter(r => r.setor).length,
|
||||
comSetor: roles.filter((r) => r.setor).length,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -65,7 +80,7 @@
|
||||
resultado = resultado.filter(
|
||||
(r) =>
|
||||
r.nome.toLowerCase().includes(buscaLower) ||
|
||||
r.descricao.toLowerCase().includes(buscaLower)
|
||||
r.descricao.toLowerCase().includes(buscaLower),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -137,33 +152,27 @@
|
||||
filtroNivel = "";
|
||||
}
|
||||
|
||||
const temFiltrosAtivos = $derived(busca.trim() !== "" || filtroSetor !== "" || filtroNivel !== "");
|
||||
const temFiltrosAtivos = $derived(
|
||||
busca.trim() !== "" || filtroSetor !== "" || filtroNivel !== "",
|
||||
);
|
||||
</script>
|
||||
|
||||
<ProtectedRoute allowedRoles={["ti_master", "admin", "ti_usuario"]} maxLevel={3}>
|
||||
<ProtectedRoute
|
||||
allowedRoles={["ti_master", "admin", "ti_usuario"]}
|
||||
maxLevel={3}
|
||||
>
|
||||
<div class="container mx-auto px-4 py-6 max-w-7xl">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="p-3 bg-primary/10 rounded-xl">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8 text-primary"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
/>
|
||||
</svg>
|
||||
<Users class="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-base-content">Gestão de Perfis</h1>
|
||||
<p class="text-base-content/60 mt-1">Visualize e gerencie os perfis de acesso do sistema</p>
|
||||
<p class="text-base-content/60 mt-1">
|
||||
Visualize e gerencie os perfis de acesso do sistema
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -171,38 +180,45 @@
|
||||
<!-- Estatísticas -->
|
||||
{#if stats}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4 mb-8">
|
||||
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||
<StatsCard
|
||||
title="Total de Perfis"
|
||||
value={stats.total}
|
||||
icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />'
|
||||
icon={Users}
|
||||
color="primary"
|
||||
/>
|
||||
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||
<StatsCard
|
||||
title="Nível Máximo"
|
||||
value={stats.nivelMaximo}
|
||||
description="Acesso total"
|
||||
icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />'
|
||||
icon={Shield}
|
||||
color="error"
|
||||
/>
|
||||
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||
<StatsCard
|
||||
title="Nível Alto"
|
||||
value={stats.nivelAlto}
|
||||
description="Acesso elevado"
|
||||
icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />'
|
||||
icon={ShieldAlert}
|
||||
color="warning"
|
||||
/>
|
||||
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||
<StatsCard
|
||||
title="Nível Médio"
|
||||
value={stats.nivelMedio}
|
||||
description="Acesso padrão"
|
||||
icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />'
|
||||
icon={Info}
|
||||
color="info"
|
||||
/>
|
||||
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||
<StatsCard
|
||||
title="Com Setor"
|
||||
value={stats.comSetor}
|
||||
description="{stats.total > 0 ? ((stats.comSetor / stats.total) * 100).toFixed(0) + '% do total' : '0%'}"
|
||||
icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />'
|
||||
description={stats.total > 0
|
||||
? ((stats.comSetor / stats.total) * 100).toFixed(0) + "% do total"
|
||||
: "0%"}
|
||||
icon={Building2}
|
||||
color="secondary"
|
||||
/>
|
||||
</div>
|
||||
@@ -214,38 +230,16 @@
|
||||
<div class="card-body">
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 mb-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 text-primary"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
|
||||
/>
|
||||
</svg>
|
||||
<Search class="h-5 w-5 text-primary" />
|
||||
<h2 class="card-title text-lg">Filtros de Busca</h2>
|
||||
</div>
|
||||
{#if temFiltrosAtivos}
|
||||
<button type="button" class="btn btn-sm btn-outline btn-error" onclick={limparFiltros}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline btn-error"
|
||||
onclick={limparFiltros}
|
||||
>
|
||||
<X class="h-4 w-4" />
|
||||
Limpar Filtros
|
||||
</button>
|
||||
{/if}
|
||||
@@ -265,20 +259,9 @@
|
||||
placeholder="Buscar por nome ou descrição..."
|
||||
class="input input-bordered w-full pl-10"
|
||||
/>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
<Search
|
||||
class="h-5 w-5 absolute left-3 top-1/2 transform -translate-y-1/2 text-base-content/40"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -287,7 +270,11 @@
|
||||
<label class="label" for="filtro-setor">
|
||||
<span class="label-text font-medium">Setor</span>
|
||||
</label>
|
||||
<select id="filtro-setor" bind:value={filtroSetor} class="select select-bordered">
|
||||
<select
|
||||
id="filtro-setor"
|
||||
bind:value={filtroSetor}
|
||||
class="select select-bordered"
|
||||
>
|
||||
<option value="">Todos os setores</option>
|
||||
{#each setoresDisponiveis as setor}
|
||||
<option value={setor}>{setor}</option>
|
||||
@@ -300,7 +287,11 @@
|
||||
<label class="label" for="filtro-nivel">
|
||||
<span class="label-text font-medium">Nível de Acesso</span>
|
||||
</label>
|
||||
<select id="filtro-nivel" bind:value={filtroNivel} class="select select-bordered">
|
||||
<select
|
||||
id="filtro-nivel"
|
||||
bind:value={filtroNivel}
|
||||
class="select select-bordered"
|
||||
>
|
||||
<option value="">Todos os níveis</option>
|
||||
<option value={0}>Máximo (0)</option>
|
||||
<option value={1}>Alto (1)</option>
|
||||
@@ -312,7 +303,12 @@
|
||||
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<div class="text-sm text-base-content/60">
|
||||
<span class="font-medium text-base-content">{rolesFiltradas.length}</span> de <span class="font-medium text-base-content">{roles.length}</span> perfil(is)
|
||||
<span class="font-medium text-base-content"
|
||||
>{rolesFiltradas.length}</span
|
||||
>
|
||||
de
|
||||
<span class="font-medium text-base-content">{roles.length}</span>
|
||||
perfil(is)
|
||||
{#if temFiltrosAtivos}
|
||||
<span class="badge badge-primary badge-sm ml-2">Filtrado</span>
|
||||
{/if}
|
||||
@@ -329,45 +325,28 @@
|
||||
</div>
|
||||
{:else if roles.length === 0}
|
||||
<div class="flex flex-col items-center justify-center py-16 text-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-16 w-16 text-base-content/30"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
/>
|
||||
</svg>
|
||||
<Users class="h-16 w-16 text-base-content/30" />
|
||||
<h3 class="text-xl font-semibold mt-4">Nenhum perfil encontrado</h3>
|
||||
<p class="text-base-content/60 mt-2">Não há perfis cadastrados no sistema.</p>
|
||||
<p class="text-base-content/60 mt-2">
|
||||
Não há perfis cadastrados no sistema.
|
||||
</p>
|
||||
</div>
|
||||
{:else if rolesFiltradas.length === 0}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-col items-center justify-center py-16 text-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-16 w-16 text-base-content/30 mb-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
class="flex flex-col items-center justify-center py-16 text-center"
|
||||
>
|
||||
<AlertCircle class="h-16 w-16 text-base-content/30 mb-4" />
|
||||
<h3 class="text-xl font-semibold mt-4">Nenhum perfil encontrado</h3>
|
||||
<p class="text-base-content/60 mt-2">Nenhum perfil corresponde aos filtros aplicados.</p>
|
||||
<p class="text-base-content/60 mt-2">
|
||||
Nenhum perfil corresponde aos filtros aplicados.
|
||||
</p>
|
||||
{#if temFiltrosAtivos}
|
||||
<button class="btn btn-primary btn-sm mt-4" onclick={limparFiltros}>
|
||||
<button
|
||||
class="btn btn-primary btn-sm mt-4"
|
||||
onclick={limparFiltros}
|
||||
>
|
||||
Limpar Filtros
|
||||
</button>
|
||||
{/if}
|
||||
@@ -376,115 +355,73 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{#each rolesFiltradas as role}
|
||||
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-all duration-300 cursor-pointer border border-base-300 {obterCorCardNivel(role.nivel)} hover:scale-[1.02]" onclick={() => abrirDetalhes(role)}>
|
||||
{#each rolesFiltradas as role (role._id)}
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class="card bg-base-100 shadow-xl hover:shadow-2xl transition-all duration-300 cursor-pointer border border-base-300 {obterCorCardNivel(
|
||||
role.nivel,
|
||||
)} hover:scale-[1.02]"
|
||||
onclick={() => abrirDetalhes(role)}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
abrirDetalhes(role);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="card-body">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="flex-1">
|
||||
<h2 class="card-title text-lg mb-1">{role.descricao}</h2>
|
||||
<div class="badge {obterCorNivel(role.nivel)} badge-sm">{obterTextoNivel(role.nivel)}</div>
|
||||
<div class="badge {obterCorNivel(role.nivel)} badge-sm">
|
||||
{obterTextoNivel(role.nivel)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-2 bg-base-200 rounded-lg">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 text-primary"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
<User class="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 text-sm">
|
||||
<div class="flex items-center gap-2 p-2 bg-base-200 rounded-lg">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 text-base-content/40"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
<Tag class="h-4 w-4 text-base-content/40" />
|
||||
<span class="font-medium text-base-content/60"
|
||||
>Nome técnico:</span
|
||||
>
|
||||
<code class="text-xs bg-base-100 px-2 py-1 rounded font-mono"
|
||||
>{role.nome}</code
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="font-medium text-base-content/60">Nome técnico:</span>
|
||||
<code class="text-xs bg-base-100 px-2 py-1 rounded font-mono">{role.nome}</code>
|
||||
</div>
|
||||
|
||||
{#if role.setor}
|
||||
<div class="flex items-center gap-2 p-2 bg-base-200 rounded-lg">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 text-base-content/40"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
class="flex items-center gap-2 p-2 bg-base-200 rounded-lg"
|
||||
>
|
||||
<Building2 class="h-4 w-4 text-base-content/40" />
|
||||
<span class="font-medium text-base-content/60">Setor:</span>
|
||||
<span class="font-medium">{role.setor}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex items-center gap-2 p-2 bg-base-200 rounded-lg">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 text-base-content/40"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
|
||||
/>
|
||||
</svg>
|
||||
<Shield class="h-4 w-4 text-base-content/40" />
|
||||
<span class="font-medium text-base-content/60">Nível:</span>
|
||||
<span class="font-bold text-lg">{role.nivel}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-actions justify-end mt-4 pt-4 border-t border-base-300">
|
||||
<button class="btn btn-sm btn-primary btn-outline" onclick={(e) => { e.stopPropagation(); abrirDetalhes(role); }}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 mr-1"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
class="card-actions justify-end mt-4 pt-4 border-t border-base-300"
|
||||
>
|
||||
<button
|
||||
class="btn btn-sm btn-primary btn-outline"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
abrirDetalhes(role);
|
||||
}}
|
||||
>
|
||||
<Eye class="h-4 w-4 mr-1" />
|
||||
Ver Detalhes
|
||||
</button>
|
||||
</div>
|
||||
@@ -501,40 +438,41 @@
|
||||
<div class="modal-box max-w-3xl">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="font-bold text-2xl">Detalhes do Perfil</h3>
|
||||
<button type="button" class="btn btn-sm btn-circle btn-ghost" onclick={fecharDetalhes}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-circle btn-ghost"
|
||||
onclick={fecharDetalhes}
|
||||
>
|
||||
<X class="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Header do Perfil -->
|
||||
<div class="card bg-gradient-to-r from-primary/10 to-secondary/10 border border-primary/20">
|
||||
<div
|
||||
class="card bg-linear-to-r from-primary/10 to-secondary/10 border border-primary/20"
|
||||
>
|
||||
<div class="card-body">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<h2 class="text-2xl font-bold mb-2">{roleSelecionada.descricao}</h2>
|
||||
<h2 class="text-2xl font-bold mb-2">
|
||||
{roleSelecionada.descricao}
|
||||
</h2>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="badge {obterCorNivel(roleSelecionada.nivel)} badge-lg">{obterTextoNivel(roleSelecionada.nivel)}</div>
|
||||
<span class="text-sm text-base-content/60">Nível {roleSelecionada.nivel}</span>
|
||||
<div
|
||||
class="badge {obterCorNivel(
|
||||
roleSelecionada.nivel,
|
||||
)} badge-lg"
|
||||
>
|
||||
{obterTextoNivel(roleSelecionada.nivel)}
|
||||
</div>
|
||||
<span class="text-sm text-base-content/60"
|
||||
>Nível {roleSelecionada.nivel}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3 bg-base-100 rounded-lg shadow-sm">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8 text-primary"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
<User class="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -545,24 +483,27 @@
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
||||
</svg>
|
||||
<span
|
||||
class="label-text font-semibold flex items-center gap-2"
|
||||
>
|
||||
<Tag class="h-5 w-5 text-primary" />
|
||||
Nome Técnico
|
||||
</span>
|
||||
</label>
|
||||
<code class="block bg-base-200 px-4 py-3 rounded-lg text-sm font-mono mt-2">{roleSelecionada.nome}</code>
|
||||
<code
|
||||
class="block bg-base-200 px-4 py-3 rounded-lg text-sm font-mono mt-2"
|
||||
>{roleSelecionada.nome}</code
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
<span
|
||||
class="label-text font-semibold flex items-center gap-2"
|
||||
>
|
||||
<Building2 class="h-5 w-5 text-primary" />
|
||||
Setor
|
||||
</span>
|
||||
</label>
|
||||
@@ -570,7 +511,9 @@
|
||||
{#if roleSelecionada.setor}
|
||||
{roleSelecionada.setor}
|
||||
{:else}
|
||||
<span class="text-base-content/40 italic">Não especificado</span>
|
||||
<span class="text-base-content/40 italic"
|
||||
>Não especificado</span
|
||||
>
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
@@ -582,26 +525,33 @@
|
||||
<div class="card-body">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
<Shield class="h-5 w-5 text-primary" />
|
||||
Nível de Acesso
|
||||
</span>
|
||||
</label>
|
||||
<div class="mt-4">
|
||||
<div class="flex items-center gap-4 mb-3">
|
||||
<span class="text-4xl font-bold">{roleSelecionada.nivel}</span>
|
||||
<div class="badge {obterCorNivel(roleSelecionada.nivel)} badge-lg">{obterTextoNivel(roleSelecionada.nivel)}</div>
|
||||
<span class="text-4xl font-bold">{roleSelecionada.nivel}</span
|
||||
>
|
||||
<div
|
||||
class="badge {obterCorNivel(
|
||||
roleSelecionada.nivel,
|
||||
)} badge-lg"
|
||||
>
|
||||
{obterTextoNivel(roleSelecionada.nivel)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-info">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<Info class="stroke-current shrink-0 w-6 h-6" />
|
||||
<span class="text-sm">
|
||||
{roleSelecionada.nivel === 0 && "Acesso total irrestrito ao sistema. Pode realizar todas as operações sem restrições."}
|
||||
{roleSelecionada.nivel === 1 && "Acesso alto com algumas restrições. Pode realizar a maioria das operações administrativas."}
|
||||
{roleSelecionada.nivel === 2 && "Acesso médio com permissões configuráveis. Pode realizar operações padrão do sistema."}
|
||||
{roleSelecionada.nivel >= 3 && "Acesso limitado com permissões específicas. Operações restritas conforme configuração."}
|
||||
{roleSelecionada.nivel === 0 &&
|
||||
"Acesso total irrestrito ao sistema. Pode realizar todas as operações sem restrições."}
|
||||
{roleSelecionada.nivel === 1 &&
|
||||
"Acesso alto com algumas restrições. Pode realizar a maioria das operações administrativas."}
|
||||
{roleSelecionada.nivel === 2 &&
|
||||
"Acesso médio com permissões configuráveis. Pode realizar operações padrão do sistema."}
|
||||
{roleSelecionada.nivel >= 3 &&
|
||||
"Acesso limitado com permissões específicas. Operações restritas conforme configuração."}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -613,35 +563,27 @@
|
||||
<div class="card-body">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<Calendar class="h-5 w-5 text-primary" />
|
||||
Data de Criação
|
||||
</span>
|
||||
</label>
|
||||
<p class="text-lg font-medium mt-2">{formatarData(roleSelecionada._creationTime)}</p>
|
||||
<p class="text-lg font-medium mt-2">
|
||||
{formatarData(roleSelecionada._creationTime)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Informação sobre Permissões -->
|
||||
<div class="alert alert-info">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="stroke-current shrink-0 w-6 h-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
></path>
|
||||
</svg>
|
||||
<Info class="stroke-current shrink-0 w-6 h-6" />
|
||||
<div>
|
||||
<h4 class="font-semibold mb-1">Configuração de Permissões</h4>
|
||||
<p class="text-sm">
|
||||
Para configurar permissões específicas deste perfil, acesse o <a href="/ti/painel-permissoes" class="link link-primary font-semibold">Painel de Permissões</a>.
|
||||
Para configurar permissões específicas deste perfil, acesse o <a
|
||||
href="/ti/painel-permissoes"
|
||||
class="link link-primary font-semibold"
|
||||
>Painel de Permissões</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -652,16 +594,23 @@
|
||||
Fechar
|
||||
</button>
|
||||
<a href="/ti/painel-permissoes" class="btn btn-primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<Settings class="h-5 w-5 mr-2" />
|
||||
Configurar Permissões
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-backdrop" onclick={fecharDetalhes}></div>
|
||||
<div
|
||||
class="modal-backdrop"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onclick={fecharDetalhes}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
fecharDetalhes();
|
||||
}
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
{/if}
|
||||
</ProtectedRoute>
|
||||
|
||||
|
||||
@@ -6,6 +6,22 @@
|
||||
import StatsCard from "$lib/components/ti/StatsCard.svelte";
|
||||
import { format } from "date-fns";
|
||||
import { ptBR } from "date-fns/locale";
|
||||
import {
|
||||
FileText,
|
||||
Clock,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
UserPlus,
|
||||
Search,
|
||||
Eye,
|
||||
Check,
|
||||
X,
|
||||
CircleCheckBig,
|
||||
CircleX,
|
||||
Mail,
|
||||
Phone,
|
||||
Calendar,
|
||||
} from "lucide-svelte";
|
||||
|
||||
type StatusSolicitacao = "pendente" | "aprovado" | "rejeitado";
|
||||
|
||||
@@ -52,7 +68,9 @@
|
||||
}
|
||||
|
||||
if ("data" in solicitacoesQuery && solicitacoesQuery.data !== undefined) {
|
||||
return Array.isArray(solicitacoesQuery.data) ? solicitacoesQuery.data : [];
|
||||
return Array.isArray(solicitacoesQuery.data)
|
||||
? solicitacoesQuery.data
|
||||
: [];
|
||||
}
|
||||
|
||||
if (Array.isArray(solicitacoesQuery)) {
|
||||
@@ -71,15 +89,21 @@
|
||||
if (carregando) return null;
|
||||
|
||||
const total = solicitacoes.length;
|
||||
const pendentes = solicitacoes.filter(s => s.status === "pendente").length;
|
||||
const aprovadas = solicitacoes.filter(s => s.status === "aprovado").length;
|
||||
const rejeitadas = solicitacoes.filter(s => s.status === "rejeitado").length;
|
||||
const pendentes = solicitacoes.filter(
|
||||
(s) => s.status === "pendente",
|
||||
).length;
|
||||
const aprovadas = solicitacoes.filter(
|
||||
(s) => s.status === "aprovado",
|
||||
).length;
|
||||
const rejeitadas = solicitacoes.filter(
|
||||
(s) => s.status === "rejeitado",
|
||||
).length;
|
||||
|
||||
return {
|
||||
total,
|
||||
pendentes,
|
||||
aprovadas,
|
||||
rejeitadas
|
||||
rejeitadas,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -89,16 +113,17 @@
|
||||
|
||||
// Filtrar por status
|
||||
if (filtroStatus !== "todos") {
|
||||
resultado = resultado.filter(s => s.status === filtroStatus);
|
||||
resultado = resultado.filter((s) => s.status === filtroStatus);
|
||||
}
|
||||
|
||||
// Buscar por nome, matrícula ou email
|
||||
if (busca.trim()) {
|
||||
const termo = busca.toLowerCase().trim();
|
||||
resultado = resultado.filter(s =>
|
||||
s.nome.toLowerCase().includes(termo) ||
|
||||
s.matricula.toLowerCase().includes(termo) ||
|
||||
s.email.toLowerCase().includes(termo)
|
||||
resultado = resultado.filter(
|
||||
(s) =>
|
||||
s.nome.toLowerCase().includes(termo) ||
|
||||
s.matricula.toLowerCase().includes(termo) ||
|
||||
s.email.toLowerCase().includes(termo),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -108,7 +133,9 @@
|
||||
|
||||
// Funções auxiliares
|
||||
function formatarData(timestamp: number): string {
|
||||
return format(new Date(timestamp), "dd/MM/yyyy 'às' HH:mm", { locale: ptBR });
|
||||
return format(new Date(timestamp), "dd/MM/yyyy 'às' HH:mm", {
|
||||
locale: ptBR,
|
||||
});
|
||||
}
|
||||
|
||||
function formatarDataRelativa(timestamp: number): string {
|
||||
@@ -118,9 +145,9 @@
|
||||
const horas = Math.floor(diff / (1000 * 60 * 60));
|
||||
const minutos = Math.floor(diff / (1000 * 60));
|
||||
|
||||
if (dias > 0) return `${dias} dia${dias > 1 ? 's' : ''} atrás`;
|
||||
if (horas > 0) return `${horas} hora${horas > 1 ? 's' : ''} atrás`;
|
||||
if (minutos > 0) return `${minutos} minuto${minutos > 1 ? 's' : ''} atrás`;
|
||||
if (dias > 0) return `${dias} dia${dias > 1 ? "s" : ""} atrás`;
|
||||
if (horas > 0) return `${horas} hora${horas > 1 ? "s" : ""} atrás`;
|
||||
if (minutos > 0) return `${minutos} minuto${minutos > 1 ? "s" : ""} atrás`;
|
||||
return "Agora";
|
||||
}
|
||||
|
||||
@@ -210,7 +237,8 @@
|
||||
mensagem = null;
|
||||
}, 3000);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Erro ao aprovar solicitação";
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Erro ao aprovar solicitação";
|
||||
mensagem = {
|
||||
tipo: "error",
|
||||
texto: errorMessage,
|
||||
@@ -244,7 +272,8 @@
|
||||
mensagem = null;
|
||||
}, 3000);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Erro ao rejeitar solicitação";
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Erro ao rejeitar solicitação";
|
||||
mensagem = {
|
||||
tipo: "error",
|
||||
texto: errorMessage,
|
||||
@@ -255,19 +284,18 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<ProtectedRoute allowedRoles={["ti_master", "admin", "ti_usuario"]} maxLevel={3}>
|
||||
<ProtectedRoute
|
||||
allowedRoles={["ti_master", "admin", "ti_usuario"]}
|
||||
maxLevel={3}
|
||||
>
|
||||
<div class="container mx-auto px-4 py-6 max-w-7xl">
|
||||
<!-- Mensagem de Feedback -->
|
||||
{#if mensagem}
|
||||
<div class="alert alert-{mensagem.tipo} shadow-lg mb-6">
|
||||
{#if mensagem.tipo === "success"}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<CircleCheckBig class="stroke-current shrink-0 h-6 w-6" />
|
||||
{:else if mensagem.tipo === "error"}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<CircleX class="stroke-current shrink-0 h-6 w-6" />
|
||||
{/if}
|
||||
<span>{mensagem.texto}</span>
|
||||
</div>
|
||||
@@ -277,13 +305,15 @@
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="p-3 bg-primary/10 rounded-xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z" />
|
||||
</svg>
|
||||
<UserPlus class="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-base-content">Solicitações de Acesso</h1>
|
||||
<p class="text-base-content/60 mt-1">Gerencie e analise solicitações de acesso ao sistema</p>
|
||||
<h1 class="text-3xl font-bold text-base-content">
|
||||
Solicitações de Acesso
|
||||
</h1>
|
||||
<p class="text-base-content/60 mt-1">
|
||||
Gerencie e analise solicitações de acesso ao sistema
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -291,34 +321,38 @@
|
||||
<!-- Estatísticas -->
|
||||
{#if stats}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||
<StatsCard
|
||||
title="Total de Solicitações"
|
||||
value={stats.total}
|
||||
icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />'
|
||||
icon={FileText}
|
||||
color="primary"
|
||||
/>
|
||||
|
||||
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||
<StatsCard
|
||||
title="Pendentes"
|
||||
value={stats.pendentes}
|
||||
description="{stats.total > 0 ? ((stats.pendentes / stats.total) * 100).toFixed(1) + '% do total' : '0% do total'}"
|
||||
icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />'
|
||||
description={`${stats.total > 0 ? ((stats.pendentes / stats.total) * 100).toFixed(1) + "% do total" : "0% do total"}`}
|
||||
icon={Clock}
|
||||
color="warning"
|
||||
/>
|
||||
|
||||
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||
<StatsCard
|
||||
title="Aprovadas"
|
||||
value={stats.aprovadas}
|
||||
description="{stats.total > 0 ? ((stats.aprovadas / stats.total) * 100).toFixed(1) + '% do total' : '0% do total'}"
|
||||
icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />'
|
||||
description={`${stats.total > 0 ? ((stats.aprovadas / stats.total) * 100).toFixed(1) + "% do total" : "0% do total"}`}
|
||||
icon={CheckCircle}
|
||||
color="success"
|
||||
/>
|
||||
|
||||
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||
<StatsCard
|
||||
title="Rejeitadas"
|
||||
value={stats.rejeitadas}
|
||||
description="{stats.total > 0 ? ((stats.rejeitadas / stats.total) * 100).toFixed(1) + '% do total' : '0% do total'}"
|
||||
icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />'
|
||||
description={`${stats.total > 0 ? ((stats.rejeitadas / stats.total) * 100).toFixed(1) + "% do total" : "0% do total"}`}
|
||||
icon={XCircle}
|
||||
color="error"
|
||||
/>
|
||||
</div>
|
||||
@@ -335,25 +369,25 @@
|
||||
<div class="tabs tabs-boxed mb-4 bg-base-200 p-2">
|
||||
<button
|
||||
class="tab {filtroStatus === 'todos' ? 'tab-active' : ''}"
|
||||
onclick={() => filtroStatus = "todos"}
|
||||
onclick={() => (filtroStatus = "todos")}
|
||||
>
|
||||
Todas
|
||||
</button>
|
||||
<button
|
||||
class="tab {filtroStatus === 'pendente' ? 'tab-active' : ''}"
|
||||
onclick={() => filtroStatus = "pendente"}
|
||||
onclick={() => (filtroStatus = "pendente")}
|
||||
>
|
||||
Pendentes
|
||||
</button>
|
||||
<button
|
||||
class="tab {filtroStatus === 'aprovado' ? 'tab-active' : ''}"
|
||||
onclick={() => filtroStatus = "aprovado"}
|
||||
onclick={() => (filtroStatus = "aprovado")}
|
||||
>
|
||||
Aprovadas
|
||||
</button>
|
||||
<button
|
||||
class="tab {filtroStatus === 'rejeitado' ? 'tab-active' : ''}"
|
||||
onclick={() => filtroStatus = "rejeitado"}
|
||||
onclick={() => (filtroStatus = "rejeitado")}
|
||||
>
|
||||
Rejeitadas
|
||||
</button>
|
||||
@@ -361,9 +395,11 @@
|
||||
|
||||
<!-- Campo de Busca -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Buscar por nome, matrícula ou e-mail</span>
|
||||
</label>
|
||||
<div class="label">
|
||||
<span class="label-text font-semibold"
|
||||
>Buscar por nome, matrícula ou e-mail</span
|
||||
>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
@@ -371,9 +407,9 @@
|
||||
class="input input-bordered w-full pl-10"
|
||||
bind:value={busca}
|
||||
/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 absolute left-3 top-1/2 transform -translate-y-1/2 text-base-content/50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
<Search
|
||||
class="h-5 w-5 absolute left-3 top-1/2 transform -translate-y-1/2 text-base-content/50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -387,10 +423,10 @@
|
||||
{:else if solicitacoesFiltradas.length === 0}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body text-center py-20">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mx-auto text-base-content/30 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<h3 class="text-xl font-semibold text-base-content/70 mb-2">Nenhuma solicitação encontrada</h3>
|
||||
<FileText class="h-16 w-16 mx-auto text-base-content/30 mb-4" />
|
||||
<h3 class="text-xl font-semibold text-base-content/70 mb-2">
|
||||
Nenhuma solicitação encontrada
|
||||
</h3>
|
||||
<p class="text-base-content/50">
|
||||
{#if busca.trim() || filtroStatus !== "todos"}
|
||||
Tente ajustar os filtros ou a busca.
|
||||
@@ -403,47 +439,60 @@
|
||||
{:else}
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
{#each solicitacoesFiltradas as solicitacao}
|
||||
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow">
|
||||
<div
|
||||
class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow"
|
||||
>
|
||||
<div class="card-body">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div
|
||||
class="flex flex-col md:flex-row md:items-center md:justify-between gap-4"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<h3 class="text-xl font-bold text-base-content">{solicitacao.nome}</h3>
|
||||
<span class="badge {getStatusBadge(solicitacao.status)} badge-lg">
|
||||
<h3 class="text-xl font-bold text-base-content">
|
||||
{solicitacao.nome}
|
||||
</h3>
|
||||
<span
|
||||
class="badge {getStatusBadge(
|
||||
solicitacao.status,
|
||||
)} badge-lg"
|
||||
>
|
||||
{getStatusTexto(solicitacao.status)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-2 text-sm text-base-content/70">
|
||||
<div
|
||||
class="grid grid-cols-1 md:grid-cols-3 gap-2 text-sm text-base-content/70"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V8a2 2 0 00-2-2h-5m-4 0V5a2 2 0 114 0v1m-4 0a2 2 0 104 0m-5 8a2 2 0 100-4 2 2 0 000 4zm0 0c1.306 0 2.417.835 2.83 2M9 14a3.001 3.001 0 00-2.83 2M15 11h3m-3 4h2" />
|
||||
</svg>
|
||||
<UserPlus class="h-4 w-4" />
|
||||
<span class="font-semibold">Matrícula:</span>
|
||||
<span>{solicitacao.matricula}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<Mail class="h-4 w-4" />
|
||||
<span class="font-semibold">E-mail:</span>
|
||||
<span>{solicitacao.email}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||||
</svg>
|
||||
<Phone class="h-4 w-4" />
|
||||
<span class="font-semibold">Telefone:</span>
|
||||
<span>{solicitacao.telefone}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 text-xs text-base-content/50">
|
||||
<span class="font-semibold">Solicitado em:</span> {formatarData(solicitacao.dataSolicitacao)} ({formatarDataRelativa(solicitacao.dataSolicitacao)})
|
||||
<div
|
||||
class="mt-3 text-xs text-base-content/50 flex items-center gap-2"
|
||||
>
|
||||
<Calendar class="h-3 w-3" />
|
||||
<span class="font-semibold">Solicitado em:</span>
|
||||
{formatarData(solicitacao.dataSolicitacao)} ({formatarDataRelativa(
|
||||
solicitacao.dataSolicitacao,
|
||||
)})
|
||||
{#if solicitacao.dataResposta}
|
||||
<span class="ml-4 font-semibold">Processado em:</span> {formatarData(solicitacao.dataResposta)}
|
||||
<span class="ml-4 font-semibold">Processado em:</span>
|
||||
{formatarData(solicitacao.dataResposta)}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -453,10 +502,7 @@
|
||||
class="btn btn-sm btn-outline btn-primary"
|
||||
onclick={() => abrirDetalhes(solicitacao)}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
<Eye class="h-4 w-4" />
|
||||
Ver Detalhes
|
||||
</button>
|
||||
|
||||
@@ -465,9 +511,7 @@
|
||||
class="btn btn-sm btn-success"
|
||||
onclick={() => abrirAprovar(solicitacao)}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<Check class="h-4 w-4" />
|
||||
Aprovar
|
||||
</button>
|
||||
|
||||
@@ -475,9 +519,7 @@
|
||||
class="btn btn-sm btn-error"
|
||||
onclick={() => abrirRejeitar(solicitacao)}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<X class="h-4 w-4" />
|
||||
Rejeitar
|
||||
</button>
|
||||
{/if}
|
||||
@@ -497,44 +539,58 @@
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<span class="badge {getStatusBadge(solicitacaoSelecionada.status)} badge-lg">
|
||||
<span
|
||||
class="badge {getStatusBadge(
|
||||
solicitacaoSelecionada.status,
|
||||
)} badge-lg"
|
||||
>
|
||||
{getStatusTexto(solicitacaoSelecionada.status)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<div class="label">
|
||||
<span class="label-text font-semibold">Nome Completo</span>
|
||||
</label>
|
||||
<div class="input input-bordered">{solicitacaoSelecionada.nome}</div>
|
||||
</div>
|
||||
<div class="input input-bordered">
|
||||
{solicitacaoSelecionada.nome}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<div class="label">
|
||||
<span class="label-text font-semibold">Matrícula</span>
|
||||
</label>
|
||||
<div class="input input-bordered">{solicitacaoSelecionada.matricula}</div>
|
||||
</div>
|
||||
<div class="input input-bordered">
|
||||
{solicitacaoSelecionada.matricula}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<div class="label">
|
||||
<span class="label-text font-semibold">E-mail</span>
|
||||
</label>
|
||||
<div class="input input-bordered">{solicitacaoSelecionada.email}</div>
|
||||
</div>
|
||||
<div class="input input-bordered">
|
||||
{solicitacaoSelecionada.email}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<div class="label">
|
||||
<span class="label-text font-semibold">Telefone</span>
|
||||
</label>
|
||||
<div class="input input-bordered">{solicitacaoSelecionada.telefone}</div>
|
||||
</div>
|
||||
<div class="input input-bordered">
|
||||
{solicitacaoSelecionada.telefone}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Data da Solicitação</span>
|
||||
</label>
|
||||
<div class="label">
|
||||
<span class="label-text font-semibold"
|
||||
>Data da Solicitação</span
|
||||
>
|
||||
</div>
|
||||
<div class="input input-bordered">
|
||||
{formatarData(solicitacaoSelecionada.dataSolicitacao)}
|
||||
</div>
|
||||
@@ -542,9 +598,11 @@
|
||||
|
||||
{#if solicitacaoSelecionada.dataResposta}
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Data de Processamento</span>
|
||||
</label>
|
||||
<div class="label">
|
||||
<span class="label-text font-semibold"
|
||||
>Data de Processamento</span
|
||||
>
|
||||
</div>
|
||||
<div class="input input-bordered">
|
||||
{formatarData(solicitacaoSelecionada.dataResposta)}
|
||||
</div>
|
||||
@@ -554,9 +612,9 @@
|
||||
|
||||
{#if solicitacaoSelecionada.observacoes}
|
||||
<div>
|
||||
<label class="label">
|
||||
<div class="label">
|
||||
<span class="label-text font-semibold">Observações</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="textarea textarea-bordered min-h-24">
|
||||
{solicitacaoSelecionada.observacoes}
|
||||
</div>
|
||||
@@ -582,17 +640,22 @@
|
||||
|
||||
<div class="mb-4">
|
||||
<p class="text-base-content/70 mb-2">
|
||||
Você está prestes a aprovar a solicitação de acesso de <strong>{solicitacaoSelecionada.nome}</strong>.
|
||||
Você está prestes a aprovar a solicitação de acesso de <strong
|
||||
>{solicitacaoSelecionada.nome}</strong
|
||||
>.
|
||||
</p>
|
||||
<p class="text-sm text-base-content/60">
|
||||
Após aprovar, o sistema permitirá que esta pessoa solicite acesso ao sistema.
|
||||
Após aprovar, o sistema permitirá que esta pessoa solicite acesso
|
||||
ao sistema.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Observações (opcional)</span>
|
||||
</label>
|
||||
<div class="label">
|
||||
<span class="label-text font-semibold"
|
||||
>Observações (opcional)</span
|
||||
>
|
||||
</div>
|
||||
<textarea
|
||||
class="textarea textarea-bordered"
|
||||
placeholder="Adicione observações sobre a aprovação..."
|
||||
@@ -618,9 +681,7 @@
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
Processando...
|
||||
{:else}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<Check class="h-5 w-5" />
|
||||
Confirmar Aprovação
|
||||
{/if}
|
||||
</button>
|
||||
@@ -640,17 +701,22 @@
|
||||
|
||||
<div class="mb-4">
|
||||
<p class="text-base-content/70 mb-2">
|
||||
Você está prestes a rejeitar a solicitação de acesso de <strong>{solicitacaoSelecionada.nome}</strong>.
|
||||
Você está prestes a rejeitar a solicitação de acesso de <strong
|
||||
>{solicitacaoSelecionada.nome}</strong
|
||||
>.
|
||||
</p>
|
||||
<p class="text-sm text-base-content/60">
|
||||
Esta ação não pode ser desfeita. Recomendamos adicionar um motivo para a rejeição.
|
||||
Esta ação não pode ser desfeita. Recomendamos adicionar um motivo
|
||||
para a rejeição.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Motivo da Rejeição (recomendado)</span>
|
||||
</label>
|
||||
<div class="label">
|
||||
<span class="label-text font-semibold"
|
||||
>Motivo da Rejeição (recomendado)</span
|
||||
>
|
||||
</div>
|
||||
<textarea
|
||||
class="textarea textarea-bordered"
|
||||
placeholder="Descreva o motivo da rejeição..."
|
||||
@@ -676,9 +742,7 @@
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
Processando...
|
||||
{:else}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<X class="h-5 w-5" />
|
||||
Confirmar Rejeição
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
});
|
||||
|
||||
// Estados do formulário
|
||||
let matricula = $state("");
|
||||
let nome = $state("");
|
||||
let email = $state("");
|
||||
let roleId = $state("");
|
||||
@@ -30,7 +29,9 @@
|
||||
let senhaInicial = $state("");
|
||||
let confirmarSenha = $state("");
|
||||
let processando = $state(false);
|
||||
let mensagem = $state<{ tipo: "success" | "error"; texto: string } | null>(null);
|
||||
let mensagem = $state<{ tipo: "success" | "error"; texto: string } | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
function mostrarMensagem(tipo: "success" | "error", texto: string) {
|
||||
mensagem = { tipo, texto };
|
||||
@@ -43,8 +44,7 @@
|
||||
e.preventDefault();
|
||||
|
||||
// Validações
|
||||
const matriculaStr = String(matricula).trim();
|
||||
if (!matriculaStr || !nome.trim() || !email.trim() || !roleId || !senhaInicial) {
|
||||
if (!nome.trim() || !email.trim() || !roleId || !senhaInicial) {
|
||||
mostrarMensagem("error", "Preencha todos os campos obrigatórios");
|
||||
return;
|
||||
}
|
||||
@@ -63,11 +63,12 @@
|
||||
|
||||
try {
|
||||
const resultado = await client.mutation(api.usuarios.criar, {
|
||||
matricula: matriculaStr,
|
||||
nome: nome.trim(),
|
||||
email: email.trim(),
|
||||
roleId: roleId as Id<"roles">,
|
||||
funcionarioId: funcionarioId ? (funcionarioId as Id<"funcionarios">) : undefined,
|
||||
funcionarioId: funcionarioId
|
||||
? (funcionarioId as Id<"funcionarios">)
|
||||
: undefined,
|
||||
senhaInicial: senhaInicial,
|
||||
});
|
||||
|
||||
@@ -75,7 +76,7 @@
|
||||
if (senhaGerada) {
|
||||
mostrarMensagem(
|
||||
"success",
|
||||
`Usuário criado! SENHA TEMPORÁRIA: ${senhaGerada} - Anote esta senha, ela não será exibida novamente!`
|
||||
`Usuário criado! SENHA TEMPORÁRIA: ${senhaGerada} - Anote esta senha, ela não será exibida novamente!`,
|
||||
);
|
||||
setTimeout(() => {
|
||||
goto("/ti/usuarios");
|
||||
@@ -102,17 +103,19 @@
|
||||
// Auto-completar ao selecionar funcionário
|
||||
$effect(() => {
|
||||
if (funcionarioId && funcionarios?.data) {
|
||||
const funcSelecionado = funcionarios.data.find((f: any) => f._id === funcionarioId);
|
||||
const funcSelecionado = funcionarios.data.find(
|
||||
(f: any) => f._id === funcionarioId,
|
||||
);
|
||||
if (funcSelecionado) {
|
||||
email = funcSelecionado.email || email;
|
||||
nome = funcSelecionado.nome || nome;
|
||||
matricula = funcSelecionado.matricula || matricula;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function gerarSenhaAleatoria() {
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$!";
|
||||
const chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$!";
|
||||
let senha = "";
|
||||
for (let i = 0; i < 12; i++) {
|
||||
senha += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
@@ -154,8 +157,12 @@
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-base-content">Criar Novo Usuário</h1>
|
||||
<p class="text-base-content/60 mt-1">Cadastre um novo usuário no sistema</p>
|
||||
<h1 class="text-3xl font-bold text-base-content">
|
||||
Criar Novo Usuário
|
||||
</h1>
|
||||
<p class="text-base-content/60 mt-1">
|
||||
Cadastre um novo usuário no sistema
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/ti/usuarios" class="btn btn-outline btn-primary gap-2">
|
||||
@@ -248,7 +255,9 @@
|
||||
<!-- Funcionário (primeiro) -->
|
||||
<div class="form-control md:col-span-2">
|
||||
<label class="label" for="funcionario">
|
||||
<span class="label-text font-semibold">Vincular Funcionário (Opcional)</span>
|
||||
<span class="label-text font-semibold"
|
||||
>Vincular Funcionário (Opcional)</span
|
||||
>
|
||||
</label>
|
||||
<select
|
||||
id="funcionario"
|
||||
@@ -256,34 +265,24 @@
|
||||
bind:value={funcionarioId}
|
||||
disabled={processando || !funcionarios?.data}
|
||||
>
|
||||
<option value="">Selecione um funcionário para auto-completar dados</option>
|
||||
<option value=""
|
||||
>Selecione um funcionário para auto-completar dados</option
|
||||
>
|
||||
{#if funcionarios?.data}
|
||||
{#each funcionarios.data as func}
|
||||
<option value={func._id}>{func.nome} - Mat: {func.matricula}</option>
|
||||
<option value={func._id}
|
||||
>{func.nome} - Mat: {func.matricula}</option
|
||||
>
|
||||
{/each}
|
||||
{/if}
|
||||
</select>
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Ao selecionar, os campos serão preenchidos automaticamente</span>
|
||||
<span class="label-text-alt"
|
||||
>Ao selecionar, os campos serão preenchidos automaticamente</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Matrícula -->
|
||||
<div class="form-control">
|
||||
<label class="label" for="matricula">
|
||||
<span class="label-text font-semibold">Matrícula *</span>
|
||||
</label>
|
||||
<input
|
||||
id="matricula"
|
||||
type="number"
|
||||
placeholder="Ex: 12345"
|
||||
class="input input-bordered"
|
||||
bind:value={matricula}
|
||||
required
|
||||
disabled={processando}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Nome -->
|
||||
<div class="form-control">
|
||||
<label class="label" for="nome">
|
||||
@@ -301,7 +300,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="form-control md:col-span-2">
|
||||
<div class="form-control">
|
||||
<label class="label" for="email">
|
||||
<span class="label-text font-semibold">E-mail *</span>
|
||||
</label>
|
||||
@@ -341,7 +340,9 @@
|
||||
</select>
|
||||
{#if !roles?.data || !Array.isArray(roles.data)}
|
||||
<div class="label">
|
||||
<span class="label-text-alt text-warning">Carregando perfis disponíveis...</span>
|
||||
<span class="label-text-alt text-warning"
|
||||
>Carregando perfis disponíveis...</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -446,7 +447,9 @@
|
||||
<div class="flex-1">
|
||||
<h3 class="font-bold">Senha Gerada:</h3>
|
||||
<div class="flex items-center gap-2 mt-2">
|
||||
<code class="bg-base-300 px-3 py-2 rounded text-lg font-mono select-all">
|
||||
<code
|
||||
class="bg-base-300 px-3 py-2 rounded text-lg font-mono select-all"
|
||||
>
|
||||
{senhaGerada}
|
||||
</code>
|
||||
<button
|
||||
@@ -473,8 +476,8 @@
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-sm mt-2">
|
||||
⚠️ <strong>IMPORTANTE:</strong> Anote esta senha! Você precisará repassá-la
|
||||
manualmente ao usuário até que o SMTP seja configurado.
|
||||
⚠️ <strong>IMPORTANTE:</strong> Anote esta senha! Você precisará
|
||||
repassá-la manualmente ao usuário até que o SMTP seja configurado.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -500,18 +503,27 @@
|
||||
<h3 class="font-bold">Informações Importantes</h3>
|
||||
<ul class="text-sm list-disc list-inside mt-2 space-y-1">
|
||||
<li>O usuário deverá alterar a senha no primeiro acesso</li>
|
||||
<li>As credenciais devem ser repassadas manualmente (por enquanto)</li>
|
||||
<li>
|
||||
Configure o SMTP em <a href="/ti/configuracoes-email" class="link"
|
||||
>Configurações de Email</a
|
||||
As credenciais devem ser repassadas manualmente (por enquanto)
|
||||
</li>
|
||||
<li>
|
||||
Configure o SMTP em <a
|
||||
href="/ti/configuracoes-email"
|
||||
class="link">Configurações de Email</a
|
||||
> para envio automático
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-actions justify-end mt-8 pt-6 border-t border-base-300">
|
||||
<a href="/ti/usuarios" class="btn btn-ghost gap-2" class:btn-disabled={processando}>
|
||||
<div
|
||||
class="card-actions justify-end mt-8 pt-6 border-t border-base-300"
|
||||
>
|
||||
<a
|
||||
href="/ti/usuarios"
|
||||
class="btn btn-ghost gap-2"
|
||||
class:btn-disabled={processando}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
@@ -528,7 +540,11 @@
|
||||
</svg>
|
||||
Cancelar
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary gap-2" disabled={processando}>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary gap-2"
|
||||
disabled={processando}
|
||||
>
|
||||
{#if processando}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
Criando Usuário...
|
||||
@@ -556,4 +572,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
|
||||
|
||||
@@ -294,12 +294,19 @@ export const login = mutation({
|
||||
timestamp: agora,
|
||||
});
|
||||
|
||||
// Obter matrícula do funcionário se houver
|
||||
let matricula: string | undefined = undefined;
|
||||
if (usuario.funcionarioId) {
|
||||
const funcionario = await ctx.db.get(usuario.funcionarioId);
|
||||
matricula = funcionario?.matricula;
|
||||
}
|
||||
|
||||
return {
|
||||
sucesso: true as const,
|
||||
token,
|
||||
usuario: {
|
||||
_id: usuario._id,
|
||||
matricula: usuario.matricula,
|
||||
matricula: matricula || "",
|
||||
nome: usuario.nome,
|
||||
email: usuario.email,
|
||||
funcionarioId: usuario.funcionarioId,
|
||||
@@ -568,12 +575,19 @@ export const loginComIP = internalMutation({
|
||||
timestamp: agora,
|
||||
});
|
||||
|
||||
// Obter matrícula do funcionário se houver
|
||||
let matricula: string | undefined = undefined;
|
||||
if (usuario.funcionarioId) {
|
||||
const funcionario = await ctx.db.get(usuario.funcionarioId);
|
||||
matricula = funcionario?.matricula;
|
||||
}
|
||||
|
||||
return {
|
||||
sucesso: true as const,
|
||||
token,
|
||||
usuario: {
|
||||
_id: usuario._id,
|
||||
matricula: usuario.matricula,
|
||||
matricula: matricula || "",
|
||||
nome: usuario.nome,
|
||||
email: usuario.email,
|
||||
funcionarioId: usuario.funcionarioId,
|
||||
@@ -688,11 +702,18 @@ export const verificarSessao = query({
|
||||
return { valido: false as const, motivo: "Role não encontrada" };
|
||||
}
|
||||
|
||||
// Obter matrícula do funcionário se houver
|
||||
let matricula: string | undefined = undefined;
|
||||
if (usuario.funcionarioId) {
|
||||
const funcionario = await ctx.db.get(usuario.funcionarioId);
|
||||
matricula = funcionario?.matricula;
|
||||
}
|
||||
|
||||
return {
|
||||
valido: true as const,
|
||||
usuario: {
|
||||
_id: usuario._id,
|
||||
matricula: usuario.matricula,
|
||||
matricula: matricula || "",
|
||||
nome: usuario.nome,
|
||||
email: usuario.email,
|
||||
funcionarioId: usuario.funcionarioId,
|
||||
|
||||
@@ -94,7 +94,9 @@ export const criarConversa = mutation({
|
||||
conversaId,
|
||||
remetenteId: usuarioAtual._id,
|
||||
titulo: "Adicionado a grupo",
|
||||
descricao: `Você foi adicionado ao grupo "${args.nome || "Sem nome"}" por ${usuarioAtual.nome}`,
|
||||
descricao: `Você foi adicionado ao grupo "${
|
||||
args.nome || "Sem nome"
|
||||
}" por ${usuarioAtual.nome}`,
|
||||
lida: false,
|
||||
criadaEm: Date.now(),
|
||||
});
|
||||
@@ -226,7 +228,8 @@ export const enviarMensagem = mutation({
|
||||
for (const participanteId of conversa.participantes) {
|
||||
// ✅ MODIFICADO: Permite notificação para si mesmo se flag estiver ativa
|
||||
const ehOMesmoUsuario = participanteId === usuarioAtual._id;
|
||||
const deveCriarNotificacao = !ehOMesmoUsuario || args.permitirNotificacaoParaSiMesmo;
|
||||
const deveCriarNotificacao =
|
||||
!ehOMesmoUsuario || args.permitirNotificacaoParaSiMesmo;
|
||||
|
||||
if (deveCriarNotificacao) {
|
||||
const tipoNotificacao = args.mencoes?.includes(participanteId)
|
||||
@@ -318,7 +321,10 @@ export const cancelarMensagemAgendada = mutation({
|
||||
}
|
||||
|
||||
if (mensagem.remetenteId !== usuarioAtual._id) {
|
||||
return { sucesso: false, erro: "Você só pode cancelar suas próprias mensagens" };
|
||||
return {
|
||||
sucesso: false,
|
||||
erro: "Você só pode cancelar suas próprias mensagens",
|
||||
};
|
||||
}
|
||||
|
||||
if (!mensagem.agendadaPara) {
|
||||
@@ -611,7 +617,9 @@ export const listarConversas = query({
|
||||
// Para conversas individuais, pegar o outro usuário
|
||||
let outroUsuario = null;
|
||||
if (conversa.tipo === "individual") {
|
||||
const outroUsuarioRaw = participantes.find((p) => p?._id !== usuarioAtual._id);
|
||||
const outroUsuarioRaw = participantes.find(
|
||||
(p) => p?._id !== usuarioAtual._id
|
||||
);
|
||||
if (outroUsuarioRaw) {
|
||||
// 🔄 BUSCAR DADOS ATUALIZADOS DO USUÁRIO (não usar snapshot)
|
||||
const usuarioAtualizado = await ctx.db.get(outroUsuarioRaw._id);
|
||||
@@ -620,7 +628,9 @@ export const listarConversas = query({
|
||||
// Adicionar URL da foto de perfil
|
||||
let fotoPerfilUrl = null;
|
||||
if (usuarioAtualizado.fotoPerfil) {
|
||||
fotoPerfilUrl = await ctx.storage.getUrl(usuarioAtualizado.fotoPerfil);
|
||||
fotoPerfilUrl = await ctx.storage.getUrl(
|
||||
usuarioAtualizado.fotoPerfil
|
||||
);
|
||||
}
|
||||
outroUsuario = {
|
||||
...usuarioAtualizado,
|
||||
@@ -756,7 +766,16 @@ export const obterMensagensAgendadas = query({
|
||||
*/
|
||||
export const listarAgendamentosChat = query({
|
||||
args: {},
|
||||
handler: async (ctx): Promise<Array<Doc<"mensagens"> & { conversaInfo: Doc<"conversas"> | null; destinatarioInfo: Doc<"usuarios"> | null }>> => {
|
||||
handler: async (
|
||||
ctx
|
||||
): Promise<
|
||||
Array<
|
||||
Doc<"mensagens"> & {
|
||||
conversaInfo: Doc<"conversas"> | null;
|
||||
destinatarioInfo: Doc<"usuarios"> | null;
|
||||
}
|
||||
>
|
||||
> => {
|
||||
const usuarioAtual = await getUsuarioAutenticado(ctx);
|
||||
if (!usuarioAtual) {
|
||||
return [];
|
||||
@@ -912,20 +931,31 @@ export const listarTodosUsuarios = query({
|
||||
.withIndex("by_ativo", (q) => q.eq("ativo", true))
|
||||
.collect();
|
||||
|
||||
// Excluir o usuário atual
|
||||
return usuarios
|
||||
.filter((u) => u._id !== usuarioAtual._id)
|
||||
.map((u) => ({
|
||||
_id: u._id,
|
||||
nome: u.nome,
|
||||
email: u.email,
|
||||
matricula: u.matricula,
|
||||
avatar: u.avatar,
|
||||
fotoPerfil: u.fotoPerfil,
|
||||
statusPresenca: u.statusPresenca,
|
||||
statusMensagem: u.statusMensagem,
|
||||
setor: u.setor,
|
||||
}));
|
||||
// Excluir o usuário atual e buscar matrículas
|
||||
const usuariosComMatricula = await Promise.all(
|
||||
usuarios
|
||||
.filter((u) => u._id !== usuarioAtual._id)
|
||||
.map(async (u) => {
|
||||
let matricula: string | undefined = undefined;
|
||||
if (u.funcionarioId) {
|
||||
const funcionario = await ctx.db.get(u.funcionarioId);
|
||||
matricula = funcionario?.matricula;
|
||||
}
|
||||
return {
|
||||
_id: u._id,
|
||||
nome: u.nome,
|
||||
email: u.email,
|
||||
matricula,
|
||||
avatar: u.avatar,
|
||||
fotoPerfil: u.fotoPerfil,
|
||||
statusPresenca: u.statusPresenca,
|
||||
statusMensagem: u.statusMensagem,
|
||||
setor: u.setor,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return usuariosComMatricula;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -88,9 +88,14 @@ export const listar = query({
|
||||
if (log.usuarioId) {
|
||||
const user = await ctx.db.get(log.usuarioId);
|
||||
if (user) {
|
||||
let matricula: string | undefined = undefined;
|
||||
if (user.funcionarioId) {
|
||||
const funcionario = await ctx.db.get(user.funcionarioId);
|
||||
matricula = funcionario?.matricula;
|
||||
}
|
||||
usuario = {
|
||||
_id: user._id,
|
||||
matricula: user.matricula,
|
||||
matricula: matricula || "",
|
||||
nome: user.nome,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -78,10 +78,15 @@ export const listarAtividades = query({
|
||||
const atividadesComUsuarios = await Promise.all(
|
||||
atividades.map(async (atividade) => {
|
||||
const usuario = await ctx.db.get(atividade.usuarioId);
|
||||
let matricula = "N/A";
|
||||
if (usuario?.funcionarioId) {
|
||||
const funcionario = await ctx.db.get(usuario.funcionarioId);
|
||||
matricula = funcionario?.matricula || "N/A";
|
||||
}
|
||||
return {
|
||||
...atividade,
|
||||
usuarioNome: usuario?.nome || "Usuário Desconhecido",
|
||||
usuarioMatricula: usuario?.matricula || "N/A",
|
||||
usuarioMatricula: matricula,
|
||||
};
|
||||
})
|
||||
);
|
||||
@@ -157,10 +162,15 @@ export const obterHistoricoRecurso = query({
|
||||
const atividadesComUsuarios = await Promise.all(
|
||||
atividades.map(async (atividade) => {
|
||||
const usuario = await ctx.db.get(atividade.usuarioId);
|
||||
let matricula = "N/A";
|
||||
if (usuario?.funcionarioId) {
|
||||
const funcionario = await ctx.db.get(usuario.funcionarioId);
|
||||
matricula = funcionario?.matricula || "N/A";
|
||||
}
|
||||
return {
|
||||
...atividade,
|
||||
usuarioNome: usuario?.nome || "Usuário Desconhecido",
|
||||
usuarioMatricula: usuario?.matricula || "N/A",
|
||||
usuarioMatricula: matricula,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
@@ -30,18 +30,19 @@ export default defineSchema({
|
||||
simboloId: v.id("simbolos"),
|
||||
simboloTipo: simboloTipo,
|
||||
gestorId: v.optional(v.id("usuarios")),
|
||||
statusFerias: v.optional(v.union(
|
||||
v.literal("ativo"),
|
||||
v.literal("em_ferias")
|
||||
)),
|
||||
statusFerias: v.optional(
|
||||
v.union(v.literal("ativo"), v.literal("em_ferias"))
|
||||
),
|
||||
|
||||
// Regime de trabalho (para cálculo correto de férias)
|
||||
regimeTrabalho: v.optional(v.union(
|
||||
v.literal("clt"), // CLT - Consolidação das Leis do Trabalho
|
||||
v.literal("estatutario_pe"), // Servidor Público Estadual de Pernambuco
|
||||
v.literal("estatutario_federal"), // Servidor Público Federal
|
||||
v.literal("estatutario_municipal") // Servidor Público Municipal
|
||||
)),
|
||||
regimeTrabalho: v.optional(
|
||||
v.union(
|
||||
v.literal("clt"), // CLT - Consolidação das Leis do Trabalho
|
||||
v.literal("estatutario_pe"), // Servidor Público Estadual de Pernambuco
|
||||
v.literal("estatutario_federal"), // Servidor Público Federal
|
||||
v.literal("estatutario_municipal") // Servidor Público Municipal
|
||||
)
|
||||
),
|
||||
|
||||
// Dados Pessoais Adicionais (opcionais)
|
||||
nomePai: v.optional(v.string()),
|
||||
@@ -191,10 +192,7 @@ export default defineSchema({
|
||||
|
||||
licencas: defineTable({
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
tipo: v.union(
|
||||
v.literal("maternidade"),
|
||||
v.literal("paternidade")
|
||||
),
|
||||
tipo: v.union(v.literal("maternidade"), v.literal("paternidade")),
|
||||
dataInicio: v.string(),
|
||||
dataFim: v.string(),
|
||||
documentoId: v.optional(v.id("_storage")),
|
||||
@@ -237,11 +235,15 @@ export default defineSchema({
|
||||
data: v.number(),
|
||||
usuarioId: v.id("usuarios"),
|
||||
acao: v.string(),
|
||||
periodosAnteriores: v.optional(v.array(v.object({
|
||||
dataInicio: v.string(),
|
||||
dataFim: v.string(),
|
||||
diasCorridos: v.number(),
|
||||
}))),
|
||||
periodosAnteriores: v.optional(
|
||||
v.array(
|
||||
v.object({
|
||||
dataInicio: v.string(),
|
||||
dataFim: v.string(),
|
||||
diasCorridos: v.number(),
|
||||
})
|
||||
)
|
||||
),
|
||||
})
|
||||
)
|
||||
),
|
||||
@@ -343,7 +345,6 @@ export default defineSchema({
|
||||
|
||||
// Sistema de Autenticação e Controle de Acesso
|
||||
usuarios: defineTable({
|
||||
matricula: v.string(),
|
||||
senhaHash: v.string(), // Senha criptografada com bcrypt
|
||||
nome: v.string(),
|
||||
email: v.string(),
|
||||
@@ -380,7 +381,6 @@ export default defineSchema({
|
||||
notificacoesAtivadas: v.optional(v.boolean()),
|
||||
somNotificacao: v.optional(v.boolean()),
|
||||
})
|
||||
.index("by_matricula", ["matricula"])
|
||||
.index("by_email", ["email"])
|
||||
.index("by_role", ["roleId"])
|
||||
.index("by_ativo", ["ativo"])
|
||||
@@ -663,8 +663,7 @@ export default defineSchema({
|
||||
mensagensPorMinuto: v.optional(v.number()),
|
||||
tempoRespostaMedio: v.optional(v.number()),
|
||||
errosCount: v.optional(v.number()),
|
||||
})
|
||||
.index("by_timestamp", ["timestamp"]),
|
||||
}).index("by_timestamp", ["timestamp"]),
|
||||
|
||||
alertConfigurations: defineTable({
|
||||
metricName: v.string(),
|
||||
@@ -681,8 +680,7 @@ export default defineSchema({
|
||||
notifyByChat: v.boolean(),
|
||||
createdBy: v.id("usuarios"),
|
||||
lastModified: v.number(),
|
||||
})
|
||||
.index("by_enabled", ["enabled"]),
|
||||
}).index("by_enabled", ["enabled"]),
|
||||
|
||||
alertHistory: defineTable({
|
||||
configId: v.id("alertConfigurations"),
|
||||
|
||||
@@ -316,7 +316,6 @@ export const seedDatabase = internalMutation({
|
||||
|
||||
const senhaInicial = await hashPassword("Mudar@123");
|
||||
await ctx.db.insert("usuarios", {
|
||||
matricula: funcionario.matricula,
|
||||
senhaHash: senhaInicial,
|
||||
nome: funcionario.nome,
|
||||
email: funcionario.email,
|
||||
|
||||
@@ -4,6 +4,21 @@ import { hashPassword, generateToken } from "./auth/utils";
|
||||
import { registrarAtividade } from "./logsAtividades";
|
||||
import { Id, Doc } from "./_generated/dataModel";
|
||||
import { api } from "./_generated/api";
|
||||
import type { QueryCtx } from "./_generated/server";
|
||||
|
||||
/**
|
||||
* Helper para obter a matrícula do usuário (do funcionário se houver)
|
||||
*/
|
||||
async function obterMatriculaUsuario(
|
||||
ctx: QueryCtx,
|
||||
usuario: Doc<"usuarios">
|
||||
): Promise<string | undefined> {
|
||||
if (usuario.funcionarioId) {
|
||||
const funcionario = await ctx.db.get(usuario.funcionarioId);
|
||||
return funcionario?.matricula;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Associar funcionário a um usuário
|
||||
@@ -30,8 +45,11 @@ export const associarFuncionario = mutation({
|
||||
.first();
|
||||
|
||||
if (usuarioExistente && usuarioExistente._id !== args.usuarioId) {
|
||||
const matricula = await obterMatriculaUsuario(ctx, usuarioExistente);
|
||||
throw new Error(
|
||||
`Este funcionário já está associado ao usuário: ${usuarioExistente.nome} (${usuarioExistente.matricula})`
|
||||
`Este funcionário já está associado ao usuário: ${
|
||||
usuarioExistente.nome
|
||||
}${matricula ? ` (${matricula})` : ""}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,7 +84,6 @@ export const desassociarFuncionario = mutation({
|
||||
*/
|
||||
export const criar = mutation({
|
||||
args: {
|
||||
matricula: v.string(),
|
||||
nome: v.string(),
|
||||
email: v.string(),
|
||||
roleId: v.id("roles"),
|
||||
@@ -78,16 +95,6 @@ export const criar = mutation({
|
||||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
// Verificar se matrícula já existe
|
||||
const existente = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
|
||||
.first();
|
||||
|
||||
if (existente) {
|
||||
return { sucesso: false as const, erro: "Matrícula já cadastrada" };
|
||||
}
|
||||
|
||||
// Verificar se email já existe
|
||||
const emailExistente = await ctx.db
|
||||
.query("usuarios")
|
||||
@@ -103,7 +110,6 @@ export const criar = mutation({
|
||||
|
||||
// Criar usuário
|
||||
const usuarioId = await ctx.db.insert("usuarios", {
|
||||
matricula: args.matricula,
|
||||
senhaHash,
|
||||
nome: args.nome,
|
||||
email: args.email,
|
||||
@@ -194,9 +200,17 @@ export const listar = query({
|
||||
handler: async (ctx, args) => {
|
||||
let usuarios = await ctx.db.query("usuarios").collect();
|
||||
|
||||
// Filtrar por matrícula
|
||||
// Filtrar por matrícula (buscar no funcionário)
|
||||
if (args.matricula) {
|
||||
usuarios = usuarios.filter((u) => u.matricula.includes(args.matricula!));
|
||||
const usuariosComMatricula = await Promise.all(
|
||||
usuarios.map(async (u) => {
|
||||
const matricula = await obterMatriculaUsuario(ctx, u);
|
||||
return { usuario: u, matricula };
|
||||
})
|
||||
);
|
||||
usuarios = usuariosComMatricula
|
||||
.filter(({ matricula }) => matricula?.includes(args.matricula!))
|
||||
.map(({ usuario }) => usuario);
|
||||
}
|
||||
|
||||
// Filtrar por ativo
|
||||
@@ -206,7 +220,11 @@ export const listar = query({
|
||||
|
||||
// Buscar roles e funcionários
|
||||
const resultado = [];
|
||||
const usuariosSemRole: Array<{ nome: string; matricula: string; roleId: Id<"roles"> }> = [];
|
||||
const usuariosSemRole: Array<{
|
||||
nome: string;
|
||||
matricula: string;
|
||||
roleId: Id<"roles">;
|
||||
}> = [];
|
||||
|
||||
for (const usuario of usuarios) {
|
||||
try {
|
||||
@@ -214,9 +232,10 @@ export const listar = query({
|
||||
|
||||
// Se a role não existe, criar uma role de erro mas ainda incluir o usuário
|
||||
if (!role) {
|
||||
const matricula = await obterMatriculaUsuario(ctx, usuario);
|
||||
usuariosSemRole.push({
|
||||
nome: usuario.nome,
|
||||
matricula: usuario.matricula,
|
||||
matricula: matricula || "N/A",
|
||||
roleId: usuario.roleId,
|
||||
});
|
||||
|
||||
@@ -240,14 +259,19 @@ export const listar = query({
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Erro ao buscar funcionário ${usuario.funcionarioId} para usuário ${usuario._id}:`, error);
|
||||
console.error(
|
||||
`Erro ao buscar funcionário ${usuario.funcionarioId} para usuário ${usuario._id}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const matriculaUsuario = await obterMatriculaUsuario(ctx, usuario);
|
||||
|
||||
// Criar role de erro (sem _creationTime pois a role não existe)
|
||||
resultado.push({
|
||||
_id: usuario._id,
|
||||
matricula: usuario.matricula,
|
||||
matricula: matriculaUsuario,
|
||||
nome: usuario.nome,
|
||||
email: usuario.email,
|
||||
ativo: usuario.ativo,
|
||||
@@ -294,7 +318,10 @@ export const listar = query({
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Erro ao buscar funcionário ${usuario.funcionarioId} para usuário ${usuario._id}:`, error);
|
||||
console.error(
|
||||
`Erro ao buscar funcionário ${usuario.funcionarioId} para usuário ${usuario._id}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,14 +332,18 @@ export const listar = query({
|
||||
nome: role.nome,
|
||||
nivel: role.nivel,
|
||||
...(role.criadoPor !== undefined && { criadoPor: role.criadoPor }),
|
||||
...(role.customizado !== undefined && { customizado: role.customizado }),
|
||||
...(role.customizado !== undefined && {
|
||||
customizado: role.customizado,
|
||||
}),
|
||||
...(role.editavel !== undefined && { editavel: role.editavel }),
|
||||
...(role.setor !== undefined && { setor: role.setor }),
|
||||
};
|
||||
|
||||
const matriculaUsuario = await obterMatriculaUsuario(ctx, usuario);
|
||||
|
||||
resultado.push({
|
||||
_id: usuario._id,
|
||||
matricula: usuario.matricula,
|
||||
matricula: matriculaUsuario,
|
||||
nome: usuario.nome,
|
||||
email: usuario.email,
|
||||
ativo: usuario.ativo,
|
||||
@@ -334,7 +365,12 @@ export const listar = query({
|
||||
if (usuariosSemRole.length > 0) {
|
||||
console.warn(
|
||||
`⚠️ Encontrados ${usuariosSemRole.length} usuário(s) com perfil ausente:`,
|
||||
usuariosSemRole.map((u) => `${u.nome} (${u.matricula}) - RoleID: ${u.roleId}`)
|
||||
usuariosSemRole.map(
|
||||
(u) =>
|
||||
`${u.nome}${
|
||||
u.matricula !== "N/A" ? ` (${u.matricula})` : ""
|
||||
} - RoleID: ${u.roleId}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -559,7 +595,9 @@ export const atualizarPerfil = mutation({
|
||||
}
|
||||
|
||||
// Atualizar apenas os campos fornecidos
|
||||
const updates: Partial<Doc<"usuarios">> & { atualizadoEm: number } = { atualizadoEm: Date.now() };
|
||||
const updates: Partial<Doc<"usuarios">> & { atualizadoEm: number } = {
|
||||
atualizadoEm: Date.now(),
|
||||
};
|
||||
|
||||
if (args.avatar !== undefined) updates.avatar = args.avatar;
|
||||
if (args.fotoPerfil !== undefined) updates.fotoPerfil = args.fotoPerfil;
|
||||
@@ -591,7 +629,7 @@ export const obterPerfil = query({
|
||||
_id: v.id("usuarios"),
|
||||
nome: v.string(),
|
||||
email: v.string(),
|
||||
matricula: v.string(),
|
||||
matricula: v.optional(v.string()),
|
||||
funcionarioId: v.optional(v.id("funcionarios")),
|
||||
avatar: v.optional(v.string()),
|
||||
fotoPerfil: v.optional(v.id("_storage")),
|
||||
@@ -675,11 +713,13 @@ export const obterPerfil = query({
|
||||
fotoPerfilUrl = await ctx.storage.getUrl(usuarioAtual.fotoPerfil);
|
||||
}
|
||||
|
||||
const matricula = await obterMatriculaUsuario(ctx, usuarioAtual);
|
||||
|
||||
return {
|
||||
_id: usuarioAtual._id,
|
||||
nome: usuarioAtual.nome,
|
||||
email: usuarioAtual.email,
|
||||
matricula: usuarioAtual.matricula,
|
||||
matricula: matricula || undefined,
|
||||
funcionarioId: usuarioAtual.funcionarioId,
|
||||
avatar: usuarioAtual.avatar,
|
||||
fotoPerfil: usuarioAtual.fotoPerfil,
|
||||
@@ -735,11 +775,13 @@ export const listarParaChat = query({
|
||||
fotoPerfilUrl = await ctx.storage.getUrl(usuario.fotoPerfil);
|
||||
}
|
||||
|
||||
const matricula = await obterMatriculaUsuario(ctx, usuario);
|
||||
|
||||
return {
|
||||
_id: usuario._id,
|
||||
nome: usuario.nome,
|
||||
email: usuario.email,
|
||||
matricula: usuario.matricula || undefined,
|
||||
matricula: matricula || undefined,
|
||||
avatar: usuario.avatar,
|
||||
fotoPerfil: usuario.fotoPerfil,
|
||||
fotoPerfilUrl,
|
||||
@@ -1035,7 +1077,6 @@ export const editarUsuario = mutation({
|
||||
*/
|
||||
export const criarAdminMaster = mutation({
|
||||
args: {
|
||||
matricula: v.string(),
|
||||
nome: v.string(),
|
||||
email: v.string(),
|
||||
senha: v.optional(v.string()),
|
||||
@@ -1074,32 +1115,9 @@ export const criarAdminMaster = mutation({
|
||||
};
|
||||
}
|
||||
|
||||
// Se já existir usuário por matrícula, promove/atualiza
|
||||
const existentePorMatricula = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
|
||||
.first();
|
||||
|
||||
const senhaTemporaria = args.senha || gerarSenhaTemporaria();
|
||||
const senhaHash = await hashPassword(senhaTemporaria);
|
||||
|
||||
if (existentePorMatricula) {
|
||||
await ctx.db.patch(existentePorMatricula._id, {
|
||||
nome: args.nome,
|
||||
email: args.email,
|
||||
senhaHash,
|
||||
roleId: roleTIMaster._id,
|
||||
ativo: true,
|
||||
primeiroAcesso: true,
|
||||
atualizadoEm: Date.now(),
|
||||
});
|
||||
return {
|
||||
sucesso: true as const,
|
||||
usuarioId: existentePorMatricula._id,
|
||||
senhaTemporaria,
|
||||
};
|
||||
}
|
||||
|
||||
// Verificar se email já existe
|
||||
const existentePorEmail = await ctx.db
|
||||
.query("usuarios")
|
||||
@@ -1108,7 +1126,6 @@ export const criarAdminMaster = mutation({
|
||||
if (existentePorEmail) {
|
||||
// Promove usuário existente por email
|
||||
await ctx.db.patch(existentePorEmail._id, {
|
||||
matricula: args.matricula,
|
||||
nome: args.nome,
|
||||
senhaHash,
|
||||
roleId: roleTIMaster._id,
|
||||
@@ -1125,7 +1142,6 @@ export const criarAdminMaster = mutation({
|
||||
|
||||
// Criar novo usuário TI Master
|
||||
const usuarioId = await ctx.db.insert("usuarios", {
|
||||
matricula: args.matricula,
|
||||
senhaHash,
|
||||
nome: args.nome,
|
||||
email: args.email,
|
||||
@@ -1194,7 +1210,6 @@ export const excluirUsuarioLogico = mutation({
|
||||
*/
|
||||
export const criarUsuarioCompleto = mutation({
|
||||
args: {
|
||||
matricula: v.string(),
|
||||
nome: v.string(),
|
||||
email: v.string(),
|
||||
roleId: v.id("roles"),
|
||||
@@ -1212,16 +1227,6 @@ export const criarUsuarioCompleto = mutation({
|
||||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
// Verificar se matrícula já existe
|
||||
const existente = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
|
||||
.first();
|
||||
|
||||
if (existente) {
|
||||
return { sucesso: false as const, erro: "Matrícula já cadastrada" };
|
||||
}
|
||||
|
||||
// Verificar se email já existe
|
||||
const emailExistente = await ctx.db
|
||||
.query("usuarios")
|
||||
@@ -1238,7 +1243,6 @@ export const criarUsuarioCompleto = mutation({
|
||||
|
||||
// Criar usuário
|
||||
const usuarioId = await ctx.db.insert("usuarios", {
|
||||
matricula: args.matricula,
|
||||
senhaHash,
|
||||
nome: args.nome,
|
||||
email: args.email,
|
||||
@@ -1256,7 +1260,7 @@ export const criarUsuarioCompleto = mutation({
|
||||
args.criadoPorId,
|
||||
"criar",
|
||||
"usuarios",
|
||||
JSON.stringify({ usuarioId, matricula: args.matricula, nome: args.nome }),
|
||||
JSON.stringify({ usuarioId, nome: args.nome }),
|
||||
usuarioId
|
||||
);
|
||||
|
||||
@@ -1272,7 +1276,6 @@ export const criarUsuarioCompleto = mutation({
|
||||
*/
|
||||
export const criarAdminPadrao = mutation({
|
||||
args: {
|
||||
matricula: v.optional(v.string()),
|
||||
nome: v.optional(v.string()),
|
||||
email: v.optional(v.string()),
|
||||
senha: v.optional(v.string()),
|
||||
@@ -1282,7 +1285,6 @@ export const criarAdminPadrao = mutation({
|
||||
usuarioId: v.optional(v.id("usuarios")),
|
||||
}),
|
||||
handler: async (ctx, args) => {
|
||||
const matricula = args.matricula ?? "0000";
|
||||
const nome = args.nome ?? "Administrador Geral";
|
||||
const email = args.email ?? "admin@sgse.pe.gov.br";
|
||||
const senha = args.senha ?? "Admin@123";
|
||||
@@ -1306,12 +1308,7 @@ export const criarAdminPadrao = mutation({
|
||||
|
||||
if (!roleAdmin) return { sucesso: false };
|
||||
|
||||
// Verificar se já existe por matrícula ou email
|
||||
const existentePorMatricula = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_matricula", (q) => q.eq("matricula", matricula))
|
||||
.first();
|
||||
|
||||
// Verificar se já existe por email
|
||||
const existentePorEmail = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_email", (q) => q.eq("email", email))
|
||||
@@ -1319,10 +1316,8 @@ export const criarAdminPadrao = mutation({
|
||||
|
||||
const senhaHash = await hashPassword(senha);
|
||||
|
||||
if (existentePorMatricula || existentePorEmail) {
|
||||
const alvo = existentePorMatricula ?? existentePorEmail!;
|
||||
await ctx.db.patch(alvo._id, {
|
||||
matricula,
|
||||
if (existentePorEmail) {
|
||||
await ctx.db.patch(existentePorEmail._id, {
|
||||
nome,
|
||||
email,
|
||||
senhaHash,
|
||||
@@ -1331,11 +1326,10 @@ export const criarAdminPadrao = mutation({
|
||||
primeiroAcesso: false,
|
||||
atualizadoEm: Date.now(),
|
||||
});
|
||||
return { sucesso: true, usuarioId: alvo._id };
|
||||
return { sucesso: true, usuarioId: existentePorEmail._id };
|
||||
}
|
||||
|
||||
const usuarioId = await ctx.db.insert("usuarios", {
|
||||
matricula,
|
||||
senhaHash,
|
||||
nome,
|
||||
email,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { v } from "convex/values";
|
||||
import { Id, Doc } from "./_generated/dataModel";
|
||||
|
||||
/**
|
||||
* Verificar duplicatas de matrícula
|
||||
* Verificar duplicatas de matrícula (agora busca do funcionário associado)
|
||||
*/
|
||||
export const verificarDuplicatas = query({
|
||||
args: {},
|
||||
@@ -23,18 +23,27 @@ export const verificarDuplicatas = query({
|
||||
handler: async (ctx) => {
|
||||
const usuarios = await ctx.db.query("usuarios").collect();
|
||||
|
||||
// Agrupar por matrícula
|
||||
const gruposPorMatricula = usuarios.reduce((acc, usuario) => {
|
||||
if (!acc[usuario.matricula]) {
|
||||
acc[usuario.matricula] = [];
|
||||
// Agrupar por matrícula do funcionário associado
|
||||
const gruposPorMatricula: Record<string, Array<{ _id: Id<"usuarios">; nome: string; email: string }>> = {};
|
||||
|
||||
for (const usuario of usuarios) {
|
||||
let matricula: string | undefined = undefined;
|
||||
if (usuario.funcionarioId) {
|
||||
const funcionario = await ctx.db.get(usuario.funcionarioId);
|
||||
matricula = funcionario?.matricula;
|
||||
}
|
||||
acc[usuario.matricula].push({
|
||||
_id: usuario._id,
|
||||
nome: usuario.nome,
|
||||
email: usuario.email || "",
|
||||
});
|
||||
return acc;
|
||||
}, {} as Record<string, Array<{ _id: Id<"usuarios">; nome: string; email: string }>>);
|
||||
|
||||
if (matricula) {
|
||||
if (!gruposPorMatricula[matricula]) {
|
||||
gruposPorMatricula[matricula] = [];
|
||||
}
|
||||
gruposPorMatricula[matricula].push({
|
||||
_id: usuario._id,
|
||||
nome: usuario.nome,
|
||||
email: usuario.email || "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Filtrar apenas duplicatas
|
||||
const duplicatas = Object.entries(gruposPorMatricula)
|
||||
@@ -50,7 +59,7 @@ export const verificarDuplicatas = query({
|
||||
});
|
||||
|
||||
/**
|
||||
* Remover duplicatas mantendo apenas o mais recente
|
||||
* Remover duplicatas mantendo apenas o mais recente (agora busca do funcionário associado)
|
||||
*/
|
||||
export const removerDuplicatas = internalMutation({
|
||||
args: {},
|
||||
@@ -61,14 +70,23 @@ export const removerDuplicatas = internalMutation({
|
||||
handler: async (ctx) => {
|
||||
const usuarios = await ctx.db.query("usuarios").collect();
|
||||
|
||||
// Agrupar por matrícula
|
||||
const gruposPorMatricula = usuarios.reduce((acc, usuario) => {
|
||||
if (!acc[usuario.matricula]) {
|
||||
acc[usuario.matricula] = [];
|
||||
// Agrupar por matrícula do funcionário associado
|
||||
const gruposPorMatricula: Record<string, Doc<"usuarios">[]> = {};
|
||||
|
||||
for (const usuario of usuarios) {
|
||||
let matricula: string | undefined = undefined;
|
||||
if (usuario.funcionarioId) {
|
||||
const funcionario = await ctx.db.get(usuario.funcionarioId);
|
||||
matricula = funcionario?.matricula;
|
||||
}
|
||||
acc[usuario.matricula].push(usuario);
|
||||
return acc;
|
||||
}, {} as Record<string, Doc<"usuarios">[]>);
|
||||
|
||||
if (matricula) {
|
||||
if (!gruposPorMatricula[matricula]) {
|
||||
gruposPorMatricula[matricula] = [];
|
||||
}
|
||||
gruposPorMatricula[matricula].push(usuario);
|
||||
}
|
||||
}
|
||||
|
||||
let removidos = 0;
|
||||
const matriculasDuplicadas: string[] = [];
|
||||
|
||||
Reference in New Issue
Block a user