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_style = space
|
||||||
indent_size = 4
|
indent_size = 2
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
nodejs 25.0.0
|
nodejs 22.21.1
|
||||||
|
|||||||
@@ -2,25 +2,38 @@
|
|||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
icon?: string;
|
icon?: any; // Componente do lucide-svelte
|
||||||
trend?: {
|
trend?: {
|
||||||
value: number;
|
value: number;
|
||||||
isPositive: boolean;
|
isPositive: boolean;
|
||||||
};
|
};
|
||||||
description?: string;
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="stats shadow bg-base-100">
|
<div class="stats shadow bg-base-100">
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-figure text-{color}">
|
<div class="stat-figure text-{color}">
|
||||||
{#if icon}
|
{#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">
|
{@const IconComponent = Icon}
|
||||||
{@html icon}
|
<IconComponent class="inline-block w-8 h-8 stroke-current" />
|
||||||
</svg>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-title">{title}</div>
|
<div class="stat-title">{title}</div>
|
||||||
@@ -30,10 +43,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{#if trend}
|
{#if trend}
|
||||||
<div class="stat-desc {trend.isPositive ? 'text-success' : 'text-error'}">
|
<div class="stat-desc {trend.isPositive ? 'text-success' : 'text-error'}">
|
||||||
{trend.isPositive ? '↗︎' : '↘︎'} {Math.abs(trend.value)}%
|
{trend.isPositive ? "↗︎" : "↘︎"}
|
||||||
|
{Math.abs(trend.value)}%
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,17 +6,21 @@
|
|||||||
let limite = $state(50);
|
let limite = $state(50);
|
||||||
|
|
||||||
// Queries com $derived para garantir reatividade
|
// Queries com $derived para garantir reatividade
|
||||||
const atividades = $derived(useQuery(api.logsAtividades.listarAtividades, { limite }));
|
const atividades = $derived(
|
||||||
const logins = $derived(useQuery(api.logsLogin.listarTodosLogins, { limite }));
|
useQuery(api.logsAtividades.listarAtividades, { limite }),
|
||||||
|
);
|
||||||
|
const logins = $derived(
|
||||||
|
useQuery(api.logsLogin.listarTodosLogins, { limite }),
|
||||||
|
);
|
||||||
|
|
||||||
function formatarData(timestamp: number) {
|
function formatarData(timestamp: number) {
|
||||||
return new Date(timestamp).toLocaleString('pt-BR', {
|
return new Date(timestamp).toLocaleString("pt-BR", {
|
||||||
day: '2-digit',
|
day: "2-digit",
|
||||||
month: '2-digit',
|
month: "2-digit",
|
||||||
year: 'numeric',
|
year: "numeric",
|
||||||
hour: '2-digit',
|
hour: "2-digit",
|
||||||
minute: '2-digit',
|
minute: "2-digit",
|
||||||
second: '2-digit'
|
second: "2-digit",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +31,7 @@
|
|||||||
excluir: "badge-error",
|
excluir: "badge-error",
|
||||||
bloquear: "badge-error",
|
bloquear: "badge-error",
|
||||||
desbloquear: "badge-success",
|
desbloquear: "badge-success",
|
||||||
resetar_senha: "badge-info"
|
resetar_senha: "badge-info",
|
||||||
};
|
};
|
||||||
return colors[acao] || "badge-neutral";
|
return colors[acao] || "badge-neutral";
|
||||||
}
|
}
|
||||||
@@ -39,7 +43,7 @@
|
|||||||
excluir: "Excluir",
|
excluir: "Excluir",
|
||||||
bloquear: "Bloquear",
|
bloquear: "Bloquear",
|
||||||
desbloquear: "Desbloquear",
|
desbloquear: "Desbloquear",
|
||||||
resetar_senha: "Resetar Senha"
|
resetar_senha: "Resetar Senha",
|
||||||
};
|
};
|
||||||
return labels[acao] || acao;
|
return labels[acao] || acao;
|
||||||
}
|
}
|
||||||
@@ -47,8 +51,12 @@
|
|||||||
// Estatísticas
|
// Estatísticas
|
||||||
const totalAtividades = $derived(atividades?.data?.length || 0);
|
const totalAtividades = $derived(atividades?.data?.length || 0);
|
||||||
const totalLogins = $derived(logins?.data?.length || 0);
|
const totalLogins = $derived(logins?.data?.length || 0);
|
||||||
const loginsSucesso = $derived(logins?.data?.filter(l => l.sucesso).length || 0);
|
const loginsSucesso = $derived(
|
||||||
const loginsFalha = $derived(logins?.data?.filter(l => !l.sucesso).length || 0);
|
logins?.data?.filter((l) => l.sucesso).length || 0,
|
||||||
|
);
|
||||||
|
const loginsFalha = $derived(
|
||||||
|
logins?.data?.filter((l) => !l.sucesso).length || 0,
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main class="container mx-auto px-4 py-6 max-w-7xl">
|
<main class="container mx-auto px-4 py-6 max-w-7xl">
|
||||||
@@ -64,13 +72,26 @@
|
|||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<div class="flex items-center gap-4 mb-2">
|
<div class="flex items-center gap-4 mb-2">
|
||||||
<div class="p-3 bg-blue-500/20 rounded-xl">
|
<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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-3xl font-bold text-primary">Auditoria e Logs</h1>
|
<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>
|
</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="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 bg-base-100 shadow-lg rounded-lg">
|
||||||
<div class="stat-figure text-primary">
|
<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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-title text-xs">Atividades</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 bg-base-100 shadow-lg rounded-lg">
|
||||||
<div class="stat-figure text-success">
|
<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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-title text-xs">Logins Totais</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 bg-base-100 shadow-lg rounded-lg">
|
||||||
<div class="stat-figure text-success">
|
<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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-title text-xs">Logins Bem-sucedidos</div>
|
<div class="stat-title text-xs">Logins Bem-sucedidos</div>
|
||||||
<div class="stat-value text-2xl text-success">{loginsSucesso}</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>
|
||||||
|
|
||||||
<div class="stat bg-base-100 shadow-lg rounded-lg">
|
<div class="stat bg-base-100 shadow-lg rounded-lg">
|
||||||
<div class="stat-figure text-error">
|
<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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-title text-xs">Logins Falhados</div>
|
<div class="stat-title text-xs">Logins Falhados</div>
|
||||||
<div class="stat-value text-2xl text-error">{loginsFalha}</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tabs -->
|
<!-- Tabs -->
|
||||||
<div class="tabs tabs-boxed mb-6 bg-base-100 shadow-lg p-1">
|
<div class="tabs tabs-boxed mb-6 bg-base-100 shadow-lg p-1">
|
||||||
<button
|
<button
|
||||||
class="tab flex items-center gap-2 {abaAtiva === 'atividades' ? 'tab-active' : ''}"
|
class="tab flex items-center gap-2 {abaAtiva === 'atividades'
|
||||||
onclick={() => 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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
<span class="font-medium">Atividades no Sistema</span>
|
<span class="font-medium">Atividades no Sistema</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="tab flex items-center gap-2 {abaAtiva === 'logins' ? 'tab-active' : ''}"
|
class="tab flex items-center gap-2 {abaAtiva === 'logins'
|
||||||
onclick={() => 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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
<span class="font-medium">Histórico de Logins</span>
|
<span class="font-medium">Histórico de Logins</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -149,10 +246,13 @@
|
|||||||
<div class="card-body py-4">
|
<div class="card-body py-4">
|
||||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label py-1">
|
<div class="label py-1">
|
||||||
<span class="label-text font-medium">Quantidade de registros</span>
|
<span class="label-text font-medium">Quantidade de registros</span>
|
||||||
</label>
|
</div>
|
||||||
<select bind:value={limite} class="select select-bordered select-sm w-full max-w-xs">
|
<select
|
||||||
|
bind:value={limite}
|
||||||
|
class="select select-bordered select-sm w-full max-w-xs"
|
||||||
|
>
|
||||||
<option value={20}>20 registros</option>
|
<option value={20}>20 registros</option>
|
||||||
<option value={50}>50 registros</option>
|
<option value={50}>50 registros</option>
|
||||||
<option value={100}>100 registros</option>
|
<option value={100}>100 registros</option>
|
||||||
@@ -162,14 +262,36 @@
|
|||||||
|
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button class="btn btn-outline btn-primary btn-sm 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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
Exportar CSV
|
Exportar CSV
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-outline btn-secondary btn-sm gap-2">
|
<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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
Filtros Avançados
|
Filtros Avançados
|
||||||
</button>
|
</button>
|
||||||
@@ -184,25 +306,54 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<h2 class="card-title text-xl">
|
<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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
Atividades Recentes
|
Atividades Recentes
|
||||||
</h2>
|
</h2>
|
||||||
{#if atividades?.data}
|
{#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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if !atividades?.data}
|
{#if !atividades?.data}
|
||||||
<div class="flex flex-col items-center justify-center py-16">
|
<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>
|
<p class="text-base-content/60">Carregando atividades...</p>
|
||||||
</div>
|
</div>
|
||||||
{:else if atividades.data.length === 0}
|
{:else if atividades.data.length === 0}
|
||||||
<div class="flex flex-col items-center justify-center py-16 text-base-content/60">
|
<div
|
||||||
<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">
|
class="flex flex-col items-center justify-center py-16 text-base-content/60"
|
||||||
<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-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>
|
</svg>
|
||||||
<p class="text-lg font-medium">Nenhuma atividade registrada</p>
|
<p class="text-lg font-medium">Nenhuma atividade registrada</p>
|
||||||
<p class="text-sm mt-1">As atividades do sistema aparecerão aqui</p>
|
<p class="text-sm mt-1">As atividades do sistema aparecerão aqui</p>
|
||||||
@@ -224,32 +375,58 @@
|
|||||||
<tr class="hover transition-colors">
|
<tr class="hover transition-colors">
|
||||||
<td>
|
<td>
|
||||||
<div class="flex items-center gap-2">
|
<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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
<span class="font-mono text-xs">{formatarData(atividade.timestamp)}</span>
|
<span class="font-mono text-xs"
|
||||||
|
>{formatarData(atividade.timestamp)}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="flex flex-col">
|
<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}
|
{#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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<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)}
|
{getAcaoLabel(atividade.acao)}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="font-medium text-sm">{atividade.recurso}</span>
|
<span class="font-medium text-sm"
|
||||||
|
>{atividade.recurso}</span
|
||||||
|
>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="max-w-md">
|
<div class="max-w-md">
|
||||||
{#if atividade.detalhes}
|
{#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}
|
{atividade.detalhes}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -270,25 +447,52 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<h2 class="card-title text-xl">
|
<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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
Histórico de Logins
|
Histórico de Logins
|
||||||
</h2>
|
</h2>
|
||||||
{#if logins?.data}
|
{#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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if !logins?.data}
|
{#if !logins?.data}
|
||||||
<div class="flex flex-col items-center justify-center py-16">
|
<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>
|
<p class="text-base-content/60">Carregando logins...</p>
|
||||||
</div>
|
</div>
|
||||||
{:else if logins.data.length === 0}
|
{:else if logins.data.length === 0}
|
||||||
<div class="flex flex-col items-center justify-center py-16 text-base-content/60">
|
<div
|
||||||
<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">
|
class="flex flex-col items-center justify-center py-16 text-base-content/60"
|
||||||
<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-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>
|
</svg>
|
||||||
<p class="text-lg font-medium">Nenhum login registrado</p>
|
<p class="text-lg font-medium">Nenhum login registrado</p>
|
||||||
<p class="text-sm mt-1">Os acessos ao sistema aparecerão aqui</p>
|
<p class="text-sm mt-1">Os acessos ao sistema aparecerão aqui</p>
|
||||||
@@ -312,53 +516,112 @@
|
|||||||
<tr class="hover transition-colors">
|
<tr class="hover transition-colors">
|
||||||
<td>
|
<td>
|
||||||
<div class="flex items-center gap-2">
|
<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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
<span class="font-mono text-xs">{formatarData(login.timestamp)}</span>
|
<span class="font-mono text-xs"
|
||||||
|
>{formatarData(login.timestamp)}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="flex items-center gap-2">
|
<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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
<span class="text-sm font-medium">{login.matriculaOuEmail}</span>
|
<span class="text-sm font-medium"
|
||||||
|
>{login.matriculaOuEmail}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
{#if login.sucesso}
|
{#if login.sucesso}
|
||||||
<span class="badge badge-success badge-sm gap-1">
|
<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">
|
<svg
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
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>
|
</svg>
|
||||||
Sucesso
|
Sucesso
|
||||||
</span>
|
</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="badge badge-error badge-sm gap-1">
|
<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">
|
<svg
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
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>
|
</svg>
|
||||||
Falhou
|
Falhou
|
||||||
</span>
|
</span>
|
||||||
{#if login.motivoFalha}
|
{#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}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<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>
|
||||||
<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>
|
||||||
<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>
|
||||||
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -372,13 +635,25 @@
|
|||||||
|
|
||||||
<!-- Informação -->
|
<!-- Informação -->
|
||||||
<div class="alert alert-info shadow-lg mt-6">
|
<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">
|
<svg
|
||||||
<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>
|
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>
|
</svg>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold">Informação Importante</h3>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@@ -6,19 +6,45 @@
|
|||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex items-center justify-between mb-8">
|
<div class="flex items-center justify-between mb-8">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<div class="p-3 bg-gradient-to-br from-primary/20 to-primary/10 rounded-2xl">
|
<div
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
class="p-3 bg-linear-to-br from-primary/20 to-primary/10 rounded-2xl"
|
||||||
<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
|
||||||
|
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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-4xl font-bold text-primary">Monitoramento SGSE</h1>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<a href="/ti" class="btn btn-ghost">
|
<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">
|
<svg
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
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>
|
</svg>
|
||||||
Voltar
|
Voltar
|
||||||
</a>
|
</a>
|
||||||
@@ -27,4 +53,3 @@
|
|||||||
<!-- Card de Monitoramento -->
|
<!-- Card de Monitoramento -->
|
||||||
<SystemMonitorCardLocal />
|
<SystemMonitorCardLocal />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,17 @@
|
|||||||
import { useQuery, useConvexClient } from "convex-svelte";
|
import { useQuery, useConvexClient } from "convex-svelte";
|
||||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||||
import StatsCard from "$lib/components/ti/StatsCard.svelte";
|
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 client = useConvexClient();
|
||||||
const usuariosQuery = useQuery(api.usuarios.listar, {});
|
const usuariosQuery = useQuery(api.usuarios.listar, {});
|
||||||
@@ -23,19 +34,19 @@
|
|||||||
total: 0,
|
total: 0,
|
||||||
ativos: 0,
|
ativos: 0,
|
||||||
bloqueados: 0,
|
bloqueados: 0,
|
||||||
inativos: 0
|
inativos: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const ativos = usuarios.filter(u => u.ativo && !u.bloqueado).length;
|
const ativos = usuarios.filter((u) => u.ativo && !u.bloqueado).length;
|
||||||
const bloqueados = usuarios.filter(u => u.bloqueado === true).length;
|
const bloqueados = usuarios.filter((u) => u.bloqueado === true).length;
|
||||||
const inativos = usuarios.filter(u => !u.ativo).length;
|
const inativos = usuarios.filter((u) => !u.ativo).length;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
total: usuarios.length,
|
total: usuarios.length,
|
||||||
ativos,
|
ativos,
|
||||||
bloqueados,
|
bloqueados,
|
||||||
inativos
|
inativos,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -45,13 +56,15 @@
|
|||||||
<div class="flex items-center justify-between mb-8">
|
<div class="flex items-center justify-between mb-8">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<div class="p-3 bg-primary/10 rounded-xl">
|
<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">
|
<BarChart3 class="h-8 w-8 text-primary" />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-3xl font-bold text-base-content">Dashboard Administrativo TI</h1>
|
<h1 class="text-3xl font-bold text-base-content">
|
||||||
<p class="text-base-content/60 mt-1">Painel de controle e monitoramento do sistema</p>
|
Dashboard Administrativo TI
|
||||||
|
</h1>
|
||||||
|
<p class="text-base-content/60 mt-1">
|
||||||
|
Painel de controle e monitoramento do sistema
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -59,34 +72,38 @@
|
|||||||
<!-- Stats Cards -->
|
<!-- Stats Cards -->
|
||||||
{#if stats}
|
{#if stats}
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
<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
|
<StatsCard
|
||||||
title="Total de Usuários"
|
title="Total de Usuários"
|
||||||
value={stats.total}
|
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"
|
color="primary"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||||
<StatsCard
|
<StatsCard
|
||||||
title="Usuários Ativos"
|
title="Usuários Ativos"
|
||||||
value={stats.ativos}
|
value={stats.ativos}
|
||||||
description="{stats.total > 0 ? ((stats.ativos / stats.total) * 100).toFixed(1) + '% do total' : '0% do total'}"
|
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" />'
|
icon={CheckCircle}
|
||||||
color="success"
|
color="success"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||||
<StatsCard
|
<StatsCard
|
||||||
title="Usuários Bloqueados"
|
title="Usuários Bloqueados"
|
||||||
value={stats.bloqueados}
|
value={stats.bloqueados}
|
||||||
description="Requerem atenção"
|
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"
|
color="error"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||||
<StatsCard
|
<StatsCard
|
||||||
title="Usuários Inativos"
|
title="Usuários Inativos"
|
||||||
value={stats.inativos}
|
value={stats.inativos}
|
||||||
description="Desativados"
|
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"
|
color="warning"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -102,23 +119,17 @@
|
|||||||
<h2 class="card-title text-2xl mb-4">Ações Rápidas</h2>
|
<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">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<a href="/ti/usuarios" class="btn btn-primary">
|
<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">
|
<Plus class="h-5 w-5" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
|
||||||
</svg>
|
|
||||||
Criar Usuário
|
Criar Usuário
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="/ti/perfis" class="btn btn-secondary">
|
<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">
|
<FolderTree class="h-5 w-5" />
|
||||||
<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>
|
|
||||||
Gerenciar Perfis
|
Gerenciar Perfis
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="/ti/auditoria" class="btn btn-accent">
|
<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">
|
<FileText class="h-5 w-5" />
|
||||||
<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>
|
|
||||||
Ver Logs
|
Ver Logs
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -127,9 +138,10 @@
|
|||||||
|
|
||||||
<!-- Informação Sistema -->
|
<!-- Informação Sistema -->
|
||||||
<div class="alert alert-info">
|
<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">
|
<Info 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>
|
<span
|
||||||
</svg>
|
>Sistema de Gestão da Secretaria de Esportes - Versão 2.0 com controle
|
||||||
<span>Sistema de Gestão da Secretaria de Esportes - Versão 2.0 com controle avançado de acesso</span>
|
avançado de acesso</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,21 @@
|
|||||||
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { ptBR } from "date-fns/locale";
|
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 = {
|
type Role = {
|
||||||
_id: Id<"roles">;
|
_id: Id<"roles">;
|
||||||
@@ -40,10 +55,10 @@
|
|||||||
if (carregando) return null;
|
if (carregando) return null;
|
||||||
|
|
||||||
const porNivel = {
|
const porNivel = {
|
||||||
0: roles.filter(r => r.nivel === 0).length,
|
0: roles.filter((r) => r.nivel === 0).length,
|
||||||
1: roles.filter(r => r.nivel === 1).length,
|
1: roles.filter((r) => r.nivel === 1).length,
|
||||||
2: roles.filter(r => r.nivel === 2).length,
|
2: roles.filter((r) => r.nivel === 2).length,
|
||||||
3: roles.filter(r => r.nivel >= 3).length,
|
3: roles.filter((r) => r.nivel >= 3).length,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -52,7 +67,7 @@
|
|||||||
nivelAlto: porNivel[1],
|
nivelAlto: porNivel[1],
|
||||||
nivelMedio: porNivel[2],
|
nivelMedio: porNivel[2],
|
||||||
nivelBaixo: porNivel[3],
|
nivelBaixo: porNivel[3],
|
||||||
comSetor: roles.filter(r => r.setor).length,
|
comSetor: roles.filter((r) => r.setor).length,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -65,7 +80,7 @@
|
|||||||
resultado = resultado.filter(
|
resultado = resultado.filter(
|
||||||
(r) =>
|
(r) =>
|
||||||
r.nome.toLowerCase().includes(buscaLower) ||
|
r.nome.toLowerCase().includes(buscaLower) ||
|
||||||
r.descricao.toLowerCase().includes(buscaLower)
|
r.descricao.toLowerCase().includes(buscaLower),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,33 +152,27 @@
|
|||||||
filtroNivel = "";
|
filtroNivel = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const temFiltrosAtivos = $derived(busca.trim() !== "" || filtroSetor !== "" || filtroNivel !== "");
|
const temFiltrosAtivos = $derived(
|
||||||
|
busca.trim() !== "" || filtroSetor !== "" || filtroNivel !== "",
|
||||||
|
);
|
||||||
</script>
|
</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">
|
<div class="container mx-auto px-4 py-6 max-w-7xl">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex items-center justify-between mb-8">
|
<div class="flex items-center justify-between mb-8">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<div class="p-3 bg-primary/10 rounded-xl">
|
<div class="p-3 bg-primary/10 rounded-xl">
|
||||||
<svg
|
<Users class="h-8 w-8 text-primary" />
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-3xl font-bold text-base-content">Gestão de Perfis</h1>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -171,38 +180,45 @@
|
|||||||
<!-- Estatísticas -->
|
<!-- Estatísticas -->
|
||||||
{#if stats}
|
{#if stats}
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4 mb-8">
|
<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
|
<StatsCard
|
||||||
title="Total de Perfis"
|
title="Total de Perfis"
|
||||||
value={stats.total}
|
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"
|
color="primary"
|
||||||
/>
|
/>
|
||||||
|
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||||
<StatsCard
|
<StatsCard
|
||||||
title="Nível Máximo"
|
title="Nível Máximo"
|
||||||
value={stats.nivelMaximo}
|
value={stats.nivelMaximo}
|
||||||
description="Acesso total"
|
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"
|
color="error"
|
||||||
/>
|
/>
|
||||||
|
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||||
<StatsCard
|
<StatsCard
|
||||||
title="Nível Alto"
|
title="Nível Alto"
|
||||||
value={stats.nivelAlto}
|
value={stats.nivelAlto}
|
||||||
description="Acesso elevado"
|
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"
|
color="warning"
|
||||||
/>
|
/>
|
||||||
|
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||||
<StatsCard
|
<StatsCard
|
||||||
title="Nível Médio"
|
title="Nível Médio"
|
||||||
value={stats.nivelMedio}
|
value={stats.nivelMedio}
|
||||||
description="Acesso padrão"
|
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"
|
color="info"
|
||||||
/>
|
/>
|
||||||
|
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||||
<StatsCard
|
<StatsCard
|
||||||
title="Com Setor"
|
title="Com Setor"
|
||||||
value={stats.comSetor}
|
value={stats.comSetor}
|
||||||
description="{stats.total > 0 ? ((stats.comSetor / stats.total) * 100).toFixed(0) + '% do total' : '0%'}"
|
description={stats.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" />'
|
? ((stats.comSetor / stats.total) * 100).toFixed(0) + "% do total"
|
||||||
|
: "0%"}
|
||||||
|
icon={Building2}
|
||||||
color="secondary"
|
color="secondary"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -214,38 +230,16 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="flex flex-wrap items-center justify-between gap-4 mb-4">
|
<div class="flex flex-wrap items-center justify-between gap-4 mb-4">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<svg
|
<Search class="h-5 w-5 text-primary" />
|
||||||
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>
|
|
||||||
<h2 class="card-title text-lg">Filtros de Busca</h2>
|
<h2 class="card-title text-lg">Filtros de Busca</h2>
|
||||||
</div>
|
</div>
|
||||||
{#if temFiltrosAtivos}
|
{#if temFiltrosAtivos}
|
||||||
<button type="button" class="btn btn-sm btn-outline btn-error" onclick={limparFiltros}>
|
<button
|
||||||
<svg
|
type="button"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
class="btn btn-sm btn-outline btn-error"
|
||||||
class="h-4 w-4"
|
onclick={limparFiltros}
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
>
|
||||||
<path
|
<X class="h-4 w-4" />
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M6 18L18 6M6 6l12 12"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Limpar Filtros
|
Limpar Filtros
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -265,20 +259,9 @@
|
|||||||
placeholder="Buscar por nome ou descrição..."
|
placeholder="Buscar por nome ou descrição..."
|
||||||
class="input input-bordered w-full pl-10"
|
class="input input-bordered w-full pl-10"
|
||||||
/>
|
/>
|
||||||
<svg
|
<Search
|
||||||
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/40"
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -287,7 +270,11 @@
|
|||||||
<label class="label" for="filtro-setor">
|
<label class="label" for="filtro-setor">
|
||||||
<span class="label-text font-medium">Setor</span>
|
<span class="label-text font-medium">Setor</span>
|
||||||
</label>
|
</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>
|
<option value="">Todos os setores</option>
|
||||||
{#each setoresDisponiveis as setor}
|
{#each setoresDisponiveis as setor}
|
||||||
<option value={setor}>{setor}</option>
|
<option value={setor}>{setor}</option>
|
||||||
@@ -300,7 +287,11 @@
|
|||||||
<label class="label" for="filtro-nivel">
|
<label class="label" for="filtro-nivel">
|
||||||
<span class="label-text font-medium">Nível de Acesso</span>
|
<span class="label-text font-medium">Nível de Acesso</span>
|
||||||
</label>
|
</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="">Todos os níveis</option>
|
||||||
<option value={0}>Máximo (0)</option>
|
<option value={0}>Máximo (0)</option>
|
||||||
<option value={1}>Alto (1)</option>
|
<option value={1}>Alto (1)</option>
|
||||||
@@ -312,7 +303,12 @@
|
|||||||
|
|
||||||
<div class="mt-4 flex items-center justify-between">
|
<div class="mt-4 flex items-center justify-between">
|
||||||
<div class="text-sm text-base-content/60">
|
<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}
|
{#if temFiltrosAtivos}
|
||||||
<span class="badge badge-primary badge-sm ml-2">Filtrado</span>
|
<span class="badge badge-primary badge-sm ml-2">Filtrado</span>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -329,45 +325,28 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else if roles.length === 0}
|
{:else if roles.length === 0}
|
||||||
<div class="flex flex-col items-center justify-center py-16 text-center">
|
<div class="flex flex-col items-center justify-center py-16 text-center">
|
||||||
<svg
|
<Users class="h-16 w-16 text-base-content/30" />
|
||||||
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>
|
|
||||||
<h3 class="text-xl font-semibold mt-4">Nenhum perfil encontrado</h3>
|
<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>
|
</div>
|
||||||
{:else if rolesFiltradas.length === 0}
|
{:else if rolesFiltradas.length === 0}
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<div class="card bg-base-100 shadow-xl">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="flex flex-col items-center justify-center py-16 text-center">
|
<div
|
||||||
<svg
|
class="flex flex-col items-center justify-center py-16 text-center"
|
||||||
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
|
<AlertCircle class="h-16 w-16 text-base-content/30 mb-4" />
|
||||||
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>
|
|
||||||
<h3 class="text-xl font-semibold mt-4">Nenhum perfil encontrado</h3>
|
<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}
|
{#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
|
Limpar Filtros
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -376,115 +355,73 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{#each rolesFiltradas as role}
|
{#each rolesFiltradas as role (role._id)}
|
||||||
<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)}>
|
<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="card-body">
|
||||||
<div class="flex items-start justify-between mb-4">
|
<div class="flex items-start justify-between mb-4">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h2 class="card-title text-lg mb-1">{role.descricao}</h2>
|
<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>
|
||||||
<div class="p-2 bg-base-200 rounded-lg">
|
<div class="p-2 bg-base-200 rounded-lg">
|
||||||
<svg
|
<User class="h-6 w-6 text-primary" />
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-3 text-sm">
|
<div class="space-y-3 text-sm">
|
||||||
<div class="flex items-center gap-2 p-2 bg-base-200 rounded-lg">
|
<div class="flex items-center gap-2 p-2 bg-base-200 rounded-lg">
|
||||||
<svg
|
<Tag class="h-4 w-4 text-base-content/40" />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<span class="font-medium text-base-content/60"
|
||||||
class="h-4 w-4 text-base-content/40"
|
>Nome técnico:</span
|
||||||
fill="none"
|
>
|
||||||
viewBox="0 0 24 24"
|
<code class="text-xs bg-base-100 px-2 py-1 rounded font-mono"
|
||||||
stroke="currentColor"
|
>{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>
|
</div>
|
||||||
|
|
||||||
{#if role.setor}
|
{#if role.setor}
|
||||||
<div class="flex items-center gap-2 p-2 bg-base-200 rounded-lg">
|
<div
|
||||||
<svg
|
class="flex items-center gap-2 p-2 bg-base-200 rounded-lg"
|
||||||
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
|
<Building2 class="h-4 w-4 text-base-content/40" />
|
||||||
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="font-medium text-base-content/60">Setor:</span>
|
<span class="font-medium text-base-content/60">Setor:</span>
|
||||||
<span class="font-medium">{role.setor}</span>
|
<span class="font-medium">{role.setor}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="flex items-center gap-2 p-2 bg-base-200 rounded-lg">
|
<div class="flex items-center gap-2 p-2 bg-base-200 rounded-lg">
|
||||||
<svg
|
<Shield class="h-4 w-4 text-base-content/40" />
|
||||||
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>
|
|
||||||
<span class="font-medium text-base-content/60">Nível:</span>
|
<span class="font-medium text-base-content/60">Nível:</span>
|
||||||
<span class="font-bold text-lg">{role.nivel}</span>
|
<span class="font-bold text-lg">{role.nivel}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-actions justify-end mt-4 pt-4 border-t border-base-300">
|
<div
|
||||||
<button class="btn btn-sm btn-primary btn-outline" onclick={(e) => { e.stopPropagation(); abrirDetalhes(role); }}>
|
class="card-actions justify-end mt-4 pt-4 border-t border-base-300"
|
||||||
<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
|
<button
|
||||||
stroke-linecap="round"
|
class="btn btn-sm btn-primary btn-outline"
|
||||||
stroke-linejoin="round"
|
onclick={(e) => {
|
||||||
stroke-width="2"
|
e.stopPropagation();
|
||||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
abrirDetalhes(role);
|
||||||
/>
|
}}
|
||||||
<path
|
>
|
||||||
stroke-linecap="round"
|
<Eye class="h-4 w-4 mr-1" />
|
||||||
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>
|
|
||||||
Ver Detalhes
|
Ver Detalhes
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -501,40 +438,41 @@
|
|||||||
<div class="modal-box max-w-3xl">
|
<div class="modal-box max-w-3xl">
|
||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<h3 class="font-bold text-2xl">Detalhes do Perfil</h3>
|
<h3 class="font-bold text-2xl">Detalhes do Perfil</h3>
|
||||||
<button type="button" class="btn btn-sm btn-circle btn-ghost" onclick={fecharDetalhes}>
|
<button
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
type="button"
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
class="btn btn-sm btn-circle btn-ghost"
|
||||||
</svg>
|
onclick={fecharDetalhes}
|
||||||
|
>
|
||||||
|
<X class="h-6 w-6" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<!-- Header do Perfil -->
|
<!-- 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="card-body">
|
||||||
<div class="flex items-start justify-between">
|
<div class="flex items-start justify-between">
|
||||||
<div class="flex-1">
|
<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="flex items-center gap-3">
|
||||||
<div class="badge {obterCorNivel(roleSelecionada.nivel)} badge-lg">{obterTextoNivel(roleSelecionada.nivel)}</div>
|
<div
|
||||||
<span class="text-sm text-base-content/60">Nível {roleSelecionada.nivel}</span>
|
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>
|
</div>
|
||||||
<div class="p-3 bg-base-100 rounded-lg shadow-sm">
|
<div class="p-3 bg-base-100 rounded-lg shadow-sm">
|
||||||
<svg
|
<User class="h-8 w-8 text-primary" />
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -545,24 +483,27 @@
|
|||||||
<div class="card bg-base-100 border border-base-300">
|
<div class="card bg-base-100 border border-base-300">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text font-semibold flex items-center gap-2">
|
<span
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
class="label-text font-semibold flex items-center gap-2"
|
||||||
<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>
|
<Tag class="h-5 w-5 text-primary" />
|
||||||
Nome Técnico
|
Nome Técnico
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</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>
|
</div>
|
||||||
|
|
||||||
<div class="card bg-base-100 border border-base-300">
|
<div class="card bg-base-100 border border-base-300">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text font-semibold flex items-center gap-2">
|
<span
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
class="label-text font-semibold flex items-center gap-2"
|
||||||
<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>
|
<Building2 class="h-5 w-5 text-primary" />
|
||||||
Setor
|
Setor
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -570,7 +511,9 @@
|
|||||||
{#if roleSelecionada.setor}
|
{#if roleSelecionada.setor}
|
||||||
{roleSelecionada.setor}
|
{roleSelecionada.setor}
|
||||||
{:else}
|
{:else}
|
||||||
<span class="text-base-content/40 italic">Não especificado</span>
|
<span class="text-base-content/40 italic"
|
||||||
|
>Não especificado</span
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -582,26 +525,33 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text font-semibold flex items-center gap-2">
|
<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">
|
<Shield class="h-5 w-5 text-primary" />
|
||||||
<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>
|
|
||||||
Nível de Acesso
|
Nível de Acesso
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<div class="flex items-center gap-4 mb-3">
|
<div class="flex items-center gap-4 mb-3">
|
||||||
<span class="text-4xl font-bold">{roleSelecionada.nivel}</span>
|
<span class="text-4xl font-bold">{roleSelecionada.nivel}</span
|
||||||
<div class="badge {obterCorNivel(roleSelecionada.nivel)} badge-lg">{obterTextoNivel(roleSelecionada.nivel)}</div>
|
>
|
||||||
|
<div
|
||||||
|
class="badge {obterCorNivel(
|
||||||
|
roleSelecionada.nivel,
|
||||||
|
)} badge-lg"
|
||||||
|
>
|
||||||
|
{obterTextoNivel(roleSelecionada.nivel)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="alert alert-info">
|
<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">
|
<Info 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 class="text-sm">
|
<span class="text-sm">
|
||||||
{roleSelecionada.nivel === 0 && "Acesso total irrestrito ao sistema. Pode realizar todas as operações sem restrições."}
|
{roleSelecionada.nivel === 0 &&
|
||||||
{roleSelecionada.nivel === 1 && "Acesso alto com algumas restrições. Pode realizar a maioria das operações administrativas."}
|
"Acesso total irrestrito ao sistema. Pode realizar todas as operações sem restrições."}
|
||||||
{roleSelecionada.nivel === 2 && "Acesso médio com permissões configuráveis. Pode realizar operações padrão do sistema."}
|
{roleSelecionada.nivel === 1 &&
|
||||||
{roleSelecionada.nivel >= 3 && "Acesso limitado com permissões específicas. Operações restritas conforme configuração."}
|
"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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -613,35 +563,27 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text font-semibold flex items-center gap-2">
|
<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">
|
<Calendar class="h-5 w-5 text-primary" />
|
||||||
<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>
|
|
||||||
Data de Criação
|
Data de Criação
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Informação sobre Permissões -->
|
<!-- Informação sobre Permissões -->
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
<svg
|
<Info class="stroke-current shrink-0 w-6 h-6" />
|
||||||
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>
|
<div>
|
||||||
<h4 class="font-semibold mb-1">Configuração de Permissões</h4>
|
<h4 class="font-semibold mb-1">Configuração de Permissões</h4>
|
||||||
<p class="text-sm">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -652,16 +594,23 @@
|
|||||||
Fechar
|
Fechar
|
||||||
</button>
|
</button>
|
||||||
<a href="/ti/painel-permissoes" class="btn btn-primary">
|
<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">
|
<Settings class="h-5 w-5 mr-2" />
|
||||||
<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>
|
|
||||||
Configurar Permissões
|
Configurar Permissões
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,22 @@
|
|||||||
import StatsCard from "$lib/components/ti/StatsCard.svelte";
|
import StatsCard from "$lib/components/ti/StatsCard.svelte";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { ptBR } from "date-fns/locale";
|
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";
|
type StatusSolicitacao = "pendente" | "aprovado" | "rejeitado";
|
||||||
|
|
||||||
@@ -52,7 +68,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ("data" in solicitacoesQuery && solicitacoesQuery.data !== undefined) {
|
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)) {
|
if (Array.isArray(solicitacoesQuery)) {
|
||||||
@@ -71,15 +89,21 @@
|
|||||||
if (carregando) return null;
|
if (carregando) return null;
|
||||||
|
|
||||||
const total = solicitacoes.length;
|
const total = solicitacoes.length;
|
||||||
const pendentes = solicitacoes.filter(s => s.status === "pendente").length;
|
const pendentes = solicitacoes.filter(
|
||||||
const aprovadas = solicitacoes.filter(s => s.status === "aprovado").length;
|
(s) => s.status === "pendente",
|
||||||
const rejeitadas = solicitacoes.filter(s => s.status === "rejeitado").length;
|
).length;
|
||||||
|
const aprovadas = solicitacoes.filter(
|
||||||
|
(s) => s.status === "aprovado",
|
||||||
|
).length;
|
||||||
|
const rejeitadas = solicitacoes.filter(
|
||||||
|
(s) => s.status === "rejeitado",
|
||||||
|
).length;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
total,
|
total,
|
||||||
pendentes,
|
pendentes,
|
||||||
aprovadas,
|
aprovadas,
|
||||||
rejeitadas
|
rejeitadas,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -89,16 +113,17 @@
|
|||||||
|
|
||||||
// Filtrar por status
|
// Filtrar por status
|
||||||
if (filtroStatus !== "todos") {
|
if (filtroStatus !== "todos") {
|
||||||
resultado = resultado.filter(s => s.status === filtroStatus);
|
resultado = resultado.filter((s) => s.status === filtroStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buscar por nome, matrícula ou email
|
// Buscar por nome, matrícula ou email
|
||||||
if (busca.trim()) {
|
if (busca.trim()) {
|
||||||
const termo = busca.toLowerCase().trim();
|
const termo = busca.toLowerCase().trim();
|
||||||
resultado = resultado.filter(s =>
|
resultado = resultado.filter(
|
||||||
|
(s) =>
|
||||||
s.nome.toLowerCase().includes(termo) ||
|
s.nome.toLowerCase().includes(termo) ||
|
||||||
s.matricula.toLowerCase().includes(termo) ||
|
s.matricula.toLowerCase().includes(termo) ||
|
||||||
s.email.toLowerCase().includes(termo)
|
s.email.toLowerCase().includes(termo),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +133,9 @@
|
|||||||
|
|
||||||
// Funções auxiliares
|
// Funções auxiliares
|
||||||
function formatarData(timestamp: number): string {
|
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 {
|
function formatarDataRelativa(timestamp: number): string {
|
||||||
@@ -118,9 +145,9 @@
|
|||||||
const horas = Math.floor(diff / (1000 * 60 * 60));
|
const horas = Math.floor(diff / (1000 * 60 * 60));
|
||||||
const minutos = Math.floor(diff / (1000 * 60));
|
const minutos = Math.floor(diff / (1000 * 60));
|
||||||
|
|
||||||
if (dias > 0) return `${dias} dia${dias > 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 (horas > 0) return `${horas} hora${horas > 1 ? "s" : ""} atrás`;
|
||||||
if (minutos > 0) return `${minutos} minuto${minutos > 1 ? 's' : ''} atrás`;
|
if (minutos > 0) return `${minutos} minuto${minutos > 1 ? "s" : ""} atrás`;
|
||||||
return "Agora";
|
return "Agora";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +237,8 @@
|
|||||||
mensagem = null;
|
mensagem = null;
|
||||||
}, 3000);
|
}, 3000);
|
||||||
} catch (error) {
|
} 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 = {
|
mensagem = {
|
||||||
tipo: "error",
|
tipo: "error",
|
||||||
texto: errorMessage,
|
texto: errorMessage,
|
||||||
@@ -244,7 +272,8 @@
|
|||||||
mensagem = null;
|
mensagem = null;
|
||||||
}, 3000);
|
}, 3000);
|
||||||
} catch (error) {
|
} 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 = {
|
mensagem = {
|
||||||
tipo: "error",
|
tipo: "error",
|
||||||
texto: errorMessage,
|
texto: errorMessage,
|
||||||
@@ -255,19 +284,18 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</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">
|
<div class="container mx-auto px-4 py-6 max-w-7xl">
|
||||||
<!-- Mensagem de Feedback -->
|
<!-- Mensagem de Feedback -->
|
||||||
{#if mensagem}
|
{#if mensagem}
|
||||||
<div class="alert alert-{mensagem.tipo} shadow-lg mb-6">
|
<div class="alert alert-{mensagem.tipo} shadow-lg mb-6">
|
||||||
{#if mensagem.tipo === "success"}
|
{#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">
|
<CircleCheckBig class="stroke-current shrink-0 h-6 w-6" />
|
||||||
<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>
|
|
||||||
{:else if mensagem.tipo === "error"}
|
{: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">
|
<CircleX class="stroke-current shrink-0 h-6 w-6" />
|
||||||
<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>
|
|
||||||
{/if}
|
{/if}
|
||||||
<span>{mensagem.texto}</span>
|
<span>{mensagem.texto}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -277,13 +305,15 @@
|
|||||||
<div class="flex items-center justify-between mb-8">
|
<div class="flex items-center justify-between mb-8">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<div class="p-3 bg-primary/10 rounded-xl">
|
<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">
|
<UserPlus class="h-8 w-8 text-primary" />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-3xl font-bold text-base-content">Solicitações de Acesso</h1>
|
<h1 class="text-3xl font-bold text-base-content">
|
||||||
<p class="text-base-content/60 mt-1">Gerencie e analise solicitações de acesso ao sistema</p>
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -291,34 +321,38 @@
|
|||||||
<!-- Estatísticas -->
|
<!-- Estatísticas -->
|
||||||
{#if stats}
|
{#if stats}
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
<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
|
<StatsCard
|
||||||
title="Total de Solicitações"
|
title="Total de Solicitações"
|
||||||
value={stats.total}
|
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"
|
color="primary"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||||
<StatsCard
|
<StatsCard
|
||||||
title="Pendentes"
|
title="Pendentes"
|
||||||
value={stats.pendentes}
|
value={stats.pendentes}
|
||||||
description="{stats.total > 0 ? ((stats.pendentes / stats.total) * 100).toFixed(1) + '% do total' : '0% do total'}"
|
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" />'
|
icon={Clock}
|
||||||
color="warning"
|
color="warning"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||||
<StatsCard
|
<StatsCard
|
||||||
title="Aprovadas"
|
title="Aprovadas"
|
||||||
value={stats.aprovadas}
|
value={stats.aprovadas}
|
||||||
description="{stats.total > 0 ? ((stats.aprovadas / stats.total) * 100).toFixed(1) + '% do total' : '0% do total'}"
|
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" />'
|
icon={CheckCircle}
|
||||||
color="success"
|
color="success"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||||
<StatsCard
|
<StatsCard
|
||||||
title="Rejeitadas"
|
title="Rejeitadas"
|
||||||
value={stats.rejeitadas}
|
value={stats.rejeitadas}
|
||||||
description="{stats.total > 0 ? ((stats.rejeitadas / stats.total) * 100).toFixed(1) + '% do total' : '0% do total'}"
|
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" />'
|
icon={XCircle}
|
||||||
color="error"
|
color="error"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -335,25 +369,25 @@
|
|||||||
<div class="tabs tabs-boxed mb-4 bg-base-200 p-2">
|
<div class="tabs tabs-boxed mb-4 bg-base-200 p-2">
|
||||||
<button
|
<button
|
||||||
class="tab {filtroStatus === 'todos' ? 'tab-active' : ''}"
|
class="tab {filtroStatus === 'todos' ? 'tab-active' : ''}"
|
||||||
onclick={() => filtroStatus = "todos"}
|
onclick={() => (filtroStatus = "todos")}
|
||||||
>
|
>
|
||||||
Todas
|
Todas
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="tab {filtroStatus === 'pendente' ? 'tab-active' : ''}"
|
class="tab {filtroStatus === 'pendente' ? 'tab-active' : ''}"
|
||||||
onclick={() => filtroStatus = "pendente"}
|
onclick={() => (filtroStatus = "pendente")}
|
||||||
>
|
>
|
||||||
Pendentes
|
Pendentes
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="tab {filtroStatus === 'aprovado' ? 'tab-active' : ''}"
|
class="tab {filtroStatus === 'aprovado' ? 'tab-active' : ''}"
|
||||||
onclick={() => filtroStatus = "aprovado"}
|
onclick={() => (filtroStatus = "aprovado")}
|
||||||
>
|
>
|
||||||
Aprovadas
|
Aprovadas
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="tab {filtroStatus === 'rejeitado' ? 'tab-active' : ''}"
|
class="tab {filtroStatus === 'rejeitado' ? 'tab-active' : ''}"
|
||||||
onclick={() => filtroStatus = "rejeitado"}
|
onclick={() => (filtroStatus = "rejeitado")}
|
||||||
>
|
>
|
||||||
Rejeitadas
|
Rejeitadas
|
||||||
</button>
|
</button>
|
||||||
@@ -361,9 +395,11 @@
|
|||||||
|
|
||||||
<!-- Campo de Busca -->
|
<!-- Campo de Busca -->
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label">
|
<div class="label">
|
||||||
<span class="label-text font-semibold">Buscar por nome, matrícula ou e-mail</span>
|
<span class="label-text font-semibold"
|
||||||
</label>
|
>Buscar por nome, matrícula ou e-mail</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -371,9 +407,9 @@
|
|||||||
class="input input-bordered w-full pl-10"
|
class="input input-bordered w-full pl-10"
|
||||||
bind:value={busca}
|
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">
|
<Search
|
||||||
<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" />
|
class="h-5 w-5 absolute left-3 top-1/2 transform -translate-y-1/2 text-base-content/50"
|
||||||
</svg>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -387,10 +423,10 @@
|
|||||||
{:else if solicitacoesFiltradas.length === 0}
|
{:else if solicitacoesFiltradas.length === 0}
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<div class="card bg-base-100 shadow-xl">
|
||||||
<div class="card-body text-center py-20">
|
<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">
|
<FileText class="h-16 w-16 mx-auto text-base-content/30 mb-4" />
|
||||||
<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" />
|
<h3 class="text-xl font-semibold text-base-content/70 mb-2">
|
||||||
</svg>
|
Nenhuma solicitação encontrada
|
||||||
<h3 class="text-xl font-semibold text-base-content/70 mb-2">Nenhuma solicitação encontrada</h3>
|
</h3>
|
||||||
<p class="text-base-content/50">
|
<p class="text-base-content/50">
|
||||||
{#if busca.trim() || filtroStatus !== "todos"}
|
{#if busca.trim() || filtroStatus !== "todos"}
|
||||||
Tente ajustar os filtros ou a busca.
|
Tente ajustar os filtros ou a busca.
|
||||||
@@ -403,47 +439,60 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div class="grid grid-cols-1 gap-4">
|
<div class="grid grid-cols-1 gap-4">
|
||||||
{#each solicitacoesFiltradas as solicitacao}
|
{#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="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-1">
|
||||||
<div class="flex items-center gap-3 mb-2">
|
<div class="flex items-center gap-3 mb-2">
|
||||||
<h3 class="text-xl font-bold text-base-content">{solicitacao.nome}</h3>
|
<h3 class="text-xl font-bold text-base-content">
|
||||||
<span class="badge {getStatusBadge(solicitacao.status)} badge-lg">
|
{solicitacao.nome}
|
||||||
|
</h3>
|
||||||
|
<span
|
||||||
|
class="badge {getStatusBadge(
|
||||||
|
solicitacao.status,
|
||||||
|
)} badge-lg"
|
||||||
|
>
|
||||||
{getStatusTexto(solicitacao.status)}
|
{getStatusTexto(solicitacao.status)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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">
|
<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">
|
<UserPlus class="h-4 w-4" />
|
||||||
<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>
|
|
||||||
<span class="font-semibold">Matrícula:</span>
|
<span class="font-semibold">Matrícula:</span>
|
||||||
<span>{solicitacao.matricula}</span>
|
<span>{solicitacao.matricula}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<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">
|
<Mail class="h-4 w-4" />
|
||||||
<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>
|
|
||||||
<span class="font-semibold">E-mail:</span>
|
<span class="font-semibold">E-mail:</span>
|
||||||
<span>{solicitacao.email}</span>
|
<span>{solicitacao.email}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<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">
|
<Phone class="h-4 w-4" />
|
||||||
<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>
|
|
||||||
<span class="font-semibold">Telefone:</span>
|
<span class="font-semibold">Telefone:</span>
|
||||||
<span>{solicitacao.telefone}</span>
|
<span>{solicitacao.telefone}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3 text-xs text-base-content/50">
|
<div
|
||||||
<span class="font-semibold">Solicitado em:</span> {formatarData(solicitacao.dataSolicitacao)} ({formatarDataRelativa(solicitacao.dataSolicitacao)})
|
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}
|
{#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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -453,10 +502,7 @@
|
|||||||
class="btn btn-sm btn-outline btn-primary"
|
class="btn btn-sm btn-outline btn-primary"
|
||||||
onclick={() => abrirDetalhes(solicitacao)}
|
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">
|
<Eye class="h-4 w-4" />
|
||||||
<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>
|
|
||||||
Ver Detalhes
|
Ver Detalhes
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -465,9 +511,7 @@
|
|||||||
class="btn btn-sm btn-success"
|
class="btn btn-sm btn-success"
|
||||||
onclick={() => abrirAprovar(solicitacao)}
|
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">
|
<Check class="h-4 w-4" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
|
||||||
</svg>
|
|
||||||
Aprovar
|
Aprovar
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -475,9 +519,7 @@
|
|||||||
class="btn btn-sm btn-error"
|
class="btn btn-sm btn-error"
|
||||||
onclick={() => abrirRejeitar(solicitacao)}
|
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">
|
<X class="h-4 w-4" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
Rejeitar
|
Rejeitar
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -497,44 +539,58 @@
|
|||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="flex items-center gap-3 mb-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)}
|
{getStatusTexto(solicitacaoSelecionada.status)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="label">
|
<div class="label">
|
||||||
<span class="label-text font-semibold">Nome Completo</span>
|
<span class="label-text font-semibold">Nome Completo</span>
|
||||||
</label>
|
</div>
|
||||||
<div class="input input-bordered">{solicitacaoSelecionada.nome}</div>
|
<div class="input input-bordered">
|
||||||
|
{solicitacaoSelecionada.nome}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="label">
|
<div class="label">
|
||||||
<span class="label-text font-semibold">Matrícula</span>
|
<span class="label-text font-semibold">Matrícula</span>
|
||||||
</label>
|
</div>
|
||||||
<div class="input input-bordered">{solicitacaoSelecionada.matricula}</div>
|
<div class="input input-bordered">
|
||||||
|
{solicitacaoSelecionada.matricula}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="label">
|
<div class="label">
|
||||||
<span class="label-text font-semibold">E-mail</span>
|
<span class="label-text font-semibold">E-mail</span>
|
||||||
</label>
|
</div>
|
||||||
<div class="input input-bordered">{solicitacaoSelecionada.email}</div>
|
<div class="input input-bordered">
|
||||||
|
{solicitacaoSelecionada.email}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="label">
|
<div class="label">
|
||||||
<span class="label-text font-semibold">Telefone</span>
|
<span class="label-text font-semibold">Telefone</span>
|
||||||
</label>
|
</div>
|
||||||
<div class="input input-bordered">{solicitacaoSelecionada.telefone}</div>
|
<div class="input input-bordered">
|
||||||
|
{solicitacaoSelecionada.telefone}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="label">
|
<div class="label">
|
||||||
<span class="label-text font-semibold">Data da Solicitação</span>
|
<span class="label-text font-semibold"
|
||||||
</label>
|
>Data da Solicitação</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
<div class="input input-bordered">
|
<div class="input input-bordered">
|
||||||
{formatarData(solicitacaoSelecionada.dataSolicitacao)}
|
{formatarData(solicitacaoSelecionada.dataSolicitacao)}
|
||||||
</div>
|
</div>
|
||||||
@@ -542,9 +598,11 @@
|
|||||||
|
|
||||||
{#if solicitacaoSelecionada.dataResposta}
|
{#if solicitacaoSelecionada.dataResposta}
|
||||||
<div>
|
<div>
|
||||||
<label class="label">
|
<div class="label">
|
||||||
<span class="label-text font-semibold">Data de Processamento</span>
|
<span class="label-text font-semibold"
|
||||||
</label>
|
>Data de Processamento</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
<div class="input input-bordered">
|
<div class="input input-bordered">
|
||||||
{formatarData(solicitacaoSelecionada.dataResposta)}
|
{formatarData(solicitacaoSelecionada.dataResposta)}
|
||||||
</div>
|
</div>
|
||||||
@@ -554,9 +612,9 @@
|
|||||||
|
|
||||||
{#if solicitacaoSelecionada.observacoes}
|
{#if solicitacaoSelecionada.observacoes}
|
||||||
<div>
|
<div>
|
||||||
<label class="label">
|
<div class="label">
|
||||||
<span class="label-text font-semibold">Observações</span>
|
<span class="label-text font-semibold">Observações</span>
|
||||||
</label>
|
</div>
|
||||||
<div class="textarea textarea-bordered min-h-24">
|
<div class="textarea textarea-bordered min-h-24">
|
||||||
{solicitacaoSelecionada.observacoes}
|
{solicitacaoSelecionada.observacoes}
|
||||||
</div>
|
</div>
|
||||||
@@ -582,17 +640,22 @@
|
|||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<p class="text-base-content/70 mb-2">
|
<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>
|
||||||
<p class="text-sm text-base-content/60">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-control mb-4">
|
<div class="form-control mb-4">
|
||||||
<label class="label">
|
<div class="label">
|
||||||
<span class="label-text font-semibold">Observações (opcional)</span>
|
<span class="label-text font-semibold"
|
||||||
</label>
|
>Observações (opcional)</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
class="textarea textarea-bordered"
|
class="textarea textarea-bordered"
|
||||||
placeholder="Adicione observações sobre a aprovação..."
|
placeholder="Adicione observações sobre a aprovação..."
|
||||||
@@ -618,9 +681,7 @@
|
|||||||
<span class="loading loading-spinner loading-sm"></span>
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
Processando...
|
Processando...
|
||||||
{:else}
|
{:else}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<Check class="h-5 w-5" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
|
||||||
</svg>
|
|
||||||
Confirmar Aprovação
|
Confirmar Aprovação
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
@@ -640,17 +701,22 @@
|
|||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<p class="text-base-content/70 mb-2">
|
<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>
|
||||||
<p class="text-sm text-base-content/60">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-control mb-4">
|
<div class="form-control mb-4">
|
||||||
<label class="label">
|
<div class="label">
|
||||||
<span class="label-text font-semibold">Motivo da Rejeição (recomendado)</span>
|
<span class="label-text font-semibold"
|
||||||
</label>
|
>Motivo da Rejeição (recomendado)</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
class="textarea textarea-bordered"
|
class="textarea textarea-bordered"
|
||||||
placeholder="Descreva o motivo da rejeição..."
|
placeholder="Descreva o motivo da rejeição..."
|
||||||
@@ -676,9 +742,7 @@
|
|||||||
<span class="loading loading-spinner loading-sm"></span>
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
Processando...
|
Processando...
|
||||||
{:else}
|
{:else}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<X class="h-5 w-5" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
Confirmar Rejeição
|
Confirmar Rejeição
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Estados do formulário
|
// Estados do formulário
|
||||||
let matricula = $state("");
|
|
||||||
let nome = $state("");
|
let nome = $state("");
|
||||||
let email = $state("");
|
let email = $state("");
|
||||||
let roleId = $state("");
|
let roleId = $state("");
|
||||||
@@ -30,7 +29,9 @@
|
|||||||
let senhaInicial = $state("");
|
let senhaInicial = $state("");
|
||||||
let confirmarSenha = $state("");
|
let confirmarSenha = $state("");
|
||||||
let processando = $state(false);
|
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) {
|
function mostrarMensagem(tipo: "success" | "error", texto: string) {
|
||||||
mensagem = { tipo, texto };
|
mensagem = { tipo, texto };
|
||||||
@@ -43,8 +44,7 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
// Validações
|
// Validações
|
||||||
const matriculaStr = String(matricula).trim();
|
if (!nome.trim() || !email.trim() || !roleId || !senhaInicial) {
|
||||||
if (!matriculaStr || !nome.trim() || !email.trim() || !roleId || !senhaInicial) {
|
|
||||||
mostrarMensagem("error", "Preencha todos os campos obrigatórios");
|
mostrarMensagem("error", "Preencha todos os campos obrigatórios");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -63,11 +63,12 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const resultado = await client.mutation(api.usuarios.criar, {
|
const resultado = await client.mutation(api.usuarios.criar, {
|
||||||
matricula: matriculaStr,
|
|
||||||
nome: nome.trim(),
|
nome: nome.trim(),
|
||||||
email: email.trim(),
|
email: email.trim(),
|
||||||
roleId: roleId as Id<"roles">,
|
roleId: roleId as Id<"roles">,
|
||||||
funcionarioId: funcionarioId ? (funcionarioId as Id<"funcionarios">) : undefined,
|
funcionarioId: funcionarioId
|
||||||
|
? (funcionarioId as Id<"funcionarios">)
|
||||||
|
: undefined,
|
||||||
senhaInicial: senhaInicial,
|
senhaInicial: senhaInicial,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -75,7 +76,7 @@
|
|||||||
if (senhaGerada) {
|
if (senhaGerada) {
|
||||||
mostrarMensagem(
|
mostrarMensagem(
|
||||||
"success",
|
"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(() => {
|
setTimeout(() => {
|
||||||
goto("/ti/usuarios");
|
goto("/ti/usuarios");
|
||||||
@@ -102,17 +103,19 @@
|
|||||||
// Auto-completar ao selecionar funcionário
|
// Auto-completar ao selecionar funcionário
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (funcionarioId && funcionarios?.data) {
|
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) {
|
if (funcSelecionado) {
|
||||||
email = funcSelecionado.email || email;
|
email = funcSelecionado.email || email;
|
||||||
nome = funcSelecionado.nome || nome;
|
nome = funcSelecionado.nome || nome;
|
||||||
matricula = funcSelecionado.matricula || matricula;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function gerarSenhaAleatoria() {
|
function gerarSenhaAleatoria() {
|
||||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$!";
|
const chars =
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$!";
|
||||||
let senha = "";
|
let senha = "";
|
||||||
for (let i = 0; i < 12; i++) {
|
for (let i = 0; i < 12; i++) {
|
||||||
senha += chars.charAt(Math.floor(Math.random() * chars.length));
|
senha += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
@@ -154,8 +157,12 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-3xl font-bold text-base-content">Criar Novo Usuário</h1>
|
<h1 class="text-3xl font-bold text-base-content">
|
||||||
<p class="text-base-content/60 mt-1">Cadastre um novo usuário no sistema</p>
|
Criar Novo Usuário
|
||||||
|
</h1>
|
||||||
|
<p class="text-base-content/60 mt-1">
|
||||||
|
Cadastre um novo usuário no sistema
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a href="/ti/usuarios" class="btn btn-outline btn-primary gap-2">
|
<a href="/ti/usuarios" class="btn btn-outline btn-primary gap-2">
|
||||||
@@ -248,7 +255,9 @@
|
|||||||
<!-- Funcionário (primeiro) -->
|
<!-- Funcionário (primeiro) -->
|
||||||
<div class="form-control md:col-span-2">
|
<div class="form-control md:col-span-2">
|
||||||
<label class="label" for="funcionario">
|
<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>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="funcionario"
|
id="funcionario"
|
||||||
@@ -256,34 +265,24 @@
|
|||||||
bind:value={funcionarioId}
|
bind:value={funcionarioId}
|
||||||
disabled={processando || !funcionarios?.data}
|
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}
|
{#if funcionarios?.data}
|
||||||
{#each funcionarios.data as func}
|
{#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}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</select>
|
</select>
|
||||||
<div class="label">
|
<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>
|
||||||
</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 -->
|
<!-- Nome -->
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="nome">
|
<label class="label" for="nome">
|
||||||
@@ -301,7 +300,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Email -->
|
<!-- Email -->
|
||||||
<div class="form-control md:col-span-2">
|
<div class="form-control">
|
||||||
<label class="label" for="email">
|
<label class="label" for="email">
|
||||||
<span class="label-text font-semibold">E-mail *</span>
|
<span class="label-text font-semibold">E-mail *</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -341,7 +340,9 @@
|
|||||||
</select>
|
</select>
|
||||||
{#if !roles?.data || !Array.isArray(roles.data)}
|
{#if !roles?.data || !Array.isArray(roles.data)}
|
||||||
<div class="label">
|
<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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -446,7 +447,9 @@
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h3 class="font-bold">Senha Gerada:</h3>
|
<h3 class="font-bold">Senha Gerada:</h3>
|
||||||
<div class="flex items-center gap-2 mt-2">
|
<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}
|
{senhaGerada}
|
||||||
</code>
|
</code>
|
||||||
<button
|
<button
|
||||||
@@ -473,8 +476,8 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm mt-2">
|
<p class="text-sm mt-2">
|
||||||
⚠️ <strong>IMPORTANTE:</strong> Anote esta senha! Você precisará repassá-la
|
⚠️ <strong>IMPORTANTE:</strong> Anote esta senha! Você precisará
|
||||||
manualmente ao usuário até que o SMTP seja configurado.
|
repassá-la manualmente ao usuário até que o SMTP seja configurado.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -500,18 +503,27 @@
|
|||||||
<h3 class="font-bold">Informações Importantes</h3>
|
<h3 class="font-bold">Informações Importantes</h3>
|
||||||
<ul class="text-sm list-disc list-inside mt-2 space-y-1">
|
<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>O usuário deverá alterar a senha no primeiro acesso</li>
|
||||||
<li>As credenciais devem ser repassadas manualmente (por enquanto)</li>
|
|
||||||
<li>
|
<li>
|
||||||
Configure o SMTP em <a href="/ti/configuracoes-email" class="link"
|
As credenciais devem ser repassadas manualmente (por enquanto)
|
||||||
>Configurações de Email</a
|
</li>
|
||||||
|
<li>
|
||||||
|
Configure o SMTP em <a
|
||||||
|
href="/ti/configuracoes-email"
|
||||||
|
class="link">Configurações de Email</a
|
||||||
> para envio automático
|
> para envio automático
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-actions justify-end mt-8 pt-6 border-t border-base-300">
|
<div
|
||||||
<a href="/ti/usuarios" class="btn btn-ghost gap-2" class:btn-disabled={processando}>
|
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
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-5 w-5"
|
class="h-5 w-5"
|
||||||
@@ -528,7 +540,11 @@
|
|||||||
</svg>
|
</svg>
|
||||||
Cancelar
|
Cancelar
|
||||||
</a>
|
</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}
|
{#if processando}
|
||||||
<span class="loading loading-spinner loading-sm"></span>
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
Criando Usuário...
|
Criando Usuário...
|
||||||
@@ -556,4 +572,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
|
|
||||||
|
|||||||
@@ -294,12 +294,19 @@ export const login = mutation({
|
|||||||
timestamp: agora,
|
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 {
|
return {
|
||||||
sucesso: true as const,
|
sucesso: true as const,
|
||||||
token,
|
token,
|
||||||
usuario: {
|
usuario: {
|
||||||
_id: usuario._id,
|
_id: usuario._id,
|
||||||
matricula: usuario.matricula,
|
matricula: matricula || "",
|
||||||
nome: usuario.nome,
|
nome: usuario.nome,
|
||||||
email: usuario.email,
|
email: usuario.email,
|
||||||
funcionarioId: usuario.funcionarioId,
|
funcionarioId: usuario.funcionarioId,
|
||||||
@@ -568,12 +575,19 @@ export const loginComIP = internalMutation({
|
|||||||
timestamp: agora,
|
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 {
|
return {
|
||||||
sucesso: true as const,
|
sucesso: true as const,
|
||||||
token,
|
token,
|
||||||
usuario: {
|
usuario: {
|
||||||
_id: usuario._id,
|
_id: usuario._id,
|
||||||
matricula: usuario.matricula,
|
matricula: matricula || "",
|
||||||
nome: usuario.nome,
|
nome: usuario.nome,
|
||||||
email: usuario.email,
|
email: usuario.email,
|
||||||
funcionarioId: usuario.funcionarioId,
|
funcionarioId: usuario.funcionarioId,
|
||||||
@@ -688,11 +702,18 @@ export const verificarSessao = query({
|
|||||||
return { valido: false as const, motivo: "Role não encontrada" };
|
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 {
|
return {
|
||||||
valido: true as const,
|
valido: true as const,
|
||||||
usuario: {
|
usuario: {
|
||||||
_id: usuario._id,
|
_id: usuario._id,
|
||||||
matricula: usuario.matricula,
|
matricula: matricula || "",
|
||||||
nome: usuario.nome,
|
nome: usuario.nome,
|
||||||
email: usuario.email,
|
email: usuario.email,
|
||||||
funcionarioId: usuario.funcionarioId,
|
funcionarioId: usuario.funcionarioId,
|
||||||
|
|||||||
@@ -94,7 +94,9 @@ export const criarConversa = mutation({
|
|||||||
conversaId,
|
conversaId,
|
||||||
remetenteId: usuarioAtual._id,
|
remetenteId: usuarioAtual._id,
|
||||||
titulo: "Adicionado a grupo",
|
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,
|
lida: false,
|
||||||
criadaEm: Date.now(),
|
criadaEm: Date.now(),
|
||||||
});
|
});
|
||||||
@@ -226,7 +228,8 @@ export const enviarMensagem = mutation({
|
|||||||
for (const participanteId of conversa.participantes) {
|
for (const participanteId of conversa.participantes) {
|
||||||
// ✅ MODIFICADO: Permite notificação para si mesmo se flag estiver ativa
|
// ✅ MODIFICADO: Permite notificação para si mesmo se flag estiver ativa
|
||||||
const ehOMesmoUsuario = participanteId === usuarioAtual._id;
|
const ehOMesmoUsuario = participanteId === usuarioAtual._id;
|
||||||
const deveCriarNotificacao = !ehOMesmoUsuario || args.permitirNotificacaoParaSiMesmo;
|
const deveCriarNotificacao =
|
||||||
|
!ehOMesmoUsuario || args.permitirNotificacaoParaSiMesmo;
|
||||||
|
|
||||||
if (deveCriarNotificacao) {
|
if (deveCriarNotificacao) {
|
||||||
const tipoNotificacao = args.mencoes?.includes(participanteId)
|
const tipoNotificacao = args.mencoes?.includes(participanteId)
|
||||||
@@ -318,7 +321,10 @@ export const cancelarMensagemAgendada = mutation({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mensagem.remetenteId !== usuarioAtual._id) {
|
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) {
|
if (!mensagem.agendadaPara) {
|
||||||
@@ -611,7 +617,9 @@ export const listarConversas = query({
|
|||||||
// Para conversas individuais, pegar o outro usuário
|
// Para conversas individuais, pegar o outro usuário
|
||||||
let outroUsuario = null;
|
let outroUsuario = null;
|
||||||
if (conversa.tipo === "individual") {
|
if (conversa.tipo === "individual") {
|
||||||
const outroUsuarioRaw = participantes.find((p) => p?._id !== usuarioAtual._id);
|
const outroUsuarioRaw = participantes.find(
|
||||||
|
(p) => p?._id !== usuarioAtual._id
|
||||||
|
);
|
||||||
if (outroUsuarioRaw) {
|
if (outroUsuarioRaw) {
|
||||||
// 🔄 BUSCAR DADOS ATUALIZADOS DO USUÁRIO (não usar snapshot)
|
// 🔄 BUSCAR DADOS ATUALIZADOS DO USUÁRIO (não usar snapshot)
|
||||||
const usuarioAtualizado = await ctx.db.get(outroUsuarioRaw._id);
|
const usuarioAtualizado = await ctx.db.get(outroUsuarioRaw._id);
|
||||||
@@ -620,7 +628,9 @@ export const listarConversas = query({
|
|||||||
// Adicionar URL da foto de perfil
|
// Adicionar URL da foto de perfil
|
||||||
let fotoPerfilUrl = null;
|
let fotoPerfilUrl = null;
|
||||||
if (usuarioAtualizado.fotoPerfil) {
|
if (usuarioAtualizado.fotoPerfil) {
|
||||||
fotoPerfilUrl = await ctx.storage.getUrl(usuarioAtualizado.fotoPerfil);
|
fotoPerfilUrl = await ctx.storage.getUrl(
|
||||||
|
usuarioAtualizado.fotoPerfil
|
||||||
|
);
|
||||||
}
|
}
|
||||||
outroUsuario = {
|
outroUsuario = {
|
||||||
...usuarioAtualizado,
|
...usuarioAtualizado,
|
||||||
@@ -756,7 +766,16 @@ export const obterMensagensAgendadas = query({
|
|||||||
*/
|
*/
|
||||||
export const listarAgendamentosChat = query({
|
export const listarAgendamentosChat = query({
|
||||||
args: {},
|
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);
|
const usuarioAtual = await getUsuarioAutenticado(ctx);
|
||||||
if (!usuarioAtual) {
|
if (!usuarioAtual) {
|
||||||
return [];
|
return [];
|
||||||
@@ -912,20 +931,31 @@ export const listarTodosUsuarios = query({
|
|||||||
.withIndex("by_ativo", (q) => q.eq("ativo", true))
|
.withIndex("by_ativo", (q) => q.eq("ativo", true))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Excluir o usuário atual
|
// Excluir o usuário atual e buscar matrículas
|
||||||
return usuarios
|
const usuariosComMatricula = await Promise.all(
|
||||||
|
usuarios
|
||||||
.filter((u) => u._id !== usuarioAtual._id)
|
.filter((u) => u._id !== usuarioAtual._id)
|
||||||
.map((u) => ({
|
.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,
|
_id: u._id,
|
||||||
nome: u.nome,
|
nome: u.nome,
|
||||||
email: u.email,
|
email: u.email,
|
||||||
matricula: u.matricula,
|
matricula,
|
||||||
avatar: u.avatar,
|
avatar: u.avatar,
|
||||||
fotoPerfil: u.fotoPerfil,
|
fotoPerfil: u.fotoPerfil,
|
||||||
statusPresenca: u.statusPresenca,
|
statusPresenca: u.statusPresenca,
|
||||||
statusMensagem: u.statusMensagem,
|
statusMensagem: u.statusMensagem,
|
||||||
setor: u.setor,
|
setor: u.setor,
|
||||||
}));
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return usuariosComMatricula;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -88,9 +88,14 @@ export const listar = query({
|
|||||||
if (log.usuarioId) {
|
if (log.usuarioId) {
|
||||||
const user = await ctx.db.get(log.usuarioId);
|
const user = await ctx.db.get(log.usuarioId);
|
||||||
if (user) {
|
if (user) {
|
||||||
|
let matricula: string | undefined = undefined;
|
||||||
|
if (user.funcionarioId) {
|
||||||
|
const funcionario = await ctx.db.get(user.funcionarioId);
|
||||||
|
matricula = funcionario?.matricula;
|
||||||
|
}
|
||||||
usuario = {
|
usuario = {
|
||||||
_id: user._id,
|
_id: user._id,
|
||||||
matricula: user.matricula,
|
matricula: matricula || "",
|
||||||
nome: user.nome,
|
nome: user.nome,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,10 +78,15 @@ export const listarAtividades = query({
|
|||||||
const atividadesComUsuarios = await Promise.all(
|
const atividadesComUsuarios = await Promise.all(
|
||||||
atividades.map(async (atividade) => {
|
atividades.map(async (atividade) => {
|
||||||
const usuario = await ctx.db.get(atividade.usuarioId);
|
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 {
|
return {
|
||||||
...atividade,
|
...atividade,
|
||||||
usuarioNome: usuario?.nome || "Usuário Desconhecido",
|
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(
|
const atividadesComUsuarios = await Promise.all(
|
||||||
atividades.map(async (atividade) => {
|
atividades.map(async (atividade) => {
|
||||||
const usuario = await ctx.db.get(atividade.usuarioId);
|
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 {
|
return {
|
||||||
...atividade,
|
...atividade,
|
||||||
usuarioNome: usuario?.nome || "Usuário Desconhecido",
|
usuarioNome: usuario?.nome || "Usuário Desconhecido",
|
||||||
usuarioMatricula: usuario?.matricula || "N/A",
|
usuarioMatricula: matricula,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -30,18 +30,19 @@ export default defineSchema({
|
|||||||
simboloId: v.id("simbolos"),
|
simboloId: v.id("simbolos"),
|
||||||
simboloTipo: simboloTipo,
|
simboloTipo: simboloTipo,
|
||||||
gestorId: v.optional(v.id("usuarios")),
|
gestorId: v.optional(v.id("usuarios")),
|
||||||
statusFerias: v.optional(v.union(
|
statusFerias: v.optional(
|
||||||
v.literal("ativo"),
|
v.union(v.literal("ativo"), v.literal("em_ferias"))
|
||||||
v.literal("em_ferias")
|
),
|
||||||
)),
|
|
||||||
|
|
||||||
// Regime de trabalho (para cálculo correto de férias)
|
// Regime de trabalho (para cálculo correto de férias)
|
||||||
regimeTrabalho: v.optional(v.union(
|
regimeTrabalho: v.optional(
|
||||||
|
v.union(
|
||||||
v.literal("clt"), // CLT - Consolidação das Leis do Trabalho
|
v.literal("clt"), // CLT - Consolidação das Leis do Trabalho
|
||||||
v.literal("estatutario_pe"), // Servidor Público Estadual de Pernambuco
|
v.literal("estatutario_pe"), // Servidor Público Estadual de Pernambuco
|
||||||
v.literal("estatutario_federal"), // Servidor Público Federal
|
v.literal("estatutario_federal"), // Servidor Público Federal
|
||||||
v.literal("estatutario_municipal") // Servidor Público Municipal
|
v.literal("estatutario_municipal") // Servidor Público Municipal
|
||||||
)),
|
)
|
||||||
|
),
|
||||||
|
|
||||||
// Dados Pessoais Adicionais (opcionais)
|
// Dados Pessoais Adicionais (opcionais)
|
||||||
nomePai: v.optional(v.string()),
|
nomePai: v.optional(v.string()),
|
||||||
@@ -191,10 +192,7 @@ export default defineSchema({
|
|||||||
|
|
||||||
licencas: defineTable({
|
licencas: defineTable({
|
||||||
funcionarioId: v.id("funcionarios"),
|
funcionarioId: v.id("funcionarios"),
|
||||||
tipo: v.union(
|
tipo: v.union(v.literal("maternidade"), v.literal("paternidade")),
|
||||||
v.literal("maternidade"),
|
|
||||||
v.literal("paternidade")
|
|
||||||
),
|
|
||||||
dataInicio: v.string(),
|
dataInicio: v.string(),
|
||||||
dataFim: v.string(),
|
dataFim: v.string(),
|
||||||
documentoId: v.optional(v.id("_storage")),
|
documentoId: v.optional(v.id("_storage")),
|
||||||
@@ -237,11 +235,15 @@ export default defineSchema({
|
|||||||
data: v.number(),
|
data: v.number(),
|
||||||
usuarioId: v.id("usuarios"),
|
usuarioId: v.id("usuarios"),
|
||||||
acao: v.string(),
|
acao: v.string(),
|
||||||
periodosAnteriores: v.optional(v.array(v.object({
|
periodosAnteriores: v.optional(
|
||||||
|
v.array(
|
||||||
|
v.object({
|
||||||
dataInicio: v.string(),
|
dataInicio: v.string(),
|
||||||
dataFim: v.string(),
|
dataFim: v.string(),
|
||||||
diasCorridos: v.number(),
|
diasCorridos: v.number(),
|
||||||
}))),
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@@ -343,7 +345,6 @@ export default defineSchema({
|
|||||||
|
|
||||||
// Sistema de Autenticação e Controle de Acesso
|
// Sistema de Autenticação e Controle de Acesso
|
||||||
usuarios: defineTable({
|
usuarios: defineTable({
|
||||||
matricula: v.string(),
|
|
||||||
senhaHash: v.string(), // Senha criptografada com bcrypt
|
senhaHash: v.string(), // Senha criptografada com bcrypt
|
||||||
nome: v.string(),
|
nome: v.string(),
|
||||||
email: v.string(),
|
email: v.string(),
|
||||||
@@ -380,7 +381,6 @@ export default defineSchema({
|
|||||||
notificacoesAtivadas: v.optional(v.boolean()),
|
notificacoesAtivadas: v.optional(v.boolean()),
|
||||||
somNotificacao: v.optional(v.boolean()),
|
somNotificacao: v.optional(v.boolean()),
|
||||||
})
|
})
|
||||||
.index("by_matricula", ["matricula"])
|
|
||||||
.index("by_email", ["email"])
|
.index("by_email", ["email"])
|
||||||
.index("by_role", ["roleId"])
|
.index("by_role", ["roleId"])
|
||||||
.index("by_ativo", ["ativo"])
|
.index("by_ativo", ["ativo"])
|
||||||
@@ -663,8 +663,7 @@ export default defineSchema({
|
|||||||
mensagensPorMinuto: v.optional(v.number()),
|
mensagensPorMinuto: v.optional(v.number()),
|
||||||
tempoRespostaMedio: v.optional(v.number()),
|
tempoRespostaMedio: v.optional(v.number()),
|
||||||
errosCount: v.optional(v.number()),
|
errosCount: v.optional(v.number()),
|
||||||
})
|
}).index("by_timestamp", ["timestamp"]),
|
||||||
.index("by_timestamp", ["timestamp"]),
|
|
||||||
|
|
||||||
alertConfigurations: defineTable({
|
alertConfigurations: defineTable({
|
||||||
metricName: v.string(),
|
metricName: v.string(),
|
||||||
@@ -681,8 +680,7 @@ export default defineSchema({
|
|||||||
notifyByChat: v.boolean(),
|
notifyByChat: v.boolean(),
|
||||||
createdBy: v.id("usuarios"),
|
createdBy: v.id("usuarios"),
|
||||||
lastModified: v.number(),
|
lastModified: v.number(),
|
||||||
})
|
}).index("by_enabled", ["enabled"]),
|
||||||
.index("by_enabled", ["enabled"]),
|
|
||||||
|
|
||||||
alertHistory: defineTable({
|
alertHistory: defineTable({
|
||||||
configId: v.id("alertConfigurations"),
|
configId: v.id("alertConfigurations"),
|
||||||
|
|||||||
@@ -316,7 +316,6 @@ export const seedDatabase = internalMutation({
|
|||||||
|
|
||||||
const senhaInicial = await hashPassword("Mudar@123");
|
const senhaInicial = await hashPassword("Mudar@123");
|
||||||
await ctx.db.insert("usuarios", {
|
await ctx.db.insert("usuarios", {
|
||||||
matricula: funcionario.matricula,
|
|
||||||
senhaHash: senhaInicial,
|
senhaHash: senhaInicial,
|
||||||
nome: funcionario.nome,
|
nome: funcionario.nome,
|
||||||
email: funcionario.email,
|
email: funcionario.email,
|
||||||
|
|||||||
@@ -4,6 +4,21 @@ import { hashPassword, generateToken } from "./auth/utils";
|
|||||||
import { registrarAtividade } from "./logsAtividades";
|
import { registrarAtividade } from "./logsAtividades";
|
||||||
import { Id, Doc } from "./_generated/dataModel";
|
import { Id, Doc } from "./_generated/dataModel";
|
||||||
import { api } from "./_generated/api";
|
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
|
* Associar funcionário a um usuário
|
||||||
@@ -30,8 +45,11 @@ export const associarFuncionario = mutation({
|
|||||||
.first();
|
.first();
|
||||||
|
|
||||||
if (usuarioExistente && usuarioExistente._id !== args.usuarioId) {
|
if (usuarioExistente && usuarioExistente._id !== args.usuarioId) {
|
||||||
|
const matricula = await obterMatriculaUsuario(ctx, usuarioExistente);
|
||||||
throw new Error(
|
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({
|
export const criar = mutation({
|
||||||
args: {
|
args: {
|
||||||
matricula: v.string(),
|
|
||||||
nome: v.string(),
|
nome: v.string(),
|
||||||
email: v.string(),
|
email: v.string(),
|
||||||
roleId: v.id("roles"),
|
roleId: v.id("roles"),
|
||||||
@@ -78,16 +95,6 @@ export const criar = mutation({
|
|||||||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||||
),
|
),
|
||||||
handler: async (ctx, args) => {
|
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
|
// Verificar se email já existe
|
||||||
const emailExistente = await ctx.db
|
const emailExistente = await ctx.db
|
||||||
.query("usuarios")
|
.query("usuarios")
|
||||||
@@ -103,7 +110,6 @@ export const criar = mutation({
|
|||||||
|
|
||||||
// Criar usuário
|
// Criar usuário
|
||||||
const usuarioId = await ctx.db.insert("usuarios", {
|
const usuarioId = await ctx.db.insert("usuarios", {
|
||||||
matricula: args.matricula,
|
|
||||||
senhaHash,
|
senhaHash,
|
||||||
nome: args.nome,
|
nome: args.nome,
|
||||||
email: args.email,
|
email: args.email,
|
||||||
@@ -194,9 +200,17 @@ export const listar = query({
|
|||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
let usuarios = await ctx.db.query("usuarios").collect();
|
let usuarios = await ctx.db.query("usuarios").collect();
|
||||||
|
|
||||||
// Filtrar por matrícula
|
// Filtrar por matrícula (buscar no funcionário)
|
||||||
if (args.matricula) {
|
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
|
// Filtrar por ativo
|
||||||
@@ -206,7 +220,11 @@ export const listar = query({
|
|||||||
|
|
||||||
// Buscar roles e funcionários
|
// Buscar roles e funcionários
|
||||||
const resultado = [];
|
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) {
|
for (const usuario of usuarios) {
|
||||||
try {
|
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
|
// Se a role não existe, criar uma role de erro mas ainda incluir o usuário
|
||||||
if (!role) {
|
if (!role) {
|
||||||
|
const matricula = await obterMatriculaUsuario(ctx, usuario);
|
||||||
usuariosSemRole.push({
|
usuariosSemRole.push({
|
||||||
nome: usuario.nome,
|
nome: usuario.nome,
|
||||||
matricula: usuario.matricula,
|
matricula: matricula || "N/A",
|
||||||
roleId: usuario.roleId,
|
roleId: usuario.roleId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -240,14 +259,19 @@ export const listar = query({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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)
|
// Criar role de erro (sem _creationTime pois a role não existe)
|
||||||
resultado.push({
|
resultado.push({
|
||||||
_id: usuario._id,
|
_id: usuario._id,
|
||||||
matricula: usuario.matricula,
|
matricula: matriculaUsuario,
|
||||||
nome: usuario.nome,
|
nome: usuario.nome,
|
||||||
email: usuario.email,
|
email: usuario.email,
|
||||||
ativo: usuario.ativo,
|
ativo: usuario.ativo,
|
||||||
@@ -294,7 +318,10 @@ export const listar = query({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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,
|
nome: role.nome,
|
||||||
nivel: role.nivel,
|
nivel: role.nivel,
|
||||||
...(role.criadoPor !== undefined && { criadoPor: role.criadoPor }),
|
...(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.editavel !== undefined && { editavel: role.editavel }),
|
||||||
...(role.setor !== undefined && { setor: role.setor }),
|
...(role.setor !== undefined && { setor: role.setor }),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const matriculaUsuario = await obterMatriculaUsuario(ctx, usuario);
|
||||||
|
|
||||||
resultado.push({
|
resultado.push({
|
||||||
_id: usuario._id,
|
_id: usuario._id,
|
||||||
matricula: usuario.matricula,
|
matricula: matriculaUsuario,
|
||||||
nome: usuario.nome,
|
nome: usuario.nome,
|
||||||
email: usuario.email,
|
email: usuario.email,
|
||||||
ativo: usuario.ativo,
|
ativo: usuario.ativo,
|
||||||
@@ -334,7 +365,12 @@ export const listar = query({
|
|||||||
if (usuariosSemRole.length > 0) {
|
if (usuariosSemRole.length > 0) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`⚠️ Encontrados ${usuariosSemRole.length} usuário(s) com perfil ausente:`,
|
`⚠️ 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
|
// 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.avatar !== undefined) updates.avatar = args.avatar;
|
||||||
if (args.fotoPerfil !== undefined) updates.fotoPerfil = args.fotoPerfil;
|
if (args.fotoPerfil !== undefined) updates.fotoPerfil = args.fotoPerfil;
|
||||||
@@ -591,7 +629,7 @@ export const obterPerfil = query({
|
|||||||
_id: v.id("usuarios"),
|
_id: v.id("usuarios"),
|
||||||
nome: v.string(),
|
nome: v.string(),
|
||||||
email: v.string(),
|
email: v.string(),
|
||||||
matricula: v.string(),
|
matricula: v.optional(v.string()),
|
||||||
funcionarioId: v.optional(v.id("funcionarios")),
|
funcionarioId: v.optional(v.id("funcionarios")),
|
||||||
avatar: v.optional(v.string()),
|
avatar: v.optional(v.string()),
|
||||||
fotoPerfil: v.optional(v.id("_storage")),
|
fotoPerfil: v.optional(v.id("_storage")),
|
||||||
@@ -675,11 +713,13 @@ export const obterPerfil = query({
|
|||||||
fotoPerfilUrl = await ctx.storage.getUrl(usuarioAtual.fotoPerfil);
|
fotoPerfilUrl = await ctx.storage.getUrl(usuarioAtual.fotoPerfil);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const matricula = await obterMatriculaUsuario(ctx, usuarioAtual);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_id: usuarioAtual._id,
|
_id: usuarioAtual._id,
|
||||||
nome: usuarioAtual.nome,
|
nome: usuarioAtual.nome,
|
||||||
email: usuarioAtual.email,
|
email: usuarioAtual.email,
|
||||||
matricula: usuarioAtual.matricula,
|
matricula: matricula || undefined,
|
||||||
funcionarioId: usuarioAtual.funcionarioId,
|
funcionarioId: usuarioAtual.funcionarioId,
|
||||||
avatar: usuarioAtual.avatar,
|
avatar: usuarioAtual.avatar,
|
||||||
fotoPerfil: usuarioAtual.fotoPerfil,
|
fotoPerfil: usuarioAtual.fotoPerfil,
|
||||||
@@ -735,11 +775,13 @@ export const listarParaChat = query({
|
|||||||
fotoPerfilUrl = await ctx.storage.getUrl(usuario.fotoPerfil);
|
fotoPerfilUrl = await ctx.storage.getUrl(usuario.fotoPerfil);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const matricula = await obterMatriculaUsuario(ctx, usuario);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_id: usuario._id,
|
_id: usuario._id,
|
||||||
nome: usuario.nome,
|
nome: usuario.nome,
|
||||||
email: usuario.email,
|
email: usuario.email,
|
||||||
matricula: usuario.matricula || undefined,
|
matricula: matricula || undefined,
|
||||||
avatar: usuario.avatar,
|
avatar: usuario.avatar,
|
||||||
fotoPerfil: usuario.fotoPerfil,
|
fotoPerfil: usuario.fotoPerfil,
|
||||||
fotoPerfilUrl,
|
fotoPerfilUrl,
|
||||||
@@ -1035,7 +1077,6 @@ export const editarUsuario = mutation({
|
|||||||
*/
|
*/
|
||||||
export const criarAdminMaster = mutation({
|
export const criarAdminMaster = mutation({
|
||||||
args: {
|
args: {
|
||||||
matricula: v.string(),
|
|
||||||
nome: v.string(),
|
nome: v.string(),
|
||||||
email: v.string(),
|
email: v.string(),
|
||||||
senha: v.optional(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 senhaTemporaria = args.senha || gerarSenhaTemporaria();
|
||||||
const senhaHash = await hashPassword(senhaTemporaria);
|
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
|
// Verificar se email já existe
|
||||||
const existentePorEmail = await ctx.db
|
const existentePorEmail = await ctx.db
|
||||||
.query("usuarios")
|
.query("usuarios")
|
||||||
@@ -1108,7 +1126,6 @@ export const criarAdminMaster = mutation({
|
|||||||
if (existentePorEmail) {
|
if (existentePorEmail) {
|
||||||
// Promove usuário existente por email
|
// Promove usuário existente por email
|
||||||
await ctx.db.patch(existentePorEmail._id, {
|
await ctx.db.patch(existentePorEmail._id, {
|
||||||
matricula: args.matricula,
|
|
||||||
nome: args.nome,
|
nome: args.nome,
|
||||||
senhaHash,
|
senhaHash,
|
||||||
roleId: roleTIMaster._id,
|
roleId: roleTIMaster._id,
|
||||||
@@ -1125,7 +1142,6 @@ export const criarAdminMaster = mutation({
|
|||||||
|
|
||||||
// Criar novo usuário TI Master
|
// Criar novo usuário TI Master
|
||||||
const usuarioId = await ctx.db.insert("usuarios", {
|
const usuarioId = await ctx.db.insert("usuarios", {
|
||||||
matricula: args.matricula,
|
|
||||||
senhaHash,
|
senhaHash,
|
||||||
nome: args.nome,
|
nome: args.nome,
|
||||||
email: args.email,
|
email: args.email,
|
||||||
@@ -1194,7 +1210,6 @@ export const excluirUsuarioLogico = mutation({
|
|||||||
*/
|
*/
|
||||||
export const criarUsuarioCompleto = mutation({
|
export const criarUsuarioCompleto = mutation({
|
||||||
args: {
|
args: {
|
||||||
matricula: v.string(),
|
|
||||||
nome: v.string(),
|
nome: v.string(),
|
||||||
email: v.string(),
|
email: v.string(),
|
||||||
roleId: v.id("roles"),
|
roleId: v.id("roles"),
|
||||||
@@ -1212,16 +1227,6 @@ export const criarUsuarioCompleto = mutation({
|
|||||||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||||
),
|
),
|
||||||
handler: async (ctx, args) => {
|
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
|
// Verificar se email já existe
|
||||||
const emailExistente = await ctx.db
|
const emailExistente = await ctx.db
|
||||||
.query("usuarios")
|
.query("usuarios")
|
||||||
@@ -1238,7 +1243,6 @@ export const criarUsuarioCompleto = mutation({
|
|||||||
|
|
||||||
// Criar usuário
|
// Criar usuário
|
||||||
const usuarioId = await ctx.db.insert("usuarios", {
|
const usuarioId = await ctx.db.insert("usuarios", {
|
||||||
matricula: args.matricula,
|
|
||||||
senhaHash,
|
senhaHash,
|
||||||
nome: args.nome,
|
nome: args.nome,
|
||||||
email: args.email,
|
email: args.email,
|
||||||
@@ -1256,7 +1260,7 @@ export const criarUsuarioCompleto = mutation({
|
|||||||
args.criadoPorId,
|
args.criadoPorId,
|
||||||
"criar",
|
"criar",
|
||||||
"usuarios",
|
"usuarios",
|
||||||
JSON.stringify({ usuarioId, matricula: args.matricula, nome: args.nome }),
|
JSON.stringify({ usuarioId, nome: args.nome }),
|
||||||
usuarioId
|
usuarioId
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1272,7 +1276,6 @@ export const criarUsuarioCompleto = mutation({
|
|||||||
*/
|
*/
|
||||||
export const criarAdminPadrao = mutation({
|
export const criarAdminPadrao = mutation({
|
||||||
args: {
|
args: {
|
||||||
matricula: v.optional(v.string()),
|
|
||||||
nome: v.optional(v.string()),
|
nome: v.optional(v.string()),
|
||||||
email: v.optional(v.string()),
|
email: v.optional(v.string()),
|
||||||
senha: v.optional(v.string()),
|
senha: v.optional(v.string()),
|
||||||
@@ -1282,7 +1285,6 @@ export const criarAdminPadrao = mutation({
|
|||||||
usuarioId: v.optional(v.id("usuarios")),
|
usuarioId: v.optional(v.id("usuarios")),
|
||||||
}),
|
}),
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
const matricula = args.matricula ?? "0000";
|
|
||||||
const nome = args.nome ?? "Administrador Geral";
|
const nome = args.nome ?? "Administrador Geral";
|
||||||
const email = args.email ?? "admin@sgse.pe.gov.br";
|
const email = args.email ?? "admin@sgse.pe.gov.br";
|
||||||
const senha = args.senha ?? "Admin@123";
|
const senha = args.senha ?? "Admin@123";
|
||||||
@@ -1306,12 +1308,7 @@ export const criarAdminPadrao = mutation({
|
|||||||
|
|
||||||
if (!roleAdmin) return { sucesso: false };
|
if (!roleAdmin) return { sucesso: false };
|
||||||
|
|
||||||
// Verificar se já existe por matrícula ou email
|
// Verificar se já existe por email
|
||||||
const existentePorMatricula = await ctx.db
|
|
||||||
.query("usuarios")
|
|
||||||
.withIndex("by_matricula", (q) => q.eq("matricula", matricula))
|
|
||||||
.first();
|
|
||||||
|
|
||||||
const existentePorEmail = await ctx.db
|
const existentePorEmail = await ctx.db
|
||||||
.query("usuarios")
|
.query("usuarios")
|
||||||
.withIndex("by_email", (q) => q.eq("email", email))
|
.withIndex("by_email", (q) => q.eq("email", email))
|
||||||
@@ -1319,10 +1316,8 @@ export const criarAdminPadrao = mutation({
|
|||||||
|
|
||||||
const senhaHash = await hashPassword(senha);
|
const senhaHash = await hashPassword(senha);
|
||||||
|
|
||||||
if (existentePorMatricula || existentePorEmail) {
|
if (existentePorEmail) {
|
||||||
const alvo = existentePorMatricula ?? existentePorEmail!;
|
await ctx.db.patch(existentePorEmail._id, {
|
||||||
await ctx.db.patch(alvo._id, {
|
|
||||||
matricula,
|
|
||||||
nome,
|
nome,
|
||||||
email,
|
email,
|
||||||
senhaHash,
|
senhaHash,
|
||||||
@@ -1331,11 +1326,10 @@ export const criarAdminPadrao = mutation({
|
|||||||
primeiroAcesso: false,
|
primeiroAcesso: false,
|
||||||
atualizadoEm: Date.now(),
|
atualizadoEm: Date.now(),
|
||||||
});
|
});
|
||||||
return { sucesso: true, usuarioId: alvo._id };
|
return { sucesso: true, usuarioId: existentePorEmail._id };
|
||||||
}
|
}
|
||||||
|
|
||||||
const usuarioId = await ctx.db.insert("usuarios", {
|
const usuarioId = await ctx.db.insert("usuarios", {
|
||||||
matricula,
|
|
||||||
senhaHash,
|
senhaHash,
|
||||||
nome,
|
nome,
|
||||||
email,
|
email,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { v } from "convex/values";
|
|||||||
import { Id, Doc } from "./_generated/dataModel";
|
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({
|
export const verificarDuplicatas = query({
|
||||||
args: {},
|
args: {},
|
||||||
@@ -23,18 +23,27 @@ export const verificarDuplicatas = query({
|
|||||||
handler: async (ctx) => {
|
handler: async (ctx) => {
|
||||||
const usuarios = await ctx.db.query("usuarios").collect();
|
const usuarios = await ctx.db.query("usuarios").collect();
|
||||||
|
|
||||||
// Agrupar por matrícula
|
// Agrupar por matrícula do funcionário associado
|
||||||
const gruposPorMatricula = usuarios.reduce((acc, usuario) => {
|
const gruposPorMatricula: Record<string, Array<{ _id: Id<"usuarios">; nome: string; email: string }>> = {};
|
||||||
if (!acc[usuario.matricula]) {
|
|
||||||
acc[usuario.matricula] = [];
|
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({
|
|
||||||
|
if (matricula) {
|
||||||
|
if (!gruposPorMatricula[matricula]) {
|
||||||
|
gruposPorMatricula[matricula] = [];
|
||||||
|
}
|
||||||
|
gruposPorMatricula[matricula].push({
|
||||||
_id: usuario._id,
|
_id: usuario._id,
|
||||||
nome: usuario.nome,
|
nome: usuario.nome,
|
||||||
email: usuario.email || "",
|
email: usuario.email || "",
|
||||||
});
|
});
|
||||||
return acc;
|
}
|
||||||
}, {} as Record<string, Array<{ _id: Id<"usuarios">; nome: string; email: string }>>);
|
}
|
||||||
|
|
||||||
// Filtrar apenas duplicatas
|
// Filtrar apenas duplicatas
|
||||||
const duplicatas = Object.entries(gruposPorMatricula)
|
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({
|
export const removerDuplicatas = internalMutation({
|
||||||
args: {},
|
args: {},
|
||||||
@@ -61,14 +70,23 @@ export const removerDuplicatas = internalMutation({
|
|||||||
handler: async (ctx) => {
|
handler: async (ctx) => {
|
||||||
const usuarios = await ctx.db.query("usuarios").collect();
|
const usuarios = await ctx.db.query("usuarios").collect();
|
||||||
|
|
||||||
// Agrupar por matrícula
|
// Agrupar por matrícula do funcionário associado
|
||||||
const gruposPorMatricula = usuarios.reduce((acc, usuario) => {
|
const gruposPorMatricula: Record<string, Doc<"usuarios">[]> = {};
|
||||||
if (!acc[usuario.matricula]) {
|
|
||||||
acc[usuario.matricula] = [];
|
for (const usuario of usuarios) {
|
||||||
|
let matricula: string | undefined = undefined;
|
||||||
|
if (usuario.funcionarioId) {
|
||||||
|
const funcionario = await ctx.db.get(usuario.funcionarioId);
|
||||||
|
matricula = funcionario?.matricula;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matricula) {
|
||||||
|
if (!gruposPorMatricula[matricula]) {
|
||||||
|
gruposPorMatricula[matricula] = [];
|
||||||
|
}
|
||||||
|
gruposPorMatricula[matricula].push(usuario);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
acc[usuario.matricula].push(usuario);
|
|
||||||
return acc;
|
|
||||||
}, {} as Record<string, Doc<"usuarios">[]>);
|
|
||||||
|
|
||||||
let removidos = 0;
|
let removidos = 0;
|
||||||
const matriculasDuplicadas: string[] = [];
|
const matriculasDuplicadas: string[] = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user