Compare commits

...

1 Commits

Author SHA1 Message Date
8167a407e7 refactor: update StatsCard component and improve page layouts
- Changed the icon prop type in StatsCard from optional string to any for better flexibility with lucide-svelte components.
- Enhanced the layout of various pages by improving indentation and formatting for better readability.
- Updated the usage of lucide-svelte icons across multiple components, ensuring a consistent and modern UI.
- Refactored the handling of derived states and queries for improved performance and clarity in the codebase.
2025-11-04 15:28:13 -03:00
7 changed files with 1013 additions and 641 deletions

View File

@@ -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>

View File

@@ -4,19 +4,23 @@
let abaAtiva = $state<"atividades" | "logins">("atividades"); let abaAtiva = $state<"atividades" | "logins">("atividades");
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,27 +246,52 @@
<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>
<option value={200}>200 registros</option> <option value={200}>200 registros</option>
</select> </select>
</div> </div>
<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>

View File

@@ -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>

View File

@@ -53,13 +53,14 @@
let emailIdsRastreados = $state<Set<string>>(new Set()); let emailIdsRastreados = $state<Set<string>>(new Set());
// Query para buscar status dos emails // Query para buscar status dos emails
const emailIdsArray = $derived( const emailsStatusQuery = useQuery(api.email.buscarEmailsPorIds, () => {
Array.from(emailIdsRastreados).map((id) => id as Id<"notificacoesEmail">), const ids = Array.from(emailIdsRastreados).map(
); (id) => id as Id<"notificacoesEmail">,
const emailsStatusQuery = useQuery( );
api.email.buscarEmailsPorIds, return ids.length > 0
emailIdsArray.length > 0 ? { emailIds: emailIdsArray } : undefined, ? { emailIds: ids }
); : { emailIds: [] as Id<"notificacoesEmail">[] };
});
// Queries para agendamentos // Queries para agendamentos
const agendamentosEmailQuery = useQuery( const agendamentosEmailQuery = useQuery(
@@ -727,12 +728,12 @@
"enviando", "enviando",
"Criando/buscando conversa...", "Criando/buscando conversa...",
); );
const conversaResult = await client.mutation( const conversaId = await client.mutation(
api.chat.criarOuBuscarConversaIndividual, api.chat.criarOuBuscarConversaIndividual,
{ outroUsuarioId: destinatario._id as Id<"usuarios"> }, { outroUsuarioId: destinatario._id as Id<"usuarios"> },
); );
if (conversaResult.conversaId) { if (conversaId) {
const mensagem = usarTemplate const mensagem = usarTemplate
? templateSelecionado?.corpo || "" ? templateSelecionado?.corpo || ""
: mensagemPersonalizada; : mensagemPersonalizada;
@@ -748,7 +749,7 @@
resultadoChat = await client.mutation( resultadoChat = await client.mutation(
api.chat.agendarMensagem, api.chat.agendarMensagem,
{ {
conversaId: conversaResult.conversaId, conversaId: conversaId,
conteudo: mensagem, conteudo: mensagem,
agendadaPara: agendadaPara, agendadaPara: agendadaPara,
}, },
@@ -773,7 +774,7 @@
"Enviando mensagem...", "Enviando mensagem...",
); );
resultadoChat = await client.mutation(api.chat.enviarMensagem, { resultadoChat = await client.mutation(api.chat.enviarMensagem, {
conversaId: conversaResult.conversaId, conversaId: conversaId,
conteudo: mensagem, conteudo: mensagem,
tipo: "texto", tipo: "texto",
permitirNotificacaoParaSiMesmo: true, permitirNotificacaoParaSiMesmo: true,
@@ -813,7 +814,7 @@
); );
if (usarTemplate && templateId) { if (usarTemplate && templateId) {
const template = templateSelecionado; const template = templateSelecionado;
if (template) { if (template && authStore.usuario) {
resultadoEmail = await client.mutation( resultadoEmail = await client.mutation(
api.email.enviarEmailComTemplate, api.email.enviarEmailComTemplate,
{ {
@@ -822,7 +823,7 @@
templateCodigo: template.codigo, templateCodigo: template.codigo,
variaveis: { variaveis: {
nome: destinatario.nome, nome: destinatario.nome,
matricula: destinatario.matricula, matricula: destinatario.matricula || "",
}, },
enviadoPorId: authStore.usuario._id as Id<"usuarios">, enviadoPorId: authStore.usuario._id as Id<"usuarios">,
agendadaPara: agendadaPara, agendadaPara: agendadaPara,
@@ -868,6 +869,15 @@
); );
} }
} else { } else {
if (!authStore.usuario) {
adicionarLog(
"email",
destinatario.nome,
"erro",
"Usuário não autenticado",
);
return;
}
resultadoEmail = await client.mutation( resultadoEmail = await client.mutation(
api.email.enfileirarEmail, api.email.enfileirarEmail,
{ {
@@ -981,12 +991,12 @@
"enviando", "enviando",
"Processando...", "Processando...",
); );
const conversaResult = await client.mutation( const conversaId = await client.mutation(
api.chat.criarOuBuscarConversaIndividual, api.chat.criarOuBuscarConversaIndividual,
{ outroUsuarioId: destinatario._id as Id<"usuarios"> }, { outroUsuarioId: destinatario._id as Id<"usuarios"> },
); );
if (conversaResult.conversaId) { if (conversaId) {
// Para templates, usar corpo direto (o backend já faz substituição via email) // Para templates, usar corpo direto (o backend já faz substituição via email)
// Para mensagem personalizada, usar diretamente // Para mensagem personalizada, usar diretamente
const mensagem = usarTemplate const mensagem = usarTemplate
@@ -995,7 +1005,7 @@
if (agendadaPara) { if (agendadaPara) {
await client.mutation(api.chat.agendarMensagem, { await client.mutation(api.chat.agendarMensagem, {
conversaId: conversaResult.conversaId, conversaId: conversaId,
conteudo: mensagem, conteudo: mensagem,
agendadaPara: agendadaPara, agendadaPara: agendadaPara,
}); });
@@ -1012,7 +1022,7 @@
); );
} else { } else {
await client.mutation(api.chat.enviarMensagem, { await client.mutation(api.chat.enviarMensagem, {
conversaId: conversaResult.conversaId, conversaId: conversaId,
conteudo: mensagem, conteudo: mensagem,
tipo: "texto", tipo: "texto",
permitirNotificacaoParaSiMesmo: true, permitirNotificacaoParaSiMesmo: true,
@@ -1063,7 +1073,7 @@
); );
if (usarTemplate && templateId) { if (usarTemplate && templateId) {
const template = templateSelecionado; const template = templateSelecionado;
if (template) { if (template && authStore.usuario) {
const resultadoEmail = await client.mutation( const resultadoEmail = await client.mutation(
api.email.enviarEmailComTemplate, api.email.enviarEmailComTemplate,
{ {
@@ -1121,6 +1131,16 @@
falhasEmail++; falhasEmail++;
} }
} else { } else {
if (!authStore.usuario) {
adicionarLog(
"email",
destinatario.nome,
"erro",
"Usuário não autenticado",
);
falhasEmail++;
continue;
}
const resultadoEmail = await client.mutation( const resultadoEmail = await client.mutation(
api.email.enfileirarEmail, api.email.enfileirarEmail,
{ {
@@ -1376,12 +1396,12 @@
{/if} {/if}
</select> </select>
{#if enviarParaTodos} {#if enviarParaTodos}
<label class="label"> <div class="label">
<span class="label-text-alt text-warning"> <span class="label-text-alt text-warning">
⚠️ A notificação será enviada para todos os {usuarios.length} usuários ⚠️ A notificação será enviada para todos os {usuarios.length} usuários
do sistema do sistema
</span> </span>
</label> </div>
{/if} {/if}
</div> </div>
@@ -1623,9 +1643,9 @@
<!-- Terminal de Status --> <!-- Terminal de Status -->
<div class="mt-4"> <div class="mt-4">
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between mb-2">
<label class="label"> <div class="label">
<span class="label-text font-medium">Terminal de Status</span> <span class="label-text font-medium">Terminal de Status</span>
</label> </div>
{#if logsEnvio.length > 0} {#if logsEnvio.length > 0}
<button <button
type="button" type="button"
@@ -2109,99 +2129,104 @@
<div class="space-y-4"> <div class="space-y-4">
<!-- Código --> <!-- Código -->
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label" for="codigo-template">
<span class="label-text font-medium">Código *</span> <span class="label-text font-medium">Código *</span>
<span class="label-text-alt">Ex: AVISO_IMPORTANTE</span> <span class="label-text-alt">Ex: AVISO_IMPORTANTE</span>
</label> </label>
<input <input
id="codigo-template"
type="text" type="text"
bind:value={codigoTemplate} bind:value={codigoTemplate}
placeholder="CODIGO_TEMPLATE" placeholder="CODIGO_TEMPLATE"
class="input input-bordered" class="input input-bordered"
maxlength="50" maxlength="50"
/> />
<label class="label"> <div class="label">
<span class="label-text-alt" <span class="label-text-alt"
>Código único para identificar o template (será convertido para >Código único para identificar o template (será convertido para
MAIÚSCULAS)</span MAIÚSCULAS)</span
> >
</label> </div>
</div> </div>
<!-- Nome --> <!-- Nome -->
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label" for="nome-template">
<span class="label-text font-medium">Nome *</span> <span class="label-text font-medium">Nome *</span>
</label> </label>
<input <input
id="nome-template"
type="text" type="text"
bind:value={nomeTemplate} bind:value={nomeTemplate}
placeholder="Nome do Template" placeholder="Nome do Template"
class="input input-bordered" class="input input-bordered"
maxlength="100" maxlength="100"
/> />
<label class="label"> <div class="label">
<span class="label-text-alt" <span class="label-text-alt"
>Nome exibido na lista de templates</span >Nome exibido na lista de templates</span
> >
</label> </div>
</div> </div>
<!-- Título --> <!-- Título -->
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label" for="titulo-template">
<span class="label-text font-medium">Título *</span> <span class="label-text font-medium">Título *</span>
</label> </label>
<input <input
id="titulo-template"
type="text" type="text"
bind:value={tituloTemplate} bind:value={tituloTemplate}
placeholder="Título da Mensagem" placeholder="Título da Mensagem"
class="input input-bordered" class="input input-bordered"
maxlength="200" maxlength="200"
/> />
<label class="label"> <div class="label">
<span class="label-text-alt" <span class="label-text-alt"
>Título usado no assunto do email ou cabeçalho da mensagem</span >Título usado no assunto do email ou cabeçalho da mensagem</span
> >
</label> </div>
</div> </div>
<!-- Corpo --> <!-- Corpo -->
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label" for="corpo-template">
<span class="label-text font-medium">Corpo da Mensagem *</span> <span class="label-text font-medium">Corpo da Mensagem *</span>
</label> </label>
<textarea <textarea
id="corpo-template"
bind:value={corpoTemplate} bind:value={corpoTemplate}
placeholder="Digite o conteúdo da mensagem..." placeholder="Digite o conteúdo da mensagem..."
class="textarea textarea-bordered h-32" class="textarea textarea-bordered h-32"
maxlength="2000" maxlength="2000"
></textarea> ></textarea>
<label class="label"> <div class="label">
<span class="label-text-alt" <span class="label-text-alt"
>Use {"{{"}variavel{"}}"} para variáveis dinâmicas (ex: {"{{"}nome{"}}"}, >Use {"{{"}variavel{"}}"} para variáveis dinâmicas (ex: {"{{"}nome{"}}"},
{"{{"}data{"}}"})</span {"{{"}data{"}}"})</span
> >
</label> </div>
</div> </div>
<!-- Variáveis --> <!-- Variáveis -->
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label" for="variaveis-template">
<span class="label-text font-medium">Variáveis (opcional)</span> <span class="label-text font-medium">Variáveis (opcional)</span>
</label> </label>
<input <input
id="variaveis-template"
type="text" type="text"
bind:value={variaveisTemplate} bind:value={variaveisTemplate}
placeholder="nome, data, valor (separadas por vírgula)" placeholder="nome, data, valor (separadas por vírgula)"
class="input input-bordered" class="input input-bordered"
/> />
<label class="label"> <div class="label">
<span class="label-text-alt" <span class="label-text-alt"
>Liste as variáveis que podem ser substituídas no template >Liste as variáveis que podem ser substituídas no template
(separadas por vírgula ou espaço)</span (separadas por vírgula ou espaço)</span
> >
</label> </div>
</div> </div>
</div> </div>
@@ -2241,6 +2266,16 @@
</button> </button>
</div> </div>
</div> </div>
<div class="modal-backdrop" onclick={fecharModalNovoTemplate}></div> <div
class="modal-backdrop"
role="button"
tabindex="0"
onclick={fecharModalNovoTemplate}
onkeydown={(e) => {
if (e.key === "Enter" || e.key === " ") {
fecharModalNovoTemplate();
}
}}
></div>
</div> </div>
{/if} {/if}

View File

@@ -2,40 +2,51 @@
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, {});
// Verificar se está carregando // Verificar se está carregando
const carregando = $derived(usuariosQuery === undefined); const carregando = $derived(usuariosQuery === undefined);
// Extrair dados dos usuários // Extrair dados dos usuários
const usuarios = $derived(usuariosQuery?.data ?? []); const usuarios = $derived(usuariosQuery?.data ?? []);
// Estatísticas derivadas // Estatísticas derivadas
const stats = $derived.by(() => { const stats = $derived.by(() => {
// Se ainda está carregando, retorna null para mostrar loading // Se ainda está carregando, retorna null para mostrar loading
if (carregando) return null; if (carregando) return null;
// Se não há usuários, retorna stats zeradas (mas não null para não mostrar loading) // Se não há usuários, retorna stats zeradas (mas não null para não mostrar loading)
if (!Array.isArray(usuarios) || usuarios.length === 0) { if (!Array.isArray(usuarios) || usuarios.length === 0) {
return { return {
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">
<StatsCard <!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
title="Total de Usuários" <StatsCard
value={stats.total} title="Total de Usuários"
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" />' value={stats.total}
icon={Users}
color="primary" color="primary"
/> />
<StatsCard <!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
title="Usuários Ativos" <StatsCard
value={stats.ativos} title="Usuários Ativos"
description="{stats.total > 0 ? ((stats.ativos / stats.total) * 100).toFixed(1) + '% do total' : '0% do total'}" value={stats.ativos}
icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />' description={`${stats.total > 0 ? ((stats.ativos / stats.total) * 100).toFixed(1) + "% do total" : "0% do total"}`}
icon={CheckCircle}
color="success" color="success"
/> />
<StatsCard <!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
title="Usuários Bloqueados" <StatsCard
value={stats.bloqueados} title="Usuários 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"
/> />
<StatsCard <!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
title="Usuários Inativos" <StatsCard
value={stats.inativos} title="Usuários 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>

View File

@@ -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">;
@@ -38,12 +53,12 @@
// Estatísticas // Estatísticas
const stats = $derived.by(() => { const stats = $derived.by(() => {
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">
<StatsCard <!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
title="Total de Perfis" <StatsCard
value={stats.total} title="Total de Perfis"
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" />' value={stats.total}
icon={Users}
color="primary" color="primary"
/> />
<StatsCard <!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
title="Nível Máximo" <StatsCard
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"
/> />
<StatsCard <!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
title="Nível Alto" <StatsCard
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"
/> />
<StatsCard <!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
title="Nível Médio" <StatsCard
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"
/> />
<StatsCard <!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
title="Com Setor" <StatsCard
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" <X class="h-4 w-4" />
stroke="currentColor"
>
<path
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" <AlertCircle class="h-16 w-16 text-base-content/30 mb-4" />
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<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" <Building2 class="h-4 w-4 text-base-content/40" />
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
/>
</svg>
<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" <button
class="h-4 w-4 mr-1" class="btn btn-sm btn-primary btn-outline"
fill="none" onclick={(e) => {
viewBox="0 0 24 24" e.stopPropagation();
stroke="currentColor" abrirDetalhes(role);
> }}
<path >
stroke-linecap="round" <Eye class="h-4 w-4 mr-1" />
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>
</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>

View File

@@ -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";
@@ -30,10 +46,10 @@
}; };
const client = useConvexClient(); const client = useConvexClient();
// Queries // Queries
const solicitacoesQuery = useQuery(api.solicitacoesAcesso.getAll, {}); const solicitacoesQuery = useQuery(api.solicitacoesAcesso.getAll, {});
// Estados // Estados
let filtroStatus = $state<FiltroStatus>("todos"); let filtroStatus = $state<FiltroStatus>("todos");
let busca = $state(""); let busca = $state("");
@@ -50,15 +66,17 @@
if (solicitacoesQuery === undefined || solicitacoesQuery === null) { if (solicitacoesQuery === undefined || solicitacoesQuery === null) {
return []; return [];
} }
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)) {
return solicitacoesQuery; return solicitacoesQuery;
} }
return []; return [];
}); });
@@ -69,17 +87,23 @@
// Estatísticas // Estatísticas
const stats = $derived.by(() => { const stats = $derived.by(() => {
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.nome.toLowerCase().includes(termo) || (s) =>
s.matricula.toLowerCase().includes(termo) || s.nome.toLowerCase().includes(termo) ||
s.email.toLowerCase().includes(termo) s.matricula.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";
} }
@@ -204,13 +231,14 @@
}; };
fecharAprovar(); fecharAprovar();
// Limpar mensagem após 3 segundos // Limpar mensagem após 3 segundos
setTimeout(() => { setTimeout(() => {
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,
@@ -238,13 +266,14 @@
}; };
fecharRejeitar(); fecharRejeitar();
// Limpar mensagem após 3 segundos // Limpar mensagem após 3 segundos
setTimeout(() => { setTimeout(() => {
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">
<StatsCard <!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
title="Total de Solicitações" <StatsCard
value={stats.total} title="Total de Solicitações"
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" />' value={stats.total}
icon={FileText}
color="primary" color="primary"
/> />
<StatsCard <!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
title="Pendentes" <StatsCard
value={stats.pendentes} title="Pendentes"
description="{stats.total > 0 ? ((stats.pendentes / stats.total) * 100).toFixed(1) + '% do total' : '0% do total'}" value={stats.pendentes}
icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />' description={`${stats.total > 0 ? ((stats.pendentes / stats.total) * 100).toFixed(1) + "% do total" : "0% do total"}`}
icon={Clock}
color="warning" color="warning"
/> />
<StatsCard <!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
title="Aprovadas" <StatsCard
value={stats.aprovadas} title="Aprovadas"
description="{stats.total > 0 ? ((stats.aprovadas / stats.total) * 100).toFixed(1) + '% do total' : '0% do total'}" value={stats.aprovadas}
icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />' description={`${stats.total > 0 ? ((stats.aprovadas / stats.total) * 100).toFixed(1) + "% do total" : "0% do total"}`}
icon={CheckCircle}
color="success" color="success"
/> />
<StatsCard <!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
title="Rejeitadas" <StatsCard
value={stats.rejeitadas} title="Rejeitadas"
description="{stats.total > 0 ? ((stats.rejeitadas / stats.total) * 100).toFixed(1) + '% do total' : '0% do total'}" value={stats.rejeitadas}
icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />' description={`${stats.total > 0 ? ((stats.rejeitadas / stats.total) * 100).toFixed(1) + "% do total" : "0% do total"}`}
icon={XCircle}
color="error" color="error"
/> />
</div> </div>
@@ -333,27 +367,27 @@
<div class="card-body"> <div class="card-body">
<!-- Tabs de Status --> <!-- Tabs de Status -->
<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,81 +439,87 @@
{: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>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<button <button
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>
{#if solicitacao.status === "pendente"} {#if solicitacao.status === "pendente"}
<button <button
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>
<button <button
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}
@@ -494,76 +536,92 @@
<dialog class="modal modal-open"> <dialog class="modal modal-open">
<div class="modal-box max-w-2xl"> <div class="modal-box max-w-2xl">
<h3 class="font-bold text-2xl mb-4">Detalhes da Solicitação</h3> <h3 class="font-bold text-2xl mb-4">Detalhes da Solicitação</h3>
<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>
</div> </div>
{#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>
</div> </div>
{/if} {/if}
</div> </div>
{#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>
</div> </div>
{/if} {/if}
</div> </div>
<div class="modal-action"> <div class="modal-action">
<button class="btn" onclick={fecharDetalhes}>Fechar</button> <button class="btn" onclick={fecharDetalhes}>Fechar</button>
</div> </div>
@@ -579,20 +637,25 @@
<dialog class="modal modal-open"> <dialog class="modal modal-open">
<div class="modal-box"> <div class="modal-box">
<h3 class="font-bold text-2xl mb-4">Aprovar Solicitação</h3> <h3 class="font-bold text-2xl mb-4">Aprovar Solicitação</h3>
<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..."
@@ -600,17 +663,17 @@
rows="3" rows="3"
></textarea> ></textarea>
</div> </div>
<div class="modal-action"> <div class="modal-action">
<button <button
class="btn btn-ghost" class="btn btn-ghost"
onclick={fecharAprovar} onclick={fecharAprovar}
disabled={processando} disabled={processando}
> >
Cancelar Cancelar
</button> </button>
<button <button
class="btn btn-success" class="btn btn-success"
onclick={aprovarSolicitacao} onclick={aprovarSolicitacao}
disabled={processando} disabled={processando}
> >
@@ -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>
@@ -637,20 +698,25 @@
<dialog class="modal modal-open"> <dialog class="modal modal-open">
<div class="modal-box"> <div class="modal-box">
<h3 class="font-bold text-2xl mb-4">Rejeitar Solicitação</h3> <h3 class="font-bold text-2xl mb-4">Rejeitar Solicitação</h3>
<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..."
@@ -658,17 +724,17 @@
rows="3" rows="3"
></textarea> ></textarea>
</div> </div>
<div class="modal-action"> <div class="modal-action">
<button <button
class="btn btn-ghost" class="btn btn-ghost"
onclick={fecharRejeitar} onclick={fecharRejeitar}
disabled={processando} disabled={processando}
> >
Cancelar Cancelar
</button> </button>
<button <button
class="btn btn-error" class="btn btn-error"
onclick={rejeitarSolicitacao} onclick={rejeitarSolicitacao}
disabled={processando} disabled={processando}
> >
@@ -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>