Compare commits
1 Commits
feat-novo-
...
fix-notifi
| Author | SHA1 | Date | |
|---|---|---|---|
| 8167a407e7 |
@@ -2,25 +2,38 @@
|
||||
interface Props {
|
||||
title: string;
|
||||
value: string | number;
|
||||
icon?: string;
|
||||
icon?: any; // Componente do lucide-svelte
|
||||
trend?: {
|
||||
value: number;
|
||||
isPositive: boolean;
|
||||
};
|
||||
description?: string;
|
||||
color?: "primary" | "secondary" | "accent" | "success" | "warning" | "error";
|
||||
color?:
|
||||
| "primary"
|
||||
| "secondary"
|
||||
| "accent"
|
||||
| "success"
|
||||
| "warning"
|
||||
| "error"
|
||||
| "info";
|
||||
}
|
||||
|
||||
let { title, value, icon, trend, description, color = "primary" }: Props = $props();
|
||||
let {
|
||||
title,
|
||||
value,
|
||||
icon: Icon,
|
||||
trend,
|
||||
description,
|
||||
color = "primary",
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="stats shadow bg-base-100">
|
||||
<div class="stat">
|
||||
<div class="stat-figure text-{color}">
|
||||
{#if icon}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="inline-block w-8 h-8 stroke-current">
|
||||
{@html icon}
|
||||
</svg>
|
||||
{#if Icon}
|
||||
{@const IconComponent = Icon}
|
||||
<IconComponent class="inline-block w-8 h-8 stroke-current" />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="stat-title">{title}</div>
|
||||
@@ -30,10 +43,9 @@
|
||||
{/if}
|
||||
{#if trend}
|
||||
<div class="stat-desc {trend.isPositive ? 'text-success' : 'text-error'}">
|
||||
{trend.isPositive ? '↗︎' : '↘︎'} {Math.abs(trend.value)}%
|
||||
{trend.isPositive ? "↗︎" : "↘︎"}
|
||||
{Math.abs(trend.value)}%
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -4,19 +4,23 @@
|
||||
|
||||
let abaAtiva = $state<"atividades" | "logins">("atividades");
|
||||
let limite = $state(50);
|
||||
|
||||
|
||||
// Queries com $derived para garantir reatividade
|
||||
const atividades = $derived(useQuery(api.logsAtividades.listarAtividades, { limite }));
|
||||
const logins = $derived(useQuery(api.logsLogin.listarTodosLogins, { limite }));
|
||||
const atividades = $derived(
|
||||
useQuery(api.logsAtividades.listarAtividades, { limite }),
|
||||
);
|
||||
const logins = $derived(
|
||||
useQuery(api.logsLogin.listarTodosLogins, { limite }),
|
||||
);
|
||||
|
||||
function formatarData(timestamp: number) {
|
||||
return new Date(timestamp).toLocaleString('pt-BR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
return new Date(timestamp).toLocaleString("pt-BR", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -27,7 +31,7 @@
|
||||
excluir: "badge-error",
|
||||
bloquear: "badge-error",
|
||||
desbloquear: "badge-success",
|
||||
resetar_senha: "badge-info"
|
||||
resetar_senha: "badge-info",
|
||||
};
|
||||
return colors[acao] || "badge-neutral";
|
||||
}
|
||||
@@ -39,7 +43,7 @@
|
||||
excluir: "Excluir",
|
||||
bloquear: "Bloquear",
|
||||
desbloquear: "Desbloquear",
|
||||
resetar_senha: "Resetar Senha"
|
||||
resetar_senha: "Resetar Senha",
|
||||
};
|
||||
return labels[acao] || acao;
|
||||
}
|
||||
@@ -47,8 +51,12 @@
|
||||
// Estatísticas
|
||||
const totalAtividades = $derived(atividades?.data?.length || 0);
|
||||
const totalLogins = $derived(logins?.data?.length || 0);
|
||||
const loginsSucesso = $derived(logins?.data?.filter(l => l.sucesso).length || 0);
|
||||
const loginsFalha = $derived(logins?.data?.filter(l => !l.sucesso).length || 0);
|
||||
const loginsSucesso = $derived(
|
||||
logins?.data?.filter((l) => l.sucesso).length || 0,
|
||||
);
|
||||
const loginsFalha = $derived(
|
||||
logins?.data?.filter((l) => !l.sucesso).length || 0,
|
||||
);
|
||||
</script>
|
||||
|
||||
<main class="container mx-auto px-4 py-6 max-w-7xl">
|
||||
@@ -64,13 +72,26 @@
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-4 mb-2">
|
||||
<div class="p-3 bg-blue-500/20 rounded-xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8 text-blue-600"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-primary">Auditoria e Logs</h1>
|
||||
<p class="text-base-content/70">Monitoramento completo de atividades e acessos do sistema</p>
|
||||
<p class="text-base-content/70">
|
||||
Monitoramento completo de atividades e acessos do sistema
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -79,8 +100,19 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<div class="stat bg-base-100 shadow-lg rounded-lg">
|
||||
<div class="stat-figure text-primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title text-xs">Atividades</div>
|
||||
@@ -90,8 +122,19 @@
|
||||
|
||||
<div class="stat bg-base-100 shadow-lg rounded-lg">
|
||||
<div class="stat-figure text-success">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title text-xs">Logins Totais</div>
|
||||
@@ -101,44 +144,98 @@
|
||||
|
||||
<div class="stat bg-base-100 shadow-lg rounded-lg">
|
||||
<div class="stat-figure text-success">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title text-xs">Logins Bem-sucedidos</div>
|
||||
<div class="stat-value text-2xl text-success">{loginsSucesso}</div>
|
||||
<div class="stat-desc">{totalLogins > 0 ? Math.round((loginsSucesso / totalLogins) * 100) : 0}% de sucesso</div>
|
||||
<div class="stat-desc">
|
||||
{totalLogins > 0 ? Math.round((loginsSucesso / totalLogins) * 100) : 0}%
|
||||
de sucesso
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat bg-base-100 shadow-lg rounded-lg">
|
||||
<div class="stat-figure text-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title text-xs">Logins Falhados</div>
|
||||
<div class="stat-value text-2xl text-error">{loginsFalha}</div>
|
||||
<div class="stat-desc">{totalLogins > 0 ? Math.round((loginsFalha / totalLogins) * 100) : 0}% de falhas</div>
|
||||
<div class="stat-desc">
|
||||
{totalLogins > 0 ? Math.round((loginsFalha / totalLogins) * 100) : 0}%
|
||||
de falhas
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="tabs tabs-boxed mb-6 bg-base-100 shadow-lg p-1">
|
||||
<button
|
||||
class="tab flex items-center gap-2 {abaAtiva === 'atividades' ? 'tab-active' : ''}"
|
||||
onclick={() => abaAtiva = "atividades"}
|
||||
<button
|
||||
class="tab flex items-center gap-2 {abaAtiva === 'atividades'
|
||||
? 'tab-active'
|
||||
: ''}"
|
||||
onclick={() => (abaAtiva = "atividades")}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"
|
||||
/>
|
||||
</svg>
|
||||
<span class="font-medium">Atividades no Sistema</span>
|
||||
</button>
|
||||
<button
|
||||
class="tab flex items-center gap-2 {abaAtiva === 'logins' ? 'tab-active' : ''}"
|
||||
onclick={() => abaAtiva = "logins"}
|
||||
<button
|
||||
class="tab flex items-center gap-2 {abaAtiva === 'logins'
|
||||
? 'tab-active'
|
||||
: ''}"
|
||||
onclick={() => (abaAtiva = "logins")}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"
|
||||
/>
|
||||
</svg>
|
||||
<span class="font-medium">Histórico de Logins</span>
|
||||
</button>
|
||||
@@ -149,27 +246,52 @@
|
||||
<div class="card-body py-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label py-1">
|
||||
<div class="label py-1">
|
||||
<span class="label-text font-medium">Quantidade de registros</span>
|
||||
</label>
|
||||
<select bind:value={limite} class="select select-bordered select-sm w-full max-w-xs">
|
||||
</div>
|
||||
<select
|
||||
bind:value={limite}
|
||||
class="select select-bordered select-sm w-full max-w-xs"
|
||||
>
|
||||
<option value={20}>20 registros</option>
|
||||
<option value={50}>50 registros</option>
|
||||
<option value={100}>100 registros</option>
|
||||
<option value={200}>200 registros</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-outline btn-primary btn-sm gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||
/>
|
||||
</svg>
|
||||
Exportar CSV
|
||||
</button>
|
||||
<button class="btn btn-outline btn-secondary btn-sm gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
|
||||
/>
|
||||
</svg>
|
||||
Filtros Avançados
|
||||
</button>
|
||||
@@ -184,25 +306,54 @@
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="card-title text-xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 mr-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
|
||||
/>
|
||||
</svg>
|
||||
Atividades Recentes
|
||||
</h2>
|
||||
{#if atividades?.data}
|
||||
<div class="badge badge-outline badge-lg">{atividades.data.length} registro{atividades.data.length !== 1 ? 's' : ''}</div>
|
||||
<div class="badge badge-outline badge-lg">
|
||||
{atividades.data.length} registro{atividades.data.length !== 1
|
||||
? "s"
|
||||
: ""}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
{#if !atividades?.data}
|
||||
<div class="flex flex-col items-center justify-center py-16">
|
||||
<span class="loading loading-spinner loading-lg text-primary mb-4"></span>
|
||||
<span class="loading loading-spinner loading-lg text-primary mb-4"
|
||||
></span>
|
||||
<p class="text-base-content/60">Carregando atividades...</p>
|
||||
</div>
|
||||
{:else if atividades.data.length === 0}
|
||||
<div class="flex flex-col items-center justify-center py-16 text-base-content/60">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mb-4 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
<div
|
||||
class="flex flex-col items-center justify-center py-16 text-base-content/60"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-16 w-16 mb-4 opacity-50"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
<p class="text-lg font-medium">Nenhuma atividade registrada</p>
|
||||
<p class="text-sm mt-1">As atividades do sistema aparecerão aqui</p>
|
||||
@@ -224,32 +375,58 @@
|
||||
<tr class="hover transition-colors">
|
||||
<td>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-base-content/40" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 text-base-content/40"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="font-mono text-xs">{formatarData(atividade.timestamp)}</span>
|
||||
<span class="font-mono text-xs"
|
||||
>{formatarData(atividade.timestamp)}</span
|
||||
>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex flex-col">
|
||||
<div class="font-semibold text-sm">{atividade.usuarioNome || "Sistema"}</div>
|
||||
<div class="font-semibold text-sm">
|
||||
{atividade.usuarioNome || "Sistema"}
|
||||
</div>
|
||||
{#if atividade.usuarioMatricula}
|
||||
<div class="text-xs text-base-content/50 font-mono">{atividade.usuarioMatricula}</div>
|
||||
<div class="text-xs text-base-content/50 font-mono">
|
||||
{atividade.usuarioMatricula}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge {getAcaoColor(atividade.acao)} badge-sm gap-1">
|
||||
<span
|
||||
class="badge {getAcaoColor(
|
||||
atividade.acao,
|
||||
)} badge-sm gap-1"
|
||||
>
|
||||
{getAcaoLabel(atividade.acao)}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="font-medium text-sm">{atividade.recurso}</span>
|
||||
<span class="font-medium text-sm"
|
||||
>{atividade.recurso}</span
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<div class="max-w-md">
|
||||
{#if atividade.detalhes}
|
||||
<div class="text-xs text-base-content/70 truncate" title={atividade.detalhes}>
|
||||
<div
|
||||
class="text-xs text-base-content/70 truncate"
|
||||
title={atividade.detalhes}
|
||||
>
|
||||
{atividade.detalhes}
|
||||
</div>
|
||||
{:else}
|
||||
@@ -270,25 +447,52 @@
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="card-title text-xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 mr-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"
|
||||
/>
|
||||
</svg>
|
||||
Histórico de Logins
|
||||
</h2>
|
||||
{#if logins?.data}
|
||||
<div class="badge badge-outline badge-lg">{logins.data.length} registro{logins.data.length !== 1 ? 's' : ''}</div>
|
||||
<div class="badge badge-outline badge-lg">
|
||||
{logins.data.length} registro{logins.data.length !== 1 ? "s" : ""}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
{#if !logins?.data}
|
||||
<div class="flex flex-col items-center justify-center py-16">
|
||||
<span class="loading loading-spinner loading-lg text-primary mb-4"></span>
|
||||
<span class="loading loading-spinner loading-lg text-primary mb-4"
|
||||
></span>
|
||||
<p class="text-base-content/60">Carregando logins...</p>
|
||||
</div>
|
||||
{:else if logins.data.length === 0}
|
||||
<div class="flex flex-col items-center justify-center py-16 text-base-content/60">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mb-4 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1" />
|
||||
<div
|
||||
class="flex flex-col items-center justify-center py-16 text-base-content/60"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-16 w-16 mb-4 opacity-50"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"
|
||||
/>
|
||||
</svg>
|
||||
<p class="text-lg font-medium">Nenhum login registrado</p>
|
||||
<p class="text-sm mt-1">Os acessos ao sistema aparecerão aqui</p>
|
||||
@@ -312,53 +516,112 @@
|
||||
<tr class="hover transition-colors">
|
||||
<td>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-base-content/40" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 text-base-content/40"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="font-mono text-xs">{formatarData(login.timestamp)}</span>
|
||||
<span class="font-mono text-xs"
|
||||
>{formatarData(login.timestamp)}</span
|
||||
>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-base-content/40" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 text-base-content/40"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-sm font-medium">{login.matriculaOuEmail}</span>
|
||||
<span class="text-sm font-medium"
|
||||
>{login.matriculaOuEmail}</span
|
||||
>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex flex-col gap-1">
|
||||
{#if login.sucesso}
|
||||
<span class="badge badge-success badge-sm gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-3 w-3"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
Sucesso
|
||||
</span>
|
||||
{:else}
|
||||
<span class="badge badge-error badge-sm gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-3 w-3"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
Falhou
|
||||
</span>
|
||||
{#if login.motivoFalha}
|
||||
<div class="text-xs text-error mt-1 font-medium">{login.motivoFalha}</div>
|
||||
<div class="text-xs text-error mt-1 font-medium">
|
||||
{login.motivoFalha}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="font-mono text-xs bg-base-200 px-2 py-1 rounded">{login.ipAddress || "-"}</span>
|
||||
<span
|
||||
class="font-mono text-xs bg-base-200 px-2 py-1 rounded"
|
||||
>{login.ipAddress || "-"}</span
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-xs text-base-content/70">{login.device || "-"}</div>
|
||||
<div class="text-xs text-base-content/70">
|
||||
{login.device || "-"}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-xs text-base-content/70">{login.browser || "-"}</div>
|
||||
<div class="text-xs text-base-content/70">
|
||||
{login.browser || "-"}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-xs text-base-content/70">{login.sistema || "-"}</div>
|
||||
<div class="text-xs text-base-content/70">
|
||||
{login.sistema || "-"}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
@@ -372,13 +635,25 @@
|
||||
|
||||
<!-- Informação -->
|
||||
<div class="alert alert-info shadow-lg mt-6">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="stroke-current shrink-0 w-6 h-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
></path>
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="font-bold">Informação Importante</h3>
|
||||
<div class="text-sm">Os logs são armazenados permanentemente e não podem ser alterados ou excluídos.</div>
|
||||
<div class="text-sm">
|
||||
Os logs são armazenados permanentemente e não podem ser alterados ou
|
||||
excluídos.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -6,19 +6,45 @@
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="p-3 bg-gradient-to-br from-primary/20 to-primary/10 rounded-2xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
|
||||
<div
|
||||
class="p-3 bg-linear-to-br from-primary/20 to-primary/10 rounded-2xl"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-10 w-10 text-primary"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-4xl font-bold text-primary">Monitoramento SGSE</h1>
|
||||
<p class="text-base-content/60 mt-2 text-lg">Sistema de monitoramento técnico em tempo real</p>
|
||||
<p class="text-base-content/60 mt-2 text-lg">
|
||||
Sistema de monitoramento técnico em tempo real
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/ti" class="btn btn-ghost">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 19l-7-7m0 0l7-7m-7 7h18"
|
||||
/>
|
||||
</svg>
|
||||
Voltar
|
||||
</a>
|
||||
@@ -27,4 +53,3 @@
|
||||
<!-- Card de Monitoramento -->
|
||||
<SystemMonitorCardLocal />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -53,13 +53,14 @@
|
||||
let emailIdsRastreados = $state<Set<string>>(new Set());
|
||||
|
||||
// Query para buscar status dos emails
|
||||
const emailIdsArray = $derived(
|
||||
Array.from(emailIdsRastreados).map((id) => id as Id<"notificacoesEmail">),
|
||||
);
|
||||
const emailsStatusQuery = useQuery(
|
||||
api.email.buscarEmailsPorIds,
|
||||
emailIdsArray.length > 0 ? { emailIds: emailIdsArray } : undefined,
|
||||
);
|
||||
const emailsStatusQuery = useQuery(api.email.buscarEmailsPorIds, () => {
|
||||
const ids = Array.from(emailIdsRastreados).map(
|
||||
(id) => id as Id<"notificacoesEmail">,
|
||||
);
|
||||
return ids.length > 0
|
||||
? { emailIds: ids }
|
||||
: { emailIds: [] as Id<"notificacoesEmail">[] };
|
||||
});
|
||||
|
||||
// Queries para agendamentos
|
||||
const agendamentosEmailQuery = useQuery(
|
||||
@@ -727,12 +728,12 @@
|
||||
"enviando",
|
||||
"Criando/buscando conversa...",
|
||||
);
|
||||
const conversaResult = await client.mutation(
|
||||
const conversaId = await client.mutation(
|
||||
api.chat.criarOuBuscarConversaIndividual,
|
||||
{ outroUsuarioId: destinatario._id as Id<"usuarios"> },
|
||||
);
|
||||
|
||||
if (conversaResult.conversaId) {
|
||||
if (conversaId) {
|
||||
const mensagem = usarTemplate
|
||||
? templateSelecionado?.corpo || ""
|
||||
: mensagemPersonalizada;
|
||||
@@ -748,7 +749,7 @@
|
||||
resultadoChat = await client.mutation(
|
||||
api.chat.agendarMensagem,
|
||||
{
|
||||
conversaId: conversaResult.conversaId,
|
||||
conversaId: conversaId,
|
||||
conteudo: mensagem,
|
||||
agendadaPara: agendadaPara,
|
||||
},
|
||||
@@ -773,7 +774,7 @@
|
||||
"Enviando mensagem...",
|
||||
);
|
||||
resultadoChat = await client.mutation(api.chat.enviarMensagem, {
|
||||
conversaId: conversaResult.conversaId,
|
||||
conversaId: conversaId,
|
||||
conteudo: mensagem,
|
||||
tipo: "texto",
|
||||
permitirNotificacaoParaSiMesmo: true,
|
||||
@@ -813,7 +814,7 @@
|
||||
);
|
||||
if (usarTemplate && templateId) {
|
||||
const template = templateSelecionado;
|
||||
if (template) {
|
||||
if (template && authStore.usuario) {
|
||||
resultadoEmail = await client.mutation(
|
||||
api.email.enviarEmailComTemplate,
|
||||
{
|
||||
@@ -822,7 +823,7 @@
|
||||
templateCodigo: template.codigo,
|
||||
variaveis: {
|
||||
nome: destinatario.nome,
|
||||
matricula: destinatario.matricula,
|
||||
matricula: destinatario.matricula || "",
|
||||
},
|
||||
enviadoPorId: authStore.usuario._id as Id<"usuarios">,
|
||||
agendadaPara: agendadaPara,
|
||||
@@ -868,6 +869,15 @@
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (!authStore.usuario) {
|
||||
adicionarLog(
|
||||
"email",
|
||||
destinatario.nome,
|
||||
"erro",
|
||||
"Usuário não autenticado",
|
||||
);
|
||||
return;
|
||||
}
|
||||
resultadoEmail = await client.mutation(
|
||||
api.email.enfileirarEmail,
|
||||
{
|
||||
@@ -981,12 +991,12 @@
|
||||
"enviando",
|
||||
"Processando...",
|
||||
);
|
||||
const conversaResult = await client.mutation(
|
||||
const conversaId = await client.mutation(
|
||||
api.chat.criarOuBuscarConversaIndividual,
|
||||
{ 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 mensagem personalizada, usar diretamente
|
||||
const mensagem = usarTemplate
|
||||
@@ -995,7 +1005,7 @@
|
||||
|
||||
if (agendadaPara) {
|
||||
await client.mutation(api.chat.agendarMensagem, {
|
||||
conversaId: conversaResult.conversaId,
|
||||
conversaId: conversaId,
|
||||
conteudo: mensagem,
|
||||
agendadaPara: agendadaPara,
|
||||
});
|
||||
@@ -1012,7 +1022,7 @@
|
||||
);
|
||||
} else {
|
||||
await client.mutation(api.chat.enviarMensagem, {
|
||||
conversaId: conversaResult.conversaId,
|
||||
conversaId: conversaId,
|
||||
conteudo: mensagem,
|
||||
tipo: "texto",
|
||||
permitirNotificacaoParaSiMesmo: true,
|
||||
@@ -1063,7 +1073,7 @@
|
||||
);
|
||||
if (usarTemplate && templateId) {
|
||||
const template = templateSelecionado;
|
||||
if (template) {
|
||||
if (template && authStore.usuario) {
|
||||
const resultadoEmail = await client.mutation(
|
||||
api.email.enviarEmailComTemplate,
|
||||
{
|
||||
@@ -1121,6 +1131,16 @@
|
||||
falhasEmail++;
|
||||
}
|
||||
} else {
|
||||
if (!authStore.usuario) {
|
||||
adicionarLog(
|
||||
"email",
|
||||
destinatario.nome,
|
||||
"erro",
|
||||
"Usuário não autenticado",
|
||||
);
|
||||
falhasEmail++;
|
||||
continue;
|
||||
}
|
||||
const resultadoEmail = await client.mutation(
|
||||
api.email.enfileirarEmail,
|
||||
{
|
||||
@@ -1376,12 +1396,12 @@
|
||||
{/if}
|
||||
</select>
|
||||
{#if enviarParaTodos}
|
||||
<label class="label">
|
||||
<div class="label">
|
||||
<span class="label-text-alt text-warning">
|
||||
⚠️ A notificação será enviada para todos os {usuarios.length} usuários
|
||||
do sistema
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1623,9 +1643,9 @@
|
||||
<!-- Terminal de Status -->
|
||||
<div class="mt-4">
|
||||
<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>
|
||||
</label>
|
||||
</div>
|
||||
{#if logsEnvio.length > 0}
|
||||
<button
|
||||
type="button"
|
||||
@@ -2109,99 +2129,104 @@
|
||||
<div class="space-y-4">
|
||||
<!-- Código -->
|
||||
<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-alt">Ex: AVISO_IMPORTANTE</span>
|
||||
</label>
|
||||
<input
|
||||
id="codigo-template"
|
||||
type="text"
|
||||
bind:value={codigoTemplate}
|
||||
placeholder="CODIGO_TEMPLATE"
|
||||
class="input input-bordered"
|
||||
maxlength="50"
|
||||
/>
|
||||
<label class="label">
|
||||
<div class="label">
|
||||
<span class="label-text-alt"
|
||||
>Código único para identificar o template (será convertido para
|
||||
MAIÚSCULAS)</span
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nome -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<label class="label" for="nome-template">
|
||||
<span class="label-text font-medium">Nome *</span>
|
||||
</label>
|
||||
<input
|
||||
id="nome-template"
|
||||
type="text"
|
||||
bind:value={nomeTemplate}
|
||||
placeholder="Nome do Template"
|
||||
class="input input-bordered"
|
||||
maxlength="100"
|
||||
/>
|
||||
<label class="label">
|
||||
<div class="label">
|
||||
<span class="label-text-alt"
|
||||
>Nome exibido na lista de templates</span
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Título -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<label class="label" for="titulo-template">
|
||||
<span class="label-text font-medium">Título *</span>
|
||||
</label>
|
||||
<input
|
||||
id="titulo-template"
|
||||
type="text"
|
||||
bind:value={tituloTemplate}
|
||||
placeholder="Título da Mensagem"
|
||||
class="input input-bordered"
|
||||
maxlength="200"
|
||||
/>
|
||||
<label class="label">
|
||||
<div class="label">
|
||||
<span class="label-text-alt"
|
||||
>Título usado no assunto do email ou cabeçalho da mensagem</span
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Corpo -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<label class="label" for="corpo-template">
|
||||
<span class="label-text font-medium">Corpo da Mensagem *</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="corpo-template"
|
||||
bind:value={corpoTemplate}
|
||||
placeholder="Digite o conteúdo da mensagem..."
|
||||
class="textarea textarea-bordered h-32"
|
||||
maxlength="2000"
|
||||
></textarea>
|
||||
<label class="label">
|
||||
<div class="label">
|
||||
<span class="label-text-alt"
|
||||
>Use {"{{"}variavel{"}}"} para variáveis dinâmicas (ex: {"{{"}nome{"}}"},
|
||||
{"{{"}data{"}}"})</span
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Variáveis -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<label class="label" for="variaveis-template">
|
||||
<span class="label-text font-medium">Variáveis (opcional)</span>
|
||||
</label>
|
||||
<input
|
||||
id="variaveis-template"
|
||||
type="text"
|
||||
bind:value={variaveisTemplate}
|
||||
placeholder="nome, data, valor (separadas por vírgula)"
|
||||
class="input input-bordered"
|
||||
/>
|
||||
<label class="label">
|
||||
<div class="label">
|
||||
<span class="label-text-alt"
|
||||
>Liste as variáveis que podem ser substituídas no template
|
||||
(separadas por vírgula ou espaço)</span
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2241,6 +2266,16 @@
|
||||
</button>
|
||||
</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>
|
||||
{/if}
|
||||
|
||||
@@ -2,40 +2,51 @@
|
||||
import { useQuery, useConvexClient } from "convex-svelte";
|
||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||
import StatsCard from "$lib/components/ti/StatsCard.svelte";
|
||||
import {
|
||||
Users,
|
||||
CheckCircle,
|
||||
Ban,
|
||||
Clock,
|
||||
BarChart3,
|
||||
Plus,
|
||||
FolderTree,
|
||||
FileText,
|
||||
Info,
|
||||
} from "lucide-svelte";
|
||||
|
||||
const client = useConvexClient();
|
||||
const usuariosQuery = useQuery(api.usuarios.listar, {});
|
||||
|
||||
|
||||
// Verificar se está carregando
|
||||
const carregando = $derived(usuariosQuery === undefined);
|
||||
|
||||
|
||||
// Extrair dados dos usuários
|
||||
const usuarios = $derived(usuariosQuery?.data ?? []);
|
||||
|
||||
|
||||
// Estatísticas derivadas
|
||||
const stats = $derived.by(() => {
|
||||
// Se ainda está carregando, retorna null para mostrar loading
|
||||
if (carregando) return null;
|
||||
|
||||
|
||||
// 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) {
|
||||
return {
|
||||
total: 0,
|
||||
ativos: 0,
|
||||
bloqueados: 0,
|
||||
inativos: 0
|
||||
inativos: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const ativos = usuarios.filter(u => u.ativo && !u.bloqueado).length;
|
||||
const bloqueados = usuarios.filter(u => u.bloqueado === true).length;
|
||||
const inativos = usuarios.filter(u => !u.ativo).length;
|
||||
|
||||
|
||||
const ativos = usuarios.filter((u) => u.ativo && !u.bloqueado).length;
|
||||
const bloqueados = usuarios.filter((u) => u.bloqueado === true).length;
|
||||
const inativos = usuarios.filter((u) => !u.ativo).length;
|
||||
|
||||
return {
|
||||
total: usuarios.length,
|
||||
ativos,
|
||||
bloqueados,
|
||||
inativos
|
||||
inativos,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
@@ -45,13 +56,15 @@
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="p-3 bg-primary/10 rounded-xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
<BarChart3 class="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-base-content">Dashboard Administrativo TI</h1>
|
||||
<p class="text-base-content/60 mt-1">Painel de controle e monitoramento do sistema</p>
|
||||
<h1 class="text-3xl font-bold text-base-content">
|
||||
Dashboard Administrativo TI
|
||||
</h1>
|
||||
<p class="text-base-content/60 mt-1">
|
||||
Painel de controle e monitoramento do sistema
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,34 +72,38 @@
|
||||
<!-- Stats Cards -->
|
||||
{#if stats}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<StatsCard
|
||||
title="Total de Usuários"
|
||||
value={stats.total}
|
||||
icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />'
|
||||
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||
<StatsCard
|
||||
title="Total de Usuários"
|
||||
value={stats.total}
|
||||
icon={Users}
|
||||
color="primary"
|
||||
/>
|
||||
|
||||
<StatsCard
|
||||
title="Usuários Ativos"
|
||||
value={stats.ativos}
|
||||
description="{stats.total > 0 ? ((stats.ativos / stats.total) * 100).toFixed(1) + '% do total' : '0% do total'}"
|
||||
icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />'
|
||||
|
||||
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||
<StatsCard
|
||||
title="Usuários Ativos"
|
||||
value={stats.ativos}
|
||||
description={`${stats.total > 0 ? ((stats.ativos / stats.total) * 100).toFixed(1) + "% do total" : "0% do total"}`}
|
||||
icon={CheckCircle}
|
||||
color="success"
|
||||
/>
|
||||
|
||||
<StatsCard
|
||||
title="Usuários Bloqueados"
|
||||
value={stats.bloqueados}
|
||||
|
||||
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||
<StatsCard
|
||||
title="Usuários Bloqueados"
|
||||
value={stats.bloqueados}
|
||||
description="Requerem atenção"
|
||||
icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />'
|
||||
icon={Ban}
|
||||
color="error"
|
||||
/>
|
||||
|
||||
<StatsCard
|
||||
title="Usuários Inativos"
|
||||
value={stats.inativos}
|
||||
|
||||
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||
<StatsCard
|
||||
title="Usuários Inativos"
|
||||
value={stats.inativos}
|
||||
description="Desativados"
|
||||
icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />'
|
||||
icon={Clock}
|
||||
color="warning"
|
||||
/>
|
||||
</div>
|
||||
@@ -102,23 +119,17 @@
|
||||
<h2 class="card-title text-2xl mb-4">Ações Rápidas</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<a href="/ti/usuarios" class="btn btn-primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
<Plus class="h-5 w-5" />
|
||||
Criar Usuário
|
||||
</a>
|
||||
|
||||
|
||||
<a href="/ti/perfis" class="btn btn-secondary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
</svg>
|
||||
<FolderTree class="h-5 w-5" />
|
||||
Gerenciar Perfis
|
||||
</a>
|
||||
|
||||
|
||||
<a href="/ti/auditoria" class="btn btn-accent">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<FileText class="h-5 w-5" />
|
||||
Ver Logs
|
||||
</a>
|
||||
</div>
|
||||
@@ -127,9 +138,10 @@
|
||||
|
||||
<!-- Informação Sistema -->
|
||||
<div class="alert alert-info">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>Sistema de Gestão da Secretaria de Esportes - Versão 2.0 com controle avançado de acesso</span>
|
||||
<Info class="stroke-current shrink-0 w-6 h-6" />
|
||||
<span
|
||||
>Sistema de Gestão da Secretaria de Esportes - Versão 2.0 com controle
|
||||
avançado de acesso</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,21 @@
|
||||
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
||||
import { format } from "date-fns";
|
||||
import { ptBR } from "date-fns/locale";
|
||||
import {
|
||||
Users,
|
||||
Shield,
|
||||
ShieldAlert,
|
||||
Info,
|
||||
Building2,
|
||||
Search,
|
||||
X,
|
||||
Eye,
|
||||
Calendar,
|
||||
Tag,
|
||||
Settings,
|
||||
User,
|
||||
AlertCircle,
|
||||
} from "lucide-svelte";
|
||||
|
||||
type Role = {
|
||||
_id: Id<"roles">;
|
||||
@@ -38,12 +53,12 @@
|
||||
// Estatísticas
|
||||
const stats = $derived.by(() => {
|
||||
if (carregando) return null;
|
||||
|
||||
|
||||
const porNivel = {
|
||||
0: roles.filter(r => r.nivel === 0).length,
|
||||
1: roles.filter(r => r.nivel === 1).length,
|
||||
2: roles.filter(r => r.nivel === 2).length,
|
||||
3: roles.filter(r => r.nivel >= 3).length,
|
||||
0: roles.filter((r) => r.nivel === 0).length,
|
||||
1: roles.filter((r) => r.nivel === 1).length,
|
||||
2: roles.filter((r) => r.nivel === 2).length,
|
||||
3: roles.filter((r) => r.nivel >= 3).length,
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -52,7 +67,7 @@
|
||||
nivelAlto: porNivel[1],
|
||||
nivelMedio: porNivel[2],
|
||||
nivelBaixo: porNivel[3],
|
||||
comSetor: roles.filter(r => r.setor).length,
|
||||
comSetor: roles.filter((r) => r.setor).length,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -65,7 +80,7 @@
|
||||
resultado = resultado.filter(
|
||||
(r) =>
|
||||
r.nome.toLowerCase().includes(buscaLower) ||
|
||||
r.descricao.toLowerCase().includes(buscaLower)
|
||||
r.descricao.toLowerCase().includes(buscaLower),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -137,33 +152,27 @@
|
||||
filtroNivel = "";
|
||||
}
|
||||
|
||||
const temFiltrosAtivos = $derived(busca.trim() !== "" || filtroSetor !== "" || filtroNivel !== "");
|
||||
const temFiltrosAtivos = $derived(
|
||||
busca.trim() !== "" || filtroSetor !== "" || filtroNivel !== "",
|
||||
);
|
||||
</script>
|
||||
|
||||
<ProtectedRoute allowedRoles={["ti_master", "admin", "ti_usuario"]} maxLevel={3}>
|
||||
<ProtectedRoute
|
||||
allowedRoles={["ti_master", "admin", "ti_usuario"]}
|
||||
maxLevel={3}
|
||||
>
|
||||
<div class="container mx-auto px-4 py-6 max-w-7xl">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="p-3 bg-primary/10 rounded-xl">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8 text-primary"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
/>
|
||||
</svg>
|
||||
<Users class="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-base-content">Gestão de Perfis</h1>
|
||||
<p class="text-base-content/60 mt-1">Visualize e gerencie os perfis de acesso do sistema</p>
|
||||
<p class="text-base-content/60 mt-1">
|
||||
Visualize e gerencie os perfis de acesso do sistema
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -171,38 +180,45 @@
|
||||
<!-- Estatísticas -->
|
||||
{#if stats}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4 mb-8">
|
||||
<StatsCard
|
||||
title="Total de Perfis"
|
||||
value={stats.total}
|
||||
icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />'
|
||||
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||
<StatsCard
|
||||
title="Total de Perfis"
|
||||
value={stats.total}
|
||||
icon={Users}
|
||||
color="primary"
|
||||
/>
|
||||
<StatsCard
|
||||
title="Nível Máximo"
|
||||
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||
<StatsCard
|
||||
title="Nível Máximo"
|
||||
value={stats.nivelMaximo}
|
||||
description="Acesso total"
|
||||
icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />'
|
||||
icon={Shield}
|
||||
color="error"
|
||||
/>
|
||||
<StatsCard
|
||||
title="Nível Alto"
|
||||
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||
<StatsCard
|
||||
title="Nível Alto"
|
||||
value={stats.nivelAlto}
|
||||
description="Acesso elevado"
|
||||
icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />'
|
||||
icon={ShieldAlert}
|
||||
color="warning"
|
||||
/>
|
||||
<StatsCard
|
||||
title="Nível Médio"
|
||||
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||
<StatsCard
|
||||
title="Nível Médio"
|
||||
value={stats.nivelMedio}
|
||||
description="Acesso padrão"
|
||||
icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />'
|
||||
icon={Info}
|
||||
color="info"
|
||||
/>
|
||||
<StatsCard
|
||||
title="Com Setor"
|
||||
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||
<StatsCard
|
||||
title="Com Setor"
|
||||
value={stats.comSetor}
|
||||
description="{stats.total > 0 ? ((stats.comSetor / stats.total) * 100).toFixed(0) + '% do total' : '0%'}"
|
||||
icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />'
|
||||
description={stats.total > 0
|
||||
? ((stats.comSetor / stats.total) * 100).toFixed(0) + "% do total"
|
||||
: "0%"}
|
||||
icon={Building2}
|
||||
color="secondary"
|
||||
/>
|
||||
</div>
|
||||
@@ -214,38 +230,16 @@
|
||||
<div class="card-body">
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 mb-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 text-primary"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
|
||||
/>
|
||||
</svg>
|
||||
<Search class="h-5 w-5 text-primary" />
|
||||
<h2 class="card-title text-lg">Filtros de Busca</h2>
|
||||
</div>
|
||||
{#if temFiltrosAtivos}
|
||||
<button type="button" class="btn btn-sm btn-outline btn-error" onclick={limparFiltros}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline btn-error"
|
||||
onclick={limparFiltros}
|
||||
>
|
||||
<X class="h-4 w-4" />
|
||||
Limpar Filtros
|
||||
</button>
|
||||
{/if}
|
||||
@@ -265,20 +259,9 @@
|
||||
placeholder="Buscar por nome ou descrição..."
|
||||
class="input input-bordered w-full pl-10"
|
||||
/>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
<Search
|
||||
class="h-5 w-5 absolute left-3 top-1/2 transform -translate-y-1/2 text-base-content/40"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -287,7 +270,11 @@
|
||||
<label class="label" for="filtro-setor">
|
||||
<span class="label-text font-medium">Setor</span>
|
||||
</label>
|
||||
<select id="filtro-setor" bind:value={filtroSetor} class="select select-bordered">
|
||||
<select
|
||||
id="filtro-setor"
|
||||
bind:value={filtroSetor}
|
||||
class="select select-bordered"
|
||||
>
|
||||
<option value="">Todos os setores</option>
|
||||
{#each setoresDisponiveis as setor}
|
||||
<option value={setor}>{setor}</option>
|
||||
@@ -300,7 +287,11 @@
|
||||
<label class="label" for="filtro-nivel">
|
||||
<span class="label-text font-medium">Nível de Acesso</span>
|
||||
</label>
|
||||
<select id="filtro-nivel" bind:value={filtroNivel} class="select select-bordered">
|
||||
<select
|
||||
id="filtro-nivel"
|
||||
bind:value={filtroNivel}
|
||||
class="select select-bordered"
|
||||
>
|
||||
<option value="">Todos os níveis</option>
|
||||
<option value={0}>Máximo (0)</option>
|
||||
<option value={1}>Alto (1)</option>
|
||||
@@ -312,7 +303,12 @@
|
||||
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<div class="text-sm text-base-content/60">
|
||||
<span class="font-medium text-base-content">{rolesFiltradas.length}</span> de <span class="font-medium text-base-content">{roles.length}</span> perfil(is)
|
||||
<span class="font-medium text-base-content"
|
||||
>{rolesFiltradas.length}</span
|
||||
>
|
||||
de
|
||||
<span class="font-medium text-base-content">{roles.length}</span>
|
||||
perfil(is)
|
||||
{#if temFiltrosAtivos}
|
||||
<span class="badge badge-primary badge-sm ml-2">Filtrado</span>
|
||||
{/if}
|
||||
@@ -329,45 +325,28 @@
|
||||
</div>
|
||||
{:else if roles.length === 0}
|
||||
<div class="flex flex-col items-center justify-center py-16 text-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-16 w-16 text-base-content/30"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
/>
|
||||
</svg>
|
||||
<Users class="h-16 w-16 text-base-content/30" />
|
||||
<h3 class="text-xl font-semibold mt-4">Nenhum perfil encontrado</h3>
|
||||
<p class="text-base-content/60 mt-2">Não há perfis cadastrados no sistema.</p>
|
||||
<p class="text-base-content/60 mt-2">
|
||||
Não há perfis cadastrados no sistema.
|
||||
</p>
|
||||
</div>
|
||||
{:else if rolesFiltradas.length === 0}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-col items-center justify-center py-16 text-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-16 w-16 text-base-content/30 mb-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
class="flex flex-col items-center justify-center py-16 text-center"
|
||||
>
|
||||
<AlertCircle class="h-16 w-16 text-base-content/30 mb-4" />
|
||||
<h3 class="text-xl font-semibold mt-4">Nenhum perfil encontrado</h3>
|
||||
<p class="text-base-content/60 mt-2">Nenhum perfil corresponde aos filtros aplicados.</p>
|
||||
<p class="text-base-content/60 mt-2">
|
||||
Nenhum perfil corresponde aos filtros aplicados.
|
||||
</p>
|
||||
{#if temFiltrosAtivos}
|
||||
<button class="btn btn-primary btn-sm mt-4" onclick={limparFiltros}>
|
||||
<button
|
||||
class="btn btn-primary btn-sm mt-4"
|
||||
onclick={limparFiltros}
|
||||
>
|
||||
Limpar Filtros
|
||||
</button>
|
||||
{/if}
|
||||
@@ -376,115 +355,73 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{#each rolesFiltradas as role}
|
||||
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-all duration-300 cursor-pointer border border-base-300 {obterCorCardNivel(role.nivel)} hover:scale-[1.02]" onclick={() => abrirDetalhes(role)}>
|
||||
{#each rolesFiltradas as role (role._id)}
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class="card bg-base-100 shadow-xl hover:shadow-2xl transition-all duration-300 cursor-pointer border border-base-300 {obterCorCardNivel(
|
||||
role.nivel,
|
||||
)} hover:scale-[1.02]"
|
||||
onclick={() => abrirDetalhes(role)}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
abrirDetalhes(role);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="card-body">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="flex-1">
|
||||
<h2 class="card-title text-lg mb-1">{role.descricao}</h2>
|
||||
<div class="badge {obterCorNivel(role.nivel)} badge-sm">{obterTextoNivel(role.nivel)}</div>
|
||||
<div class="badge {obterCorNivel(role.nivel)} badge-sm">
|
||||
{obterTextoNivel(role.nivel)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-2 bg-base-200 rounded-lg">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 text-primary"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
<User class="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="space-y-3 text-sm">
|
||||
<div class="flex items-center gap-2 p-2 bg-base-200 rounded-lg">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 text-base-content/40"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
<Tag class="h-4 w-4 text-base-content/40" />
|
||||
<span class="font-medium text-base-content/60"
|
||||
>Nome técnico:</span
|
||||
>
|
||||
<code class="text-xs bg-base-100 px-2 py-1 rounded font-mono"
|
||||
>{role.nome}</code
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="font-medium text-base-content/60">Nome técnico:</span>
|
||||
<code class="text-xs bg-base-100 px-2 py-1 rounded font-mono">{role.nome}</code>
|
||||
</div>
|
||||
|
||||
|
||||
{#if role.setor}
|
||||
<div class="flex items-center gap-2 p-2 bg-base-200 rounded-lg">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 text-base-content/40"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
class="flex items-center gap-2 p-2 bg-base-200 rounded-lg"
|
||||
>
|
||||
<Building2 class="h-4 w-4 text-base-content/40" />
|
||||
<span class="font-medium text-base-content/60">Setor:</span>
|
||||
<span class="font-medium">{role.setor}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
<div class="flex items-center gap-2 p-2 bg-base-200 rounded-lg">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 text-base-content/40"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
|
||||
/>
|
||||
</svg>
|
||||
<Shield class="h-4 w-4 text-base-content/40" />
|
||||
<span class="font-medium text-base-content/60">Nível:</span>
|
||||
<span class="font-bold text-lg">{role.nivel}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-actions justify-end mt-4 pt-4 border-t border-base-300">
|
||||
<button class="btn btn-sm btn-primary btn-outline" onclick={(e) => { e.stopPropagation(); abrirDetalhes(role); }}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 mr-1"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
class="card-actions justify-end mt-4 pt-4 border-t border-base-300"
|
||||
>
|
||||
<button
|
||||
class="btn btn-sm btn-primary btn-outline"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
abrirDetalhes(role);
|
||||
}}
|
||||
>
|
||||
<Eye class="h-4 w-4 mr-1" />
|
||||
Ver Detalhes
|
||||
</button>
|
||||
</div>
|
||||
@@ -501,40 +438,41 @@
|
||||
<div class="modal-box max-w-3xl">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="font-bold text-2xl">Detalhes do Perfil</h3>
|
||||
<button type="button" class="btn btn-sm btn-circle btn-ghost" onclick={fecharDetalhes}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-circle btn-ghost"
|
||||
onclick={fecharDetalhes}
|
||||
>
|
||||
<X class="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Header do Perfil -->
|
||||
<div class="card bg-gradient-to-r from-primary/10 to-secondary/10 border border-primary/20">
|
||||
<div
|
||||
class="card bg-linear-to-r from-primary/10 to-secondary/10 border border-primary/20"
|
||||
>
|
||||
<div class="card-body">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<h2 class="text-2xl font-bold mb-2">{roleSelecionada.descricao}</h2>
|
||||
<h2 class="text-2xl font-bold mb-2">
|
||||
{roleSelecionada.descricao}
|
||||
</h2>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="badge {obterCorNivel(roleSelecionada.nivel)} badge-lg">{obterTextoNivel(roleSelecionada.nivel)}</div>
|
||||
<span class="text-sm text-base-content/60">Nível {roleSelecionada.nivel}</span>
|
||||
<div
|
||||
class="badge {obterCorNivel(
|
||||
roleSelecionada.nivel,
|
||||
)} badge-lg"
|
||||
>
|
||||
{obterTextoNivel(roleSelecionada.nivel)}
|
||||
</div>
|
||||
<span class="text-sm text-base-content/60"
|
||||
>Nível {roleSelecionada.nivel}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3 bg-base-100 rounded-lg shadow-sm">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8 text-primary"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
<User class="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -545,24 +483,27 @@
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
||||
</svg>
|
||||
<span
|
||||
class="label-text font-semibold flex items-center gap-2"
|
||||
>
|
||||
<Tag class="h-5 w-5 text-primary" />
|
||||
Nome Técnico
|
||||
</span>
|
||||
</label>
|
||||
<code class="block bg-base-200 px-4 py-3 rounded-lg text-sm font-mono mt-2">{roleSelecionada.nome}</code>
|
||||
<code
|
||||
class="block bg-base-200 px-4 py-3 rounded-lg text-sm font-mono mt-2"
|
||||
>{roleSelecionada.nome}</code
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
<span
|
||||
class="label-text font-semibold flex items-center gap-2"
|
||||
>
|
||||
<Building2 class="h-5 w-5 text-primary" />
|
||||
Setor
|
||||
</span>
|
||||
</label>
|
||||
@@ -570,7 +511,9 @@
|
||||
{#if roleSelecionada.setor}
|
||||
{roleSelecionada.setor}
|
||||
{:else}
|
||||
<span class="text-base-content/40 italic">Não especificado</span>
|
||||
<span class="text-base-content/40 italic"
|
||||
>Não especificado</span
|
||||
>
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
@@ -582,26 +525,33 @@
|
||||
<div class="card-body">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
<Shield class="h-5 w-5 text-primary" />
|
||||
Nível de Acesso
|
||||
</span>
|
||||
</label>
|
||||
<div class="mt-4">
|
||||
<div class="flex items-center gap-4 mb-3">
|
||||
<span class="text-4xl font-bold">{roleSelecionada.nivel}</span>
|
||||
<div class="badge {obterCorNivel(roleSelecionada.nivel)} badge-lg">{obterTextoNivel(roleSelecionada.nivel)}</div>
|
||||
<span class="text-4xl font-bold">{roleSelecionada.nivel}</span
|
||||
>
|
||||
<div
|
||||
class="badge {obterCorNivel(
|
||||
roleSelecionada.nivel,
|
||||
)} badge-lg"
|
||||
>
|
||||
{obterTextoNivel(roleSelecionada.nivel)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-info">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<Info class="stroke-current shrink-0 w-6 h-6" />
|
||||
<span class="text-sm">
|
||||
{roleSelecionada.nivel === 0 && "Acesso total irrestrito ao sistema. Pode realizar todas as operações sem restrições."}
|
||||
{roleSelecionada.nivel === 1 && "Acesso alto com algumas restrições. Pode realizar a maioria das operações administrativas."}
|
||||
{roleSelecionada.nivel === 2 && "Acesso médio com permissões configuráveis. Pode realizar operações padrão do sistema."}
|
||||
{roleSelecionada.nivel >= 3 && "Acesso limitado com permissões específicas. Operações restritas conforme configuração."}
|
||||
{roleSelecionada.nivel === 0 &&
|
||||
"Acesso total irrestrito ao sistema. Pode realizar todas as operações sem restrições."}
|
||||
{roleSelecionada.nivel === 1 &&
|
||||
"Acesso alto com algumas restrições. Pode realizar a maioria das operações administrativas."}
|
||||
{roleSelecionada.nivel === 2 &&
|
||||
"Acesso médio com permissões configuráveis. Pode realizar operações padrão do sistema."}
|
||||
{roleSelecionada.nivel >= 3 &&
|
||||
"Acesso limitado com permissões específicas. Operações restritas conforme configuração."}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -613,35 +563,27 @@
|
||||
<div class="card-body">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<Calendar class="h-5 w-5 text-primary" />
|
||||
Data de Criação
|
||||
</span>
|
||||
</label>
|
||||
<p class="text-lg font-medium mt-2">{formatarData(roleSelecionada._creationTime)}</p>
|
||||
<p class="text-lg font-medium mt-2">
|
||||
{formatarData(roleSelecionada._creationTime)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Informação sobre Permissões -->
|
||||
<div class="alert alert-info">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="stroke-current shrink-0 w-6 h-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
></path>
|
||||
</svg>
|
||||
<Info class="stroke-current shrink-0 w-6 h-6" />
|
||||
<div>
|
||||
<h4 class="font-semibold mb-1">Configuração de Permissões</h4>
|
||||
<p class="text-sm">
|
||||
Para configurar permissões específicas deste perfil, acesse o <a href="/ti/painel-permissoes" class="link link-primary font-semibold">Painel de Permissões</a>.
|
||||
Para configurar permissões específicas deste perfil, acesse o <a
|
||||
href="/ti/painel-permissoes"
|
||||
class="link link-primary font-semibold"
|
||||
>Painel de Permissões</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -652,16 +594,23 @@
|
||||
Fechar
|
||||
</button>
|
||||
<a href="/ti/painel-permissoes" class="btn btn-primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<Settings class="h-5 w-5 mr-2" />
|
||||
Configurar Permissões
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-backdrop" onclick={fecharDetalhes}></div>
|
||||
<div
|
||||
class="modal-backdrop"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onclick={fecharDetalhes}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
fecharDetalhes();
|
||||
}
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
{/if}
|
||||
</ProtectedRoute>
|
||||
|
||||
|
||||
@@ -6,6 +6,22 @@
|
||||
import StatsCard from "$lib/components/ti/StatsCard.svelte";
|
||||
import { format } from "date-fns";
|
||||
import { ptBR } from "date-fns/locale";
|
||||
import {
|
||||
FileText,
|
||||
Clock,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
UserPlus,
|
||||
Search,
|
||||
Eye,
|
||||
Check,
|
||||
X,
|
||||
CircleCheckBig,
|
||||
CircleX,
|
||||
Mail,
|
||||
Phone,
|
||||
Calendar,
|
||||
} from "lucide-svelte";
|
||||
|
||||
type StatusSolicitacao = "pendente" | "aprovado" | "rejeitado";
|
||||
|
||||
@@ -30,10 +46,10 @@
|
||||
};
|
||||
|
||||
const client = useConvexClient();
|
||||
|
||||
|
||||
// Queries
|
||||
const solicitacoesQuery = useQuery(api.solicitacoesAcesso.getAll, {});
|
||||
|
||||
|
||||
// Estados
|
||||
let filtroStatus = $state<FiltroStatus>("todos");
|
||||
let busca = $state("");
|
||||
@@ -50,15 +66,17 @@
|
||||
if (solicitacoesQuery === undefined || solicitacoesQuery === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
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)) {
|
||||
return solicitacoesQuery;
|
||||
}
|
||||
|
||||
|
||||
return [];
|
||||
});
|
||||
|
||||
@@ -69,17 +87,23 @@
|
||||
// Estatísticas
|
||||
const stats = $derived.by(() => {
|
||||
if (carregando) return null;
|
||||
|
||||
|
||||
const total = solicitacoes.length;
|
||||
const pendentes = solicitacoes.filter(s => s.status === "pendente").length;
|
||||
const aprovadas = solicitacoes.filter(s => s.status === "aprovado").length;
|
||||
const rejeitadas = solicitacoes.filter(s => s.status === "rejeitado").length;
|
||||
|
||||
const pendentes = solicitacoes.filter(
|
||||
(s) => s.status === "pendente",
|
||||
).length;
|
||||
const aprovadas = solicitacoes.filter(
|
||||
(s) => s.status === "aprovado",
|
||||
).length;
|
||||
const rejeitadas = solicitacoes.filter(
|
||||
(s) => s.status === "rejeitado",
|
||||
).length;
|
||||
|
||||
return {
|
||||
total,
|
||||
pendentes,
|
||||
aprovadas,
|
||||
rejeitadas
|
||||
rejeitadas,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -89,16 +113,17 @@
|
||||
|
||||
// Filtrar por status
|
||||
if (filtroStatus !== "todos") {
|
||||
resultado = resultado.filter(s => s.status === filtroStatus);
|
||||
resultado = resultado.filter((s) => s.status === filtroStatus);
|
||||
}
|
||||
|
||||
// Buscar por nome, matrícula ou email
|
||||
if (busca.trim()) {
|
||||
const termo = busca.toLowerCase().trim();
|
||||
resultado = resultado.filter(s =>
|
||||
s.nome.toLowerCase().includes(termo) ||
|
||||
s.matricula.toLowerCase().includes(termo) ||
|
||||
s.email.toLowerCase().includes(termo)
|
||||
resultado = resultado.filter(
|
||||
(s) =>
|
||||
s.nome.toLowerCase().includes(termo) ||
|
||||
s.matricula.toLowerCase().includes(termo) ||
|
||||
s.email.toLowerCase().includes(termo),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -108,7 +133,9 @@
|
||||
|
||||
// Funções auxiliares
|
||||
function formatarData(timestamp: number): string {
|
||||
return format(new Date(timestamp), "dd/MM/yyyy 'às' HH:mm", { locale: ptBR });
|
||||
return format(new Date(timestamp), "dd/MM/yyyy 'às' HH:mm", {
|
||||
locale: ptBR,
|
||||
});
|
||||
}
|
||||
|
||||
function formatarDataRelativa(timestamp: number): string {
|
||||
@@ -118,9 +145,9 @@
|
||||
const horas = Math.floor(diff / (1000 * 60 * 60));
|
||||
const minutos = Math.floor(diff / (1000 * 60));
|
||||
|
||||
if (dias > 0) return `${dias} dia${dias > 1 ? 's' : ''} atrás`;
|
||||
if (horas > 0) return `${horas} hora${horas > 1 ? 's' : ''} atrás`;
|
||||
if (minutos > 0) return `${minutos} minuto${minutos > 1 ? 's' : ''} atrás`;
|
||||
if (dias > 0) return `${dias} dia${dias > 1 ? "s" : ""} atrás`;
|
||||
if (horas > 0) return `${horas} hora${horas > 1 ? "s" : ""} atrás`;
|
||||
if (minutos > 0) return `${minutos} minuto${minutos > 1 ? "s" : ""} atrás`;
|
||||
return "Agora";
|
||||
}
|
||||
|
||||
@@ -204,13 +231,14 @@
|
||||
};
|
||||
|
||||
fecharAprovar();
|
||||
|
||||
|
||||
// Limpar mensagem após 3 segundos
|
||||
setTimeout(() => {
|
||||
mensagem = null;
|
||||
}, 3000);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Erro ao aprovar solicitação";
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Erro ao aprovar solicitação";
|
||||
mensagem = {
|
||||
tipo: "error",
|
||||
texto: errorMessage,
|
||||
@@ -238,13 +266,14 @@
|
||||
};
|
||||
|
||||
fecharRejeitar();
|
||||
|
||||
|
||||
// Limpar mensagem após 3 segundos
|
||||
setTimeout(() => {
|
||||
mensagem = null;
|
||||
}, 3000);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Erro ao rejeitar solicitação";
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Erro ao rejeitar solicitação";
|
||||
mensagem = {
|
||||
tipo: "error",
|
||||
texto: errorMessage,
|
||||
@@ -255,19 +284,18 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<ProtectedRoute allowedRoles={["ti_master", "admin", "ti_usuario"]} maxLevel={3}>
|
||||
<ProtectedRoute
|
||||
allowedRoles={["ti_master", "admin", "ti_usuario"]}
|
||||
maxLevel={3}
|
||||
>
|
||||
<div class="container mx-auto px-4 py-6 max-w-7xl">
|
||||
<!-- Mensagem de Feedback -->
|
||||
{#if mensagem}
|
||||
<div class="alert alert-{mensagem.tipo} shadow-lg mb-6">
|
||||
{#if mensagem.tipo === "success"}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<CircleCheckBig class="stroke-current shrink-0 h-6 w-6" />
|
||||
{:else if mensagem.tipo === "error"}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<CircleX class="stroke-current shrink-0 h-6 w-6" />
|
||||
{/if}
|
||||
<span>{mensagem.texto}</span>
|
||||
</div>
|
||||
@@ -277,13 +305,15 @@
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="p-3 bg-primary/10 rounded-xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z" />
|
||||
</svg>
|
||||
<UserPlus class="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-base-content">Solicitações de Acesso</h1>
|
||||
<p class="text-base-content/60 mt-1">Gerencie e analise solicitações de acesso ao sistema</p>
|
||||
<h1 class="text-3xl font-bold text-base-content">
|
||||
Solicitações de Acesso
|
||||
</h1>
|
||||
<p class="text-base-content/60 mt-1">
|
||||
Gerencie e analise solicitações de acesso ao sistema
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -291,34 +321,38 @@
|
||||
<!-- Estatísticas -->
|
||||
{#if stats}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<StatsCard
|
||||
title="Total de Solicitações"
|
||||
value={stats.total}
|
||||
icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />'
|
||||
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||
<StatsCard
|
||||
title="Total de Solicitações"
|
||||
value={stats.total}
|
||||
icon={FileText}
|
||||
color="primary"
|
||||
/>
|
||||
|
||||
<StatsCard
|
||||
title="Pendentes"
|
||||
value={stats.pendentes}
|
||||
description="{stats.total > 0 ? ((stats.pendentes / stats.total) * 100).toFixed(1) + '% do total' : '0% do total'}"
|
||||
icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />'
|
||||
|
||||
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||
<StatsCard
|
||||
title="Pendentes"
|
||||
value={stats.pendentes}
|
||||
description={`${stats.total > 0 ? ((stats.pendentes / stats.total) * 100).toFixed(1) + "% do total" : "0% do total"}`}
|
||||
icon={Clock}
|
||||
color="warning"
|
||||
/>
|
||||
|
||||
<StatsCard
|
||||
title="Aprovadas"
|
||||
value={stats.aprovadas}
|
||||
description="{stats.total > 0 ? ((stats.aprovadas / stats.total) * 100).toFixed(1) + '% do total' : '0% do total'}"
|
||||
icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />'
|
||||
|
||||
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||
<StatsCard
|
||||
title="Aprovadas"
|
||||
value={stats.aprovadas}
|
||||
description={`${stats.total > 0 ? ((stats.aprovadas / stats.total) * 100).toFixed(1) + "% do total" : "0% do total"}`}
|
||||
icon={CheckCircle}
|
||||
color="success"
|
||||
/>
|
||||
|
||||
<StatsCard
|
||||
title="Rejeitadas"
|
||||
value={stats.rejeitadas}
|
||||
description="{stats.total > 0 ? ((stats.rejeitadas / stats.total) * 100).toFixed(1) + '% do total' : '0% do total'}"
|
||||
icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />'
|
||||
|
||||
<!-- @ts-ignore - Componentes lucide-svelte são válidos em runtime -->
|
||||
<StatsCard
|
||||
title="Rejeitadas"
|
||||
value={stats.rejeitadas}
|
||||
description={`${stats.total > 0 ? ((stats.rejeitadas / stats.total) * 100).toFixed(1) + "% do total" : "0% do total"}`}
|
||||
icon={XCircle}
|
||||
color="error"
|
||||
/>
|
||||
</div>
|
||||
@@ -333,27 +367,27 @@
|
||||
<div class="card-body">
|
||||
<!-- Tabs de Status -->
|
||||
<div class="tabs tabs-boxed mb-4 bg-base-200 p-2">
|
||||
<button
|
||||
<button
|
||||
class="tab {filtroStatus === 'todos' ? 'tab-active' : ''}"
|
||||
onclick={() => filtroStatus = "todos"}
|
||||
onclick={() => (filtroStatus = "todos")}
|
||||
>
|
||||
Todas
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
class="tab {filtroStatus === 'pendente' ? 'tab-active' : ''}"
|
||||
onclick={() => filtroStatus = "pendente"}
|
||||
onclick={() => (filtroStatus = "pendente")}
|
||||
>
|
||||
Pendentes
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
class="tab {filtroStatus === 'aprovado' ? 'tab-active' : ''}"
|
||||
onclick={() => filtroStatus = "aprovado"}
|
||||
onclick={() => (filtroStatus = "aprovado")}
|
||||
>
|
||||
Aprovadas
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
class="tab {filtroStatus === 'rejeitado' ? 'tab-active' : ''}"
|
||||
onclick={() => filtroStatus = "rejeitado"}
|
||||
onclick={() => (filtroStatus = "rejeitado")}
|
||||
>
|
||||
Rejeitadas
|
||||
</button>
|
||||
@@ -361,9 +395,11 @@
|
||||
|
||||
<!-- Campo de Busca -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Buscar por nome, matrícula ou e-mail</span>
|
||||
</label>
|
||||
<div class="label">
|
||||
<span class="label-text font-semibold"
|
||||
>Buscar por nome, matrícula ou e-mail</span
|
||||
>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
@@ -371,9 +407,9 @@
|
||||
class="input input-bordered w-full pl-10"
|
||||
bind:value={busca}
|
||||
/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 absolute left-3 top-1/2 transform -translate-y-1/2 text-base-content/50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
<Search
|
||||
class="h-5 w-5 absolute left-3 top-1/2 transform -translate-y-1/2 text-base-content/50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -387,10 +423,10 @@
|
||||
{:else if solicitacoesFiltradas.length === 0}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body text-center py-20">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mx-auto text-base-content/30 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<h3 class="text-xl font-semibold text-base-content/70 mb-2">Nenhuma solicitação encontrada</h3>
|
||||
<FileText class="h-16 w-16 mx-auto text-base-content/30 mb-4" />
|
||||
<h3 class="text-xl font-semibold text-base-content/70 mb-2">
|
||||
Nenhuma solicitação encontrada
|
||||
</h3>
|
||||
<p class="text-base-content/50">
|
||||
{#if busca.trim() || filtroStatus !== "todos"}
|
||||
Tente ajustar os filtros ou a busca.
|
||||
@@ -403,81 +439,87 @@
|
||||
{:else}
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
{#each solicitacoesFiltradas as solicitacao}
|
||||
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow">
|
||||
<div
|
||||
class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow"
|
||||
>
|
||||
<div class="card-body">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div
|
||||
class="flex flex-col md:flex-row md:items-center md:justify-between gap-4"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<h3 class="text-xl font-bold text-base-content">{solicitacao.nome}</h3>
|
||||
<span class="badge {getStatusBadge(solicitacao.status)} badge-lg">
|
||||
<h3 class="text-xl font-bold text-base-content">
|
||||
{solicitacao.nome}
|
||||
</h3>
|
||||
<span
|
||||
class="badge {getStatusBadge(
|
||||
solicitacao.status,
|
||||
)} badge-lg"
|
||||
>
|
||||
{getStatusTexto(solicitacao.status)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-2 text-sm text-base-content/70">
|
||||
|
||||
<div
|
||||
class="grid grid-cols-1 md:grid-cols-3 gap-2 text-sm text-base-content/70"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V8a2 2 0 00-2-2h-5m-4 0V5a2 2 0 114 0v1m-4 0a2 2 0 104 0m-5 8a2 2 0 100-4 2 2 0 000 4zm0 0c1.306 0 2.417.835 2.83 2M9 14a3.001 3.001 0 00-2.83 2M15 11h3m-3 4h2" />
|
||||
</svg>
|
||||
<UserPlus class="h-4 w-4" />
|
||||
<span class="font-semibold">Matrícula:</span>
|
||||
<span>{solicitacao.matricula}</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<Mail class="h-4 w-4" />
|
||||
<span class="font-semibold">E-mail:</span>
|
||||
<span>{solicitacao.email}</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||||
</svg>
|
||||
<Phone class="h-4 w-4" />
|
||||
<span class="font-semibold">Telefone:</span>
|
||||
<span>{solicitacao.telefone}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 text-xs text-base-content/50">
|
||||
<span class="font-semibold">Solicitado em:</span> {formatarData(solicitacao.dataSolicitacao)} ({formatarDataRelativa(solicitacao.dataSolicitacao)})
|
||||
|
||||
<div
|
||||
class="mt-3 text-xs text-base-content/50 flex items-center gap-2"
|
||||
>
|
||||
<Calendar class="h-3 w-3" />
|
||||
<span class="font-semibold">Solicitado em:</span>
|
||||
{formatarData(solicitacao.dataSolicitacao)} ({formatarDataRelativa(
|
||||
solicitacao.dataSolicitacao,
|
||||
)})
|
||||
{#if solicitacao.dataResposta}
|
||||
<span class="ml-4 font-semibold">Processado em:</span> {formatarData(solicitacao.dataResposta)}
|
||||
<span class="ml-4 font-semibold">Processado em:</span>
|
||||
{formatarData(solicitacao.dataResposta)}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<button
|
||||
<button
|
||||
class="btn btn-sm btn-outline btn-primary"
|
||||
onclick={() => abrirDetalhes(solicitacao)}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
<Eye class="h-4 w-4" />
|
||||
Ver Detalhes
|
||||
</button>
|
||||
|
||||
|
||||
{#if solicitacao.status === "pendente"}
|
||||
<button
|
||||
<button
|
||||
class="btn btn-sm btn-success"
|
||||
onclick={() => abrirAprovar(solicitacao)}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<Check class="h-4 w-4" />
|
||||
Aprovar
|
||||
</button>
|
||||
|
||||
<button
|
||||
|
||||
<button
|
||||
class="btn btn-sm btn-error"
|
||||
onclick={() => abrirRejeitar(solicitacao)}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<X class="h-4 w-4" />
|
||||
Rejeitar
|
||||
</button>
|
||||
{/if}
|
||||
@@ -494,76 +536,92 @@
|
||||
<dialog class="modal modal-open">
|
||||
<div class="modal-box max-w-2xl">
|
||||
<h3 class="font-bold text-2xl mb-4">Detalhes da Solicitação</h3>
|
||||
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<span class="badge {getStatusBadge(solicitacaoSelecionada.status)} badge-lg">
|
||||
<span
|
||||
class="badge {getStatusBadge(
|
||||
solicitacaoSelecionada.status,
|
||||
)} badge-lg"
|
||||
>
|
||||
{getStatusTexto(solicitacaoSelecionada.status)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<div class="label">
|
||||
<span class="label-text font-semibold">Nome Completo</span>
|
||||
</label>
|
||||
<div class="input input-bordered">{solicitacaoSelecionada.nome}</div>
|
||||
</div>
|
||||
<div class="input input-bordered">
|
||||
{solicitacaoSelecionada.nome}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<div class="label">
|
||||
<span class="label-text font-semibold">Matrícula</span>
|
||||
</label>
|
||||
<div class="input input-bordered">{solicitacaoSelecionada.matricula}</div>
|
||||
</div>
|
||||
<div class="input input-bordered">
|
||||
{solicitacaoSelecionada.matricula}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<div class="label">
|
||||
<span class="label-text font-semibold">E-mail</span>
|
||||
</label>
|
||||
<div class="input input-bordered">{solicitacaoSelecionada.email}</div>
|
||||
</div>
|
||||
<div class="input input-bordered">
|
||||
{solicitacaoSelecionada.email}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<div class="label">
|
||||
<span class="label-text font-semibold">Telefone</span>
|
||||
</label>
|
||||
<div class="input input-bordered">{solicitacaoSelecionada.telefone}</div>
|
||||
</div>
|
||||
<div class="input input-bordered">
|
||||
{solicitacaoSelecionada.telefone}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Data da Solicitação</span>
|
||||
</label>
|
||||
<div class="label">
|
||||
<span class="label-text font-semibold"
|
||||
>Data da Solicitação</span
|
||||
>
|
||||
</div>
|
||||
<div class="input input-bordered">
|
||||
{formatarData(solicitacaoSelecionada.dataSolicitacao)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{#if solicitacaoSelecionada.dataResposta}
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Data de Processamento</span>
|
||||
</label>
|
||||
<div class="label">
|
||||
<span class="label-text font-semibold"
|
||||
>Data de Processamento</span
|
||||
>
|
||||
</div>
|
||||
<div class="input input-bordered">
|
||||
{formatarData(solicitacaoSelecionada.dataResposta)}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
{#if solicitacaoSelecionada.observacoes}
|
||||
<div>
|
||||
<label class="label">
|
||||
<div class="label">
|
||||
<span class="label-text font-semibold">Observações</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="textarea textarea-bordered min-h-24">
|
||||
{solicitacaoSelecionada.observacoes}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal-action">
|
||||
<button class="btn" onclick={fecharDetalhes}>Fechar</button>
|
||||
</div>
|
||||
@@ -579,20 +637,25 @@
|
||||
<dialog class="modal modal-open">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-2xl mb-4">Aprovar Solicitação</h3>
|
||||
|
||||
|
||||
<div class="mb-4">
|
||||
<p class="text-base-content/70 mb-2">
|
||||
Você está prestes a aprovar a solicitação de acesso de <strong>{solicitacaoSelecionada.nome}</strong>.
|
||||
Você está prestes a aprovar a solicitação de acesso de <strong
|
||||
>{solicitacaoSelecionada.nome}</strong
|
||||
>.
|
||||
</p>
|
||||
<p class="text-sm text-base-content/60">
|
||||
Após aprovar, o sistema permitirá que esta pessoa solicite acesso ao sistema.
|
||||
Após aprovar, o sistema permitirá que esta pessoa solicite acesso
|
||||
ao sistema.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Observações (opcional)</span>
|
||||
</label>
|
||||
<div class="label">
|
||||
<span class="label-text font-semibold"
|
||||
>Observações (opcional)</span
|
||||
>
|
||||
</div>
|
||||
<textarea
|
||||
class="textarea textarea-bordered"
|
||||
placeholder="Adicione observações sobre a aprovação..."
|
||||
@@ -600,17 +663,17 @@
|
||||
rows="3"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal-action">
|
||||
<button
|
||||
class="btn btn-ghost"
|
||||
<button
|
||||
class="btn btn-ghost"
|
||||
onclick={fecharAprovar}
|
||||
disabled={processando}
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-success"
|
||||
<button
|
||||
class="btn btn-success"
|
||||
onclick={aprovarSolicitacao}
|
||||
disabled={processando}
|
||||
>
|
||||
@@ -618,9 +681,7 @@
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
Processando...
|
||||
{:else}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<Check class="h-5 w-5" />
|
||||
Confirmar Aprovação
|
||||
{/if}
|
||||
</button>
|
||||
@@ -637,20 +698,25 @@
|
||||
<dialog class="modal modal-open">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-2xl mb-4">Rejeitar Solicitação</h3>
|
||||
|
||||
|
||||
<div class="mb-4">
|
||||
<p class="text-base-content/70 mb-2">
|
||||
Você está prestes a rejeitar a solicitação de acesso de <strong>{solicitacaoSelecionada.nome}</strong>.
|
||||
Você está prestes a rejeitar a solicitação de acesso de <strong
|
||||
>{solicitacaoSelecionada.nome}</strong
|
||||
>.
|
||||
</p>
|
||||
<p class="text-sm text-base-content/60">
|
||||
Esta ação não pode ser desfeita. Recomendamos adicionar um motivo para a rejeição.
|
||||
Esta ação não pode ser desfeita. Recomendamos adicionar um motivo
|
||||
para a rejeição.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Motivo da Rejeição (recomendado)</span>
|
||||
</label>
|
||||
<div class="label">
|
||||
<span class="label-text font-semibold"
|
||||
>Motivo da Rejeição (recomendado)</span
|
||||
>
|
||||
</div>
|
||||
<textarea
|
||||
class="textarea textarea-bordered"
|
||||
placeholder="Descreva o motivo da rejeição..."
|
||||
@@ -658,17 +724,17 @@
|
||||
rows="3"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal-action">
|
||||
<button
|
||||
class="btn btn-ghost"
|
||||
<button
|
||||
class="btn btn-ghost"
|
||||
onclick={fecharRejeitar}
|
||||
disabled={processando}
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-error"
|
||||
<button
|
||||
class="btn btn-error"
|
||||
onclick={rejeitarSolicitacao}
|
||||
disabled={processando}
|
||||
>
|
||||
@@ -676,9 +742,7 @@
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
Processando...
|
||||
{:else}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<X class="h-5 w-5" />
|
||||
Confirmar Rejeição
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user