feat: update ESLint and TypeScript configurations across frontend and backend; enhance component structure and improve data handling in various modules

This commit is contained in:
2025-12-02 16:36:02 -03:00
parent f48d28067c
commit d79e6959c3
215 changed files with 29474 additions and 28173 deletions

View File

@@ -449,7 +449,7 @@
{#if card.href && !card.disabled}
<a
href={resolve(card.href)}
class={`group relative flex cursor-pointer items-center gap-4 overflow-hidden rounded-2xl border ${paletteStyles[card.palette].cardBorder} bg-base-100/90 p-6 shadow-lg transition-all duration-300 hover:shadow-xl hover:scale-[1.02]`}
class={`group relative flex cursor-pointer items-center gap-4 overflow-hidden rounded-2xl border ${paletteStyles[card.palette].cardBorder} bg-base-100/90 p-6 shadow-lg transition-all duration-300 hover:scale-[1.02] hover:shadow-xl`}
>
<div
class="from-base-200/40 absolute inset-x-6 top-0 h-24 rounded-b-full bg-linear-to-b to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100"
@@ -482,7 +482,7 @@
</a>
{:else}
<article
class={`group relative flex cursor-not-allowed items-center gap-4 overflow-hidden rounded-2xl border ${paletteStyles[card.palette].cardBorder} bg-base-100/50 p-6 shadow-lg opacity-60`}
class={`group relative flex cursor-not-allowed items-center gap-4 overflow-hidden rounded-2xl border ${paletteStyles[card.palette].cardBorder} bg-base-100/50 p-6 opacity-60 shadow-lg`}
>
<div
class={`relative flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl ${paletteStyles[card.palette].iconBg} ${paletteStyles[card.palette].iconRing}`}

File diff suppressed because it is too large Load Diff

View File

@@ -44,7 +44,6 @@
}
});
// Ativar HTTPS automaticamente se domínio contém porta 8443
$effect(() => {
if (domain.includes(':8443')) {
@@ -348,7 +347,6 @@
</div>
</div>
<!-- Ações -->
<div class="card-actions mt-6 justify-end gap-3">
<button

View File

@@ -53,7 +53,12 @@
}
// Validação dos nomes
if (!nomeEntrada.trim() || !nomeSaidaAlmoco.trim() || !nomeRetornoAlmoco.trim() || !nomeSaida.trim()) {
if (
!nomeEntrada.trim() ||
!nomeSaidaAlmoco.trim() ||
!nomeRetornoAlmoco.trim() ||
!nomeSaida.trim()
) {
mostrarMensagem('error', 'Preencha todos os nomes dos registros');
return;
}
@@ -69,35 +74,35 @@
nomeEntrada: nomeEntrada.trim(),
nomeSaidaAlmoco: nomeSaidaAlmoco.trim(),
nomeRetornoAlmoco: nomeRetornoAlmoco.trim(),
nomeSaida: nomeSaida.trim(),
nomeSaida: nomeSaida.trim()
});
mostrarMensagem('success', 'Configuração salva com sucesso!');
} catch (error) {
console.error('Erro ao salvar configuração:', error);
mostrarMensagem('error', error instanceof Error ? error.message : 'Erro ao salvar configuração');
mostrarMensagem(
'error',
error instanceof Error ? error.message : 'Erro ao salvar configuração'
);
} finally {
processando = false;
}
}
</script>
<div class="container mx-auto px-4 py-6 max-w-4xl">
<div class="container mx-auto max-w-4xl px-4 py-6">
<!-- Header -->
<div class="flex items-center justify-between mb-6">
<div class="mb-6 flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="p-3 bg-primary/10 rounded-xl">
<Clock class="h-8 w-8 text-primary" strokeWidth={2} />
<div class="bg-primary/10 rounded-xl p-3">
<Clock class="text-primary h-8 w-8" strokeWidth={2} />
</div>
<div>
<h1 class="text-3xl font-bold text-base-content">Configurações de Ponto</h1>
<h1 class="text-base-content text-3xl font-bold">Configurações de Ponto</h1>
<p class="text-base-content/60 mt-1">Configure os horários de trabalho e tolerâncias</p>
</div>
</div>
<a
href={resolve('/ti/configuracoes-ponto/enderecos')}
class="btn btn-secondary gap-2"
>
<a href={resolve('/ti/configuracoes-ponto/enderecos')} class="btn btn-secondary gap-2">
<MapPin class="h-5 w-5" />
Endereços de Marcação
</a>
@@ -120,7 +125,7 @@
<div class="card-body">
<h2 class="card-title mb-4">Horários de Trabalho</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<!-- Entrada -->
<div class="form-control">
<label class="label" for="horario-entrada">
@@ -200,11 +205,11 @@
<!-- Nomes Personalizados dos Registros -->
<h2 class="card-title mb-4">Nomes dos Registros</h2>
<p class="text-sm text-base-content/70 mb-4">
<p class="text-base-content/70 mb-4 text-sm">
Personalize os nomes exibidos para cada tipo de registro de ponto
</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<!-- Nome Entrada -->
<div class="form-control">
<label class="label" for="nome-entrada">
@@ -275,12 +280,8 @@
</div>
<!-- Ações -->
<div class="card-actions justify-end mt-6">
<button
class="btn btn-primary"
onclick={salvarConfiguracao}
disabled={processando}
>
<div class="card-actions mt-6 justify-end">
<button class="btn btn-primary" onclick={salvarConfiguracao} disabled={processando}>
{#if processando}
<span class="loading loading-spinner loading-sm"></span>
{:else}
@@ -292,4 +293,3 @@
</div>
</div>
</div>

View File

@@ -51,7 +51,7 @@
// Função para buscar endereço por CEP usando ViaCEP
async function buscarEnderecoPorCEP() {
const cepLimpo = onlyDigits(cep);
if (cepLimpo.length !== 8) {
return;
}
@@ -71,7 +71,7 @@
bairro = data.bairro || '';
cidade = data.localidade || '';
estado = data.uf || '';
// Formatar CEP com máscara
cep = maskCEP(cepLimpo);
@@ -79,7 +79,9 @@
if (endereco && cidade && estado) {
await buscarCoordenadasPorEndereco();
} else {
toast.success('Endereço encontrado! Preencha o número do endereço e busque as coordenadas.');
toast.success(
'Endereço encontrado! Preencha o número do endereço e busque as coordenadas.'
);
}
} catch (error) {
console.error('Erro ao buscar CEP:', error);
@@ -107,21 +109,21 @@
// Tentar primeiro com Google Maps Geocoding API (se API key estiver configurada)
const googleApiKey = import.meta.env.VITE_GOOGLE_MAPS_API_KEY;
if (googleApiKey) {
try {
const googleUrl = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(enderecoCompleto)}&key=${googleApiKey}&language=pt-BR&region=br`;
const googleResponse = await fetch(googleUrl);
const googleData = await googleResponse.json();
if (googleData.status === 'OK' && googleData.results && googleData.results.length > 0) {
const resultado = googleData.results[0];
const location = resultado.geometry.location;
latitude = parseFloat(location.lat.toFixed(6));
longitude = parseFloat(location.lng.toFixed(6));
toast.success('Coordenadas encontradas via Google Maps!');
return;
}
@@ -132,7 +134,7 @@
// Fallback para Nominatim (OpenStreetMap) se Google Maps falhar ou não tiver API key
const nominatimUrl = `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(enderecoCompleto)}&limit=1&addressdetails=1`;
const response = await fetch(nominatimUrl, {
headers: {
'User-Agent': 'SGSE-App/1.0'
@@ -145,7 +147,7 @@
const resultado = data[0];
latitude = parseFloat(parseFloat(resultado.lat).toFixed(6));
longitude = parseFloat(parseFloat(resultado.lon).toFixed(6));
toast.success('Coordenadas encontradas via OpenStreetMap!');
} else {
toast.warning('Coordenadas não encontradas. Preencha manualmente.');
@@ -162,10 +164,10 @@
function handleCEPInput(event: Event) {
const target = event.target as HTMLInputElement;
const cepLimpo = onlyDigits(target.value);
// Aplicar máscara
cep = maskCEP(cepLimpo);
// Buscar endereço quando CEP estiver completo
if (cepLimpo.length === 8) {
setTimeout(() => buscarEnderecoPorCEP(), 500); // Delay para evitar múltiplas requisições
@@ -199,7 +201,7 @@
buscandoCoordenadas = false;
}
function abrirFormularioEdicao(enderecoParaEditar: typeof enderecos[number]) {
function abrirFormularioEdicao(enderecoParaEditar: (typeof enderecos)[number]) {
editandoId = enderecoParaEditar._id;
nome = enderecoParaEditar.nome;
descricao = enderecoParaEditar.descricao || '';
@@ -224,7 +226,12 @@
return;
}
if (latitude === '' || longitude === '' || typeof latitude !== 'number' || typeof longitude !== 'number') {
if (
latitude === '' ||
longitude === '' ||
typeof latitude !== 'number' ||
typeof longitude !== 'number'
) {
toast.error('Latitude e longitude são obrigatórias');
return;
}
@@ -265,7 +272,7 @@
estado: estado.trim(),
pais: pais.trim() || undefined,
raioMetros,
tipo,
tipo
});
toast.success('Endereço atualizado com sucesso!');
} else {
@@ -281,7 +288,7 @@
estado: estado.trim(),
pais: pais.trim() || undefined,
raioMetros,
tipo,
tipo
});
toast.success('Endereço criado com sucesso!');
}
@@ -315,7 +322,7 @@
try {
await client.mutation(api.enderecosMarcacao.atualizarEndereco, {
enderecoId,
ativo: true,
ativo: true
});
toast.success('Endereço ativado com sucesso!');
} catch (error) {
@@ -328,22 +335,20 @@
sede: 'Sede Principal',
home_office: 'Home Office',
deslocamento: 'Deslocamento',
cliente: 'Cliente',
cliente: 'Cliente'
};
</script>
<div class="container mx-auto px-4 py-6 max-w-7xl">
<div class="container mx-auto max-w-7xl px-4 py-6">
<!-- Header -->
<div class="flex items-center justify-between mb-6">
<div class="mb-6 flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="p-3 bg-primary/10 rounded-xl">
<MapPin class="h-8 w-8 text-primary" />
<div class="bg-primary/10 rounded-xl p-3">
<MapPin class="text-primary h-8 w-8" />
</div>
<div>
<h1 class="text-3xl font-bold text-base-content">Endereços de Marcação</h1>
<p class="text-base-content/60 mt-1">
Gerenciar locais permitidos para registro de ponto
</p>
<h1 class="text-base-content text-3xl font-bold">Endereços de Marcação</h1>
<p class="text-base-content/60 mt-1">Gerenciar locais permitidos para registro de ponto</p>
</div>
</div>
<button
@@ -362,23 +367,28 @@
<!-- Formulário Modal -->
{#if mostrarFormulario}
<div class="modal modal-open">
<div class="modal-box max-w-4xl max-h-[90vh] overflow-y-auto">
<div class="modal-box max-h-[90vh] max-w-4xl overflow-y-auto">
<!-- Header do Modal -->
<div class="flex items-center justify-between mb-6 pb-4 border-b border-base-300">
<div class="border-base-300 mb-6 flex items-center justify-between border-b pb-4">
<div class="flex items-center gap-3">
<div class="p-2 bg-primary/10 rounded-lg">
<MapPin class="h-6 w-6 text-primary" strokeWidth={2.5} />
<div class="bg-primary/10 rounded-lg p-2">
<MapPin class="text-primary h-6 w-6" strokeWidth={2.5} />
</div>
<div>
<h2 class="text-2xl font-bold text-base-content">
<h2 class="text-base-content text-2xl font-bold">
{editandoId ? 'Editar Endereço' : 'Novo Endereço'}
</h2>
<p class="text-sm text-base-content/60 mt-1">
{editandoId ? 'Atualize as informações do endereço' : 'Preencha os dados do novo endereço de marcação'}
<p class="text-base-content/60 mt-1 text-sm">
{editandoId
? 'Atualize as informações do endereço'
: 'Preencha os dados do novo endereço de marcação'}
</p>
</div>
</div>
<button class="btn btn-sm btn-circle btn-ghost hover:btn-error transition-all" onclick={limparFormulario}>
<button
class="btn btn-sm btn-circle btn-ghost hover:btn-error transition-all"
onclick={limparFormulario}
>
<X class="h-5 w-5" />
</button>
</div>
@@ -386,16 +396,20 @@
<div class="space-y-6">
<!-- Seção 1: Informações Básicas -->
<div class="space-y-4">
<div class="flex items-center gap-2 mb-4">
<div class="h-px flex-1 bg-gradient-to-r from-transparent via-base-300 to-transparent"></div>
<h3 class="text-lg font-semibold text-base-content flex items-center gap-2">
<div class="mb-4 flex items-center gap-2">
<div
class="via-base-300 h-px flex-1 bg-gradient-to-r from-transparent to-transparent"
></div>
<h3 class="text-base-content flex items-center gap-2 text-lg font-semibold">
<span class="badge badge-primary badge-sm">1</span>
Informações Básicas
</h3>
<div class="h-px flex-1 bg-gradient-to-r from-transparent via-base-300 to-transparent"></div>
<div
class="via-base-300 h-px flex-1 bg-gradient-to-r from-transparent to-transparent"
></div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="form-control">
<label class="label">
<span class="label-text font-semibold">Nome *</span>
@@ -404,7 +418,7 @@
type="text"
bind:value={nome}
placeholder="Ex: Sede Principal, Home Office João"
class="input input-bordered input-primary focus:input-primary focus:ring-2 focus:ring-primary/20"
class="input input-bordered input-primary focus:input-primary focus:ring-primary/20 focus:ring-2"
/>
</div>
@@ -412,7 +426,10 @@
<label class="label">
<span class="label-text font-semibold">Tipo *</span>
</label>
<select bind:value={tipo} class="select select-bordered select-primary focus:select-primary focus:ring-2 focus:ring-primary/20">
<select
bind:value={tipo}
class="select select-bordered select-primary focus:select-primary focus:ring-primary/20 focus:ring-2"
>
<option value="sede">🏢 Sede Principal</option>
<option value="home_office">🏠 Home Office</option>
<option value="deslocamento">🚗 Deslocamento</option>
@@ -429,7 +446,7 @@
<textarea
bind:value={descricao}
placeholder="Descrição detalhada do endereço ou observações importantes..."
class="textarea textarea-bordered textarea-primary focus:textarea-primary focus:ring-2 focus:ring-primary/20"
class="textarea textarea-bordered textarea-primary focus:textarea-primary focus:ring-primary/20 focus:ring-2"
rows="3"
></textarea>
</div>
@@ -437,13 +454,17 @@
<!-- Seção 2: Endereço Físico -->
<div class="space-y-4">
<div class="flex items-center gap-2 mb-4">
<div class="h-px flex-1 bg-gradient-to-r from-transparent via-base-300 to-transparent"></div>
<h3 class="text-lg font-semibold text-base-content flex items-center gap-2">
<div class="mb-4 flex items-center gap-2">
<div
class="via-base-300 h-px flex-1 bg-gradient-to-r from-transparent to-transparent"
></div>
<h3 class="text-base-content flex items-center gap-2 text-lg font-semibold">
<span class="badge badge-primary badge-sm">2</span>
Endereço Físico
</h3>
<div class="h-px flex-1 bg-gradient-to-r from-transparent via-base-300 to-transparent"></div>
<div
class="via-base-300 h-px flex-1 bg-gradient-to-r from-transparent to-transparent"
></div>
</div>
<div class="form-control">
@@ -456,13 +477,16 @@
bind:value={endereco}
oninput={handleEnderecoChange}
placeholder="Ex: Rua Deputado Cunha Rabelo, 214"
class="input input-bordered input-primary focus:input-primary focus:ring-2 focus:ring-primary/20 flex-1"
class="input input-bordered input-primary focus:input-primary focus:ring-primary/20 flex-1 focus:ring-2"
/>
<button
type="button"
class="btn btn-primary gap-2"
onclick={buscarCoordenadasPorEndereco}
disabled={buscandoCoordenadas || !endereco.trim() || !cidade.trim() || !estado.trim()}
disabled={buscandoCoordenadas ||
!endereco.trim() ||
!cidade.trim() ||
!estado.trim()}
title="Buscar coordenadas GPS automaticamente"
>
{#if buscandoCoordenadas}
@@ -484,15 +508,17 @@
type="text"
bind:value={bairro}
placeholder="Ex: Boa Viagem, Centro, Pina"
class="input input-bordered focus:input-primary focus:ring-2 focus:ring-primary/20"
class="input input-bordered focus:input-primary focus:ring-primary/20 focus:ring-2"
/>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<div class="form-control">
<label class="label">
<span class="label-text font-semibold">CEP</span>
<span class="label-text-alt text-base-content/50">(Opcional - Busca automática)</span>
<span class="label-text-alt text-base-content/50"
>(Opcional - Busca automática)</span
>
</label>
<div class="relative">
<input
@@ -507,11 +533,11 @@
}}
placeholder="00000-000"
maxlength="9"
class="input input-bordered focus:input-primary focus:ring-2 focus:ring-primary/20 w-full pr-10"
class="input input-bordered focus:input-primary focus:ring-primary/20 w-full pr-10 focus:ring-2"
/>
{#if buscandoCEP}
<div class="absolute right-3 top-1/2 transform -translate-y-1/2">
<Loader2 class="h-5 w-5 animate-spin text-primary" />
<div class="absolute top-1/2 right-3 -translate-y-1/2 transform">
<Loader2 class="text-primary h-5 w-5 animate-spin" />
</div>
{/if}
</div>
@@ -534,7 +560,7 @@
type="text"
bind:value={cidade}
placeholder="Ex: Recife"
class="input input-bordered input-primary focus:input-primary focus:ring-2 focus:ring-primary/20"
class="input input-bordered input-primary focus:input-primary focus:ring-primary/20 focus:ring-2"
/>
</div>
@@ -547,7 +573,7 @@
bind:value={estado}
placeholder="Ex: PE"
maxlength="2"
class="input input-bordered input-primary focus:input-primary focus:ring-2 focus:ring-primary/20 uppercase"
class="input input-bordered input-primary focus:input-primary focus:ring-primary/20 uppercase focus:ring-2"
/>
</div>
</div>
@@ -561,37 +587,42 @@
type="text"
bind:value={pais}
placeholder="Brasil"
class="input input-bordered focus:input-primary focus:ring-2 focus:ring-primary/20"
class="input input-bordered focus:input-primary focus:ring-primary/20 focus:ring-2"
/>
</div>
</div>
<!-- Seção 3: Localização GPS e Configuração -->
<div class="space-y-4">
<div class="flex items-center gap-2 mb-4">
<div class="h-px flex-1 bg-gradient-to-r from-transparent via-base-300 to-transparent"></div>
<h3 class="text-lg font-semibold text-base-content flex items-center gap-2">
<div class="mb-4 flex items-center gap-2">
<div
class="via-base-300 h-px flex-1 bg-gradient-to-r from-transparent to-transparent"
></div>
<h3 class="text-base-content flex items-center gap-2 text-lg font-semibold">
<span class="badge badge-primary badge-sm">3</span>
Localização GPS e Configuração
</h3>
<div class="h-px flex-1 bg-gradient-to-r from-transparent via-base-300 to-transparent"></div>
<div
class="via-base-300 h-px flex-1 bg-gradient-to-r from-transparent to-transparent"
></div>
</div>
<div class="bg-base-200/50 rounded-xl p-4 border border-base-300 mb-4">
<div class="bg-base-200/50 border-base-300 mb-4 rounded-xl border p-4">
<div class="flex items-start gap-3">
<div class="p-2 bg-info/20 rounded-lg mt-0.5">
<MapPin class="h-5 w-5 text-info" strokeWidth={2} />
<div class="bg-info/20 mt-0.5 rounded-lg p-2">
<MapPin class="text-info h-5 w-5" strokeWidth={2} />
</div>
<div class="flex-1">
<p class="text-sm font-semibold text-base-content mb-1">Coordenadas GPS</p>
<p class="text-xs text-base-content/70">
As coordenadas são usadas para validar a localização dos registros de ponto. O sistema busca automaticamente via Google Maps (se configurado) ou OpenStreetMap.
<p class="text-base-content mb-1 text-sm font-semibold">Coordenadas GPS</p>
<p class="text-base-content/70 text-xs">
As coordenadas são usadas para validar a localização dos registros de ponto. O
sistema busca automaticamente via Google Maps (se configurado) ou OpenStreetMap.
</p>
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<div class="form-control">
<label class="label">
<span class="label-text font-semibold">Latitude *</span>
@@ -602,11 +633,11 @@
step="0.000001"
bind:value={latitude}
placeholder="-8.047600"
class="input input-bordered input-primary focus:input-primary focus:ring-2 focus:ring-primary/20 w-full"
class="input input-bordered input-primary focus:input-primary focus:ring-primary/20 w-full focus:ring-2"
/>
{#if buscandoCoordenadas}
<div class="absolute right-3 top-1/2 transform -translate-y-1/2">
<Loader2 class="h-4 w-4 animate-spin text-primary" />
<div class="absolute top-1/2 right-3 -translate-y-1/2 transform">
<Loader2 class="text-primary h-4 w-4 animate-spin" />
</div>
{/if}
</div>
@@ -625,11 +656,11 @@
step="0.000001"
bind:value={longitude}
placeholder="-34.877000"
class="input input-bordered input-primary focus:input-primary focus:ring-2 focus:ring-primary/20 w-full"
class="input input-bordered input-primary focus:input-primary focus:ring-primary/20 w-full focus:ring-2"
/>
{#if buscandoCoordenadas}
<div class="absolute right-3 top-1/2 transform -translate-y-1/2">
<Loader2 class="h-4 w-4 animate-spin text-primary" />
<div class="absolute top-1/2 right-3 -translate-y-1/2 transform">
<Loader2 class="text-primary h-4 w-4 animate-spin" />
</div>
{/if}
</div>
@@ -648,7 +679,7 @@
min="0"
max="50000"
placeholder="100"
class="input input-bordered input-primary focus:input-primary focus:ring-2 focus:ring-primary/20"
class="input input-bordered input-primary focus:input-primary focus:ring-primary/20 focus:ring-2"
/>
<div class="label">
<span class="label-text-alt text-base-content/50">
@@ -661,17 +692,17 @@
</div>
<!-- Footer do Modal -->
<div class="modal-action mt-6 pt-4 border-t border-base-300">
<button
class="btn btn-ghost hover:btn-error transition-all"
onclick={limparFormulario}
<div class="modal-action border-base-300 mt-6 border-t pt-4">
<button
class="btn btn-ghost hover:btn-error transition-all"
onclick={limparFormulario}
disabled={processando}
>
<X class="h-4 w-4 mr-2" />
<X class="mr-2 h-4 w-4" />
Cancelar
</button>
<button
class="btn btn-primary gap-2 shadow-md hover:shadow-lg transition-all"
class="btn btn-primary gap-2 shadow-md transition-all hover:shadow-lg"
onclick={salvarEndereco}
disabled={processando}
>
@@ -689,16 +720,18 @@
{/if}
<!-- Barra de Busca -->
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card bg-base-100 mb-6 shadow-xl">
<div class="card-body p-4">
<div class="form-control">
<div class="relative">
<Search class="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-base-content/50" />
<Search
class="text-base-content/50 absolute top-1/2 left-3 h-5 w-5 -translate-y-1/2 transform"
/>
<input
type="text"
bind:value={termoBusca}
placeholder="Buscar por nome, endereço, cidade, estado ou tipo..."
class="input input-bordered pl-10 w-full"
class="input input-bordered w-full pl-10"
/>
</div>
</div>
@@ -709,26 +742,22 @@
<div class="grid grid-cols-1 gap-4">
{#if enderecosFiltrados.length === 0}
<div class="card bg-base-100 shadow-xl">
<div class="card-body text-center py-12">
<MapPin class="h-16 w-16 text-base-content/20 mx-auto mb-4" />
<p class="text-lg text-base-content/60">
<div class="card-body py-12 text-center">
<MapPin class="text-base-content/20 mx-auto mb-4 h-16 w-16" />
<p class="text-base-content/60 text-lg">
{termoBusca ? 'Nenhum endereço encontrado' : 'Nenhum endereço cadastrado'}
</p>
</div>
</div>
{:else}
{#each enderecosFiltrados as enderecoItem (enderecoItem._id)}
<div
class="card bg-base-100 shadow-xl {enderecoItem.ativo ? '' : 'opacity-60'}"
>
<div class="card bg-base-100 shadow-xl {enderecoItem.ativo ? '' : 'opacity-60'}">
<div class="card-body">
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center gap-3 mb-2">
<div class="mb-2 flex items-center gap-3">
<h3 class="text-xl font-bold">{enderecoItem.nome}</h3>
<span
class="badge {enderecoItem.ativo ? 'badge-success' : 'badge-error'}"
>
<span class="badge {enderecoItem.ativo ? 'badge-success' : 'badge-error'}">
{enderecoItem.ativo ? 'Ativo' : 'Inativo'}
</span>
<span class="badge badge-outline">
@@ -742,19 +771,22 @@
<div class="space-y-1 text-sm">
<p class="text-base-content/80">
<strong>Endereço:</strong> {enderecoItem.endereco}
<strong>Endereço:</strong>
{enderecoItem.endereco}
{#if (enderecoItem as any).bairro}
- <strong>Bairro:</strong> {(enderecoItem as any).bairro}
{/if}
</p>
<p class="text-base-content/80">
<strong>Cidade:</strong> {enderecoItem.cidade}/{enderecoItem.estado}
<strong>Cidade:</strong>
{enderecoItem.cidade}/{enderecoItem.estado}
{#if enderecoItem.cep}
- CEP: {enderecoItem.cep}
{/if}
</p>
<p class="text-base-content/80">
<strong>Coordenadas:</strong> {enderecoItem.latitude.toFixed(6)}, {enderecoItem.longitude.toFixed(6)}
<strong>Coordenadas:</strong>
{enderecoItem.latitude.toFixed(6)}, {enderecoItem.longitude.toFixed(6)}
</p>
<p class="text-base-content/80">
<strong>Raio Permitido:</strong>
@@ -801,4 +833,3 @@
{/if}
</div>
</div>

View File

@@ -30,14 +30,15 @@
servidorNTP = configQuery.data.servidorNTP || 'pool.ntp.org';
portaNTP = configQuery.data.portaNTP || 123;
usarServidorExterno = configQuery.data.usarServidorExterno || false;
fallbackParaPC = configQuery.data.fallbackParaPC !== undefined ? configQuery.data.fallbackParaPC : true;
fallbackParaPC =
configQuery.data.fallbackParaPC !== undefined ? configQuery.data.fallbackParaPC : true;
gmtOffset = configQuery.data.gmtOffset ?? -3; // Padrão GMT-3 para Brasília
// Atualizar status de sincronização
statusSincronizacao = {
ultimaSincronizacao: configQuery.data.ultimaSincronizacao ?? null,
offsetSegundos: configQuery.data.offsetSegundos ?? null,
usandoServidorExterno: configQuery.data.usarServidorExterno || false,
usandoServidorExterno: configQuery.data.usarServidorExterno || false
};
}
});
@@ -102,7 +103,7 @@
let timestampAjustado: number;
if (ajusteGMT !== 0) {
// Aplicar offset configurado
timestampAjustado = timestamp + (ajusteGMT * 60 * 60 * 1000);
timestampAjustado = timestamp + ajusteGMT * 60 * 60 * 1000;
} else {
// Quando GMT = 0, manter timestamp UTC puro
// O toLocaleTimeString() converterá automaticamente para o timezone local do navegador
@@ -113,7 +114,7 @@
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
hour12: false
});
}
@@ -125,7 +126,7 @@
let timestampAjustado: number;
if (ajusteGMT !== 0) {
// Aplicar offset configurado
timestampAjustado = timestamp + (ajusteGMT * 60 * 60 * 1000);
timestampAjustado = timestamp + ajusteGMT * 60 * 60 * 1000;
} else {
// Quando GMT = 0, manter timestamp UTC puro
// O toLocaleDateString() converterá automaticamente para o timezone local do navegador
@@ -135,7 +136,7 @@
return data.toLocaleDateString('pt-BR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
year: 'numeric'
});
}
@@ -172,7 +173,7 @@
'9': 'JST (Japan Standard Time)',
'10': 'AEST (Australian Eastern Standard Time)',
'11': 'SBT (Solomon Islands Time)',
'12': 'NZST (New Zealand Standard Time)',
'12': 'NZST (New Zealand Standard Time)'
};
return fusos[offset.toString()] || `UTC${offset >= 0 ? '+' : ''}${offset}`;
}
@@ -210,16 +211,19 @@
portaNTP: usarServidorExterno ? portaNTP : undefined,
usarServidorExterno,
fallbackParaPC,
gmtOffset,
gmtOffset
});
mostrarMensagem('success', 'Configuração salva com sucesso!');
// Recarregar configuração para atualizar status
// A query será atualizada automaticamente pelo useQuery
} catch (error) {
console.error('Erro ao salvar configuração:', error);
mostrarMensagem('error', error instanceof Error ? error.message : 'Erro ao salvar configuração');
mostrarMensagem(
'error',
error instanceof Error ? error.message : 'Erro ao salvar configuração'
);
} finally {
processando = false;
}
@@ -235,18 +239,18 @@
statusSincronizacao = {
ultimaSincronizacao: Date.now(),
offsetSegundos: resultado.offsetSegundos,
usandoServidorExterno: resultado.usandoServidorExterno,
usandoServidorExterno: resultado.usandoServidorExterno
};
// Atualizar timestamps dos relógios
timestampUTC = resultado.timestamp;
timestampOriginal = resultado.timestamp;
// Calcular horário atual com GMT offset
// Quando GMT é 0, usar timestamp UTC puro e deixar toLocaleTimeString() fazer a conversão automática
let timestampAjustado: number;
if (gmtOffset !== 0) {
timestampAjustado = timestampUTC + (gmtOffset * 60 * 60 * 1000);
timestampAjustado = timestampUTC + gmtOffset * 60 * 60 * 1000;
} else {
// Quando GMT = 0, manter timestamp UTC puro
// O toLocaleTimeString() converterá automaticamente para o timezone local do navegador
@@ -255,9 +259,9 @@
const horarioAtual = new Date(timestampAjustado).toLocaleTimeString('pt-BR', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
second: '2-digit'
});
mostrarMensagem(
'success',
resultado.usandoServidorExterno
@@ -269,7 +273,10 @@
}
} catch (error) {
console.error('Erro ao testar sincronização:', error);
mostrarMensagem('error', error instanceof Error ? error.message : 'Erro ao testar sincronização');
mostrarMensagem(
'error',
error instanceof Error ? error.message : 'Erro ao testar sincronização'
);
} finally {
testando = false;
}
@@ -280,7 +287,7 @@
await obterTempoSincronizado();
mostrarMensagem('success', 'Relógios atualizados!');
}
function formatarDataHora(timestamp: number | null): string {
if (!timestamp) return 'Nunca';
return new Date(timestamp).toLocaleString('pt-BR', {
@@ -289,20 +296,20 @@
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
second: '2-digit'
});
}
</script>
<div class="container mx-auto px-4 py-6 max-w-4xl">
<div class="container mx-auto max-w-4xl px-4 py-6">
<!-- Header -->
<div class="flex items-center justify-between mb-6">
<div class="mb-6 flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="p-3 bg-primary/10 rounded-xl">
<Clock class="h-8 w-8 text-primary" strokeWidth={2} />
<div class="bg-primary/10 rounded-xl p-3">
<Clock class="text-primary h-8 w-8" strokeWidth={2} />
</div>
<div>
<h1 class="text-3xl font-bold text-base-content">Configurações de Relógio</h1>
<h1 class="text-base-content text-3xl font-bold">Configurações de Relógio</h1>
<p class="text-base-content/60 mt-1">Configure a sincronização de tempo do sistema</p>
</div>
</div>
@@ -325,9 +332,9 @@
{/if}
<!-- Relógios em Tempo Real -->
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card bg-base-100 mb-6 shadow-xl">
<div class="card-body">
<div class="flex items-center justify-between mb-4">
<div class="mb-4 flex items-center justify-between">
<h2 class="card-title">Relógios Sincronizados</h2>
<button
class="btn btn-sm btn-outline btn-info"
@@ -338,111 +345,123 @@
Atualizar Relógios
</button>
</div>
<p class="text-sm text-base-content/70 mb-6">
Visualização em tempo real dos horários sincronizados. O primeiro relógio mostra o horário original (UTC) da fonte de sincronismo escolhida, e o segundo mostra o horário ajustado conforme o GMT configurado.
<p class="text-base-content/70 mb-6 text-sm">
Visualização em tempo real dos horários sincronizados. O primeiro relógio mostra o horário
original (UTC) da fonte de sincronismo escolhida, e o segundo mostra o horário ajustado
conforme o GMT configurado.
</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<!-- Relógio Original (UTC) -->
<div class="bg-gradient-to-br from-blue-500/10 to-blue-600/10 rounded-xl p-6 border-2 border-blue-500/30">
<div
class="rounded-xl border-2 border-blue-500/30 bg-gradient-to-br from-blue-500/10 to-blue-600/10 p-6"
>
<div class="text-center">
<div class="text-sm font-semibold text-blue-600 uppercase tracking-wide mb-2">
<div class="mb-2 text-sm font-semibold tracking-wide text-blue-600 uppercase">
Horário Original (UTC)
</div>
<div class="text-5xl font-bold text-blue-700 mb-2 font-mono">
<div class="mb-2 font-mono text-5xl font-bold text-blue-700">
{formatarRelogio(timestampOriginal, 0)}
</div>
<div class="text-sm text-blue-600/80 mb-1">
<div class="mb-1 text-sm text-blue-600/80">
{formatarDataRelogio(timestampOriginal, 0)}
</div>
<!-- Divider -->
<div class="divider my-3 opacity-30"></div>
<!-- Detalhes do Fuso Horário -->
<div class="text-left space-y-2 mt-4">
<div class="mt-4 space-y-2 text-left">
<div class="text-xs">
<span class="font-semibold text-blue-700">Fuso Horário:</span>
<span class="text-blue-600/90 ml-1">UTC / GMT (Greenwich Mean Time)</span>
<span class="ml-1 text-blue-600/90">UTC / GMT (Greenwich Mean Time)</span>
</div>
<div class="text-xs">
<span class="font-semibold text-blue-700">Offset UTC:</span>
<span class="text-blue-600/90 ml-1">±00:00 (Coordenado Universal)</span>
<span class="ml-1 text-blue-600/90">±00:00 (Coordenado Universal)</span>
</div>
<div class="text-xs">
<span class="font-semibold text-blue-700">Fonte:</span>
<span class="text-blue-600/90 ml-1">
{statusSincronizacao?.usandoServidorExterno ? `Servidor NTP (${servidorNTP})` : 'Relógio do PC'}
<span class="ml-1 text-blue-600/90">
{statusSincronizacao?.usandoServidorExterno
? `Servidor NTP (${servidorNTP})`
: 'Relógio do PC'}
</span>
</div>
{#if statusSincronizacao?.offsetSegundos !== null && statusSincronizacao?.offsetSegundos !== undefined}
<div class="text-xs">
<span class="font-semibold text-blue-700">Offset Calculado:</span>
<span class="text-blue-600/90 ml-1">
{statusSincronizacao.offsetSegundos > 0 ? '+' : ''}{statusSincronizacao.offsetSegundos}s
<span class="ml-1 text-blue-600/90">
{statusSincronizacao.offsetSegundos > 0
? '+'
: ''}{statusSincronizacao.offsetSegundos}s
</span>
</div>
{/if}
{#if timestampOriginal}
<div class="text-xs">
<span class="font-semibold text-blue-700">Timestamp UTC:</span>
<span class="text-blue-600/90 ml-1 font-mono text-[10px]">
<span class="ml-1 font-mono text-[10px] text-blue-600/90">
{formatarTimestampISO(timestampOriginal)}
</span>
</div>
{/if}
<div class="text-xs">
<span class="font-semibold text-blue-700">Formato Recebido:</span>
<span class="text-blue-600/90 ml-1">ISO 8601 (UTC)</span>
<span class="ml-1 text-blue-600/90">ISO 8601 (UTC)</span>
</div>
</div>
</div>
</div>
<!-- Relógio com GMT Ajustado -->
<div class="bg-gradient-to-br from-primary/10 to-primary/20 rounded-xl p-6 border-2 border-primary/30">
<div
class="from-primary/10 to-primary/20 border-primary/30 rounded-xl border-2 bg-gradient-to-br p-6"
>
<div class="text-center">
<div class="text-sm font-semibold text-primary uppercase tracking-wide mb-2">
<div class="text-primary mb-2 text-sm font-semibold tracking-wide uppercase">
Horário com GMT {gmtOffset >= 0 ? '+' : ''}{gmtOffset}
</div>
<div class="text-5xl font-bold text-primary mb-2 font-mono">
<div class="text-primary mb-2 font-mono text-5xl font-bold">
{formatarRelogio(timestampUTC, gmtOffset)}
</div>
<div class="text-sm text-primary/80 mb-1">
<div class="text-primary/80 mb-1 text-sm">
{formatarDataRelogio(timestampUTC, gmtOffset)}
</div>
<!-- Divider -->
<div class="divider my-3 opacity-30"></div>
<!-- Detalhes do Fuso Horário Aplicado -->
<div class="text-left space-y-2 mt-4">
<div class="mt-4 space-y-2 text-left">
<div class="text-xs">
<span class="font-semibold text-primary">Fuso Horário:</span>
<span class="text-primary font-semibold">Fuso Horário:</span>
<span class="text-primary/90 ml-1">{obterNomeFusoHorario(gmtOffset)}</span>
</div>
<div class="text-xs">
<span class="font-semibold text-primary">Offset Configurado:</span>
<span class="text-primary font-semibold">Offset Configurado:</span>
<span class="text-primary/90 ml-1">
GMT{gmtOffset >= 0 ? '+' : ''}{gmtOffset} ({calcularDiferencaFuso(gmtOffset)})
</span>
</div>
<div class="text-xs">
<span class="font-semibold text-primary">Ajuste Aplicado:</span>
<span class="text-primary font-semibold">Ajuste Aplicado:</span>
<span class="text-primary/90 ml-1">
{gmtOffset >= 0 ? '+' : ''}{gmtOffset}:00 UTC
</span>
</div>
{#if timestampUTC}
<div class="text-xs">
<span class="font-semibold text-primary">Timestamp Local:</span>
<span class="text-primary font-semibold">Timestamp Local:</span>
<span class="text-primary/90 ml-1 font-mono text-[10px]">
{formatarTimestampISO(gmtOffset !== 0 ? timestampUTC + (gmtOffset * 60 * 60 * 1000) : timestampUTC)}
{formatarTimestampISO(
gmtOffset !== 0 ? timestampUTC + gmtOffset * 60 * 60 * 1000 : timestampUTC
)}
</span>
</div>
{/if}
<div class="text-xs">
<span class="font-semibold text-primary">Status:</span>
<span class="text-primary font-semibold">Status:</span>
<span class="text-primary/90 ml-1">Ajuste aplicado em tempo real</span>
</div>
</div>
@@ -475,7 +494,7 @@
</div>
{#if usarServidorExterno}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div class="mb-4 grid grid-cols-1 gap-4 md:grid-cols-2">
<!-- Servidor NTP -->
<div class="form-control">
<label class="label" for="servidor-ntp">
@@ -489,7 +508,8 @@
class="input input-bordered"
/>
<div class="label">
<span class="label-text-alt">Ex: pool.ntp.org, time.google.com, time.windows.com</span>
<span class="label-text-alt">Ex: pool.ntp.org, time.google.com, time.windows.com</span
>
</div>
</div>
@@ -517,11 +537,7 @@
<!-- Fallback para PC -->
<div class="form-control mb-4">
<label class="label cursor-pointer gap-3">
<input
type="checkbox"
bind:checked={fallbackParaPC}
class="checkbox checkbox-primary"
/>
<input type="checkbox" bind:checked={fallbackParaPC} class="checkbox checkbox-primary" />
<span class="label-text font-medium">Usar relógio do PC se servidor externo falhar</span>
</label>
<div class="label">
@@ -536,21 +552,22 @@
<!-- Ajuste de Fuso Horário (GMT) -->
<h2 class="card-title mb-4">Ajuste de Fuso Horário (GMT)</h2>
<p class="text-sm text-base-content/70 mb-4">
Configure o fuso horário para ajustar o horário de registro. Use valores negativos para fusos a oeste de UTC e positivos para fusos a leste.
<p class="text-base-content/70 mb-4 text-sm">
Configure o fuso horário para ajustar o horário de registro. Use valores negativos para
fusos a oeste de UTC e positivos para fusos a leste.
</p>
<div class="form-control">
<label class="label" for="gmt-offset">
<span class="label-text font-medium">GMT Offset (horas) *</span>
</label>
<select
id="gmt-offset"
bind:value={gmtOffset}
class="select select-bordered"
>
<select id="gmt-offset" bind:value={gmtOffset} class="select select-bordered">
{#each Array.from({ length: 49 }, (_, i) => i - 12) as offset}
<option value={offset} selected={gmtOffset === offset}>
GMT{offset >= 0 ? '+' : ''}{offset}{offset === -3 ? ' (Brasil - Brasília)' : offset === 0 ? ' (UTC)' : ''}
GMT{offset >= 0 ? '+' : ''}{offset}{offset === -3
? ' (Brasil - Brasília)'
: offset === 0
? ' (UTC)'
: ''}
</option>
{/each}
</select>
@@ -565,7 +582,7 @@
{#if statusSincronizacao}
<div class="divider"></div>
<h2 class="card-title mb-4">Status de Sincronização</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<div class="stat bg-base-200 rounded-lg p-4">
<div class="stat-title text-xs">Última Sincronização</div>
<div class="stat-value text-lg">
@@ -590,7 +607,7 @@
{/if}
<!-- Ações -->
<div class="card-actions justify-end mt-6 gap-3">
<div class="card-actions mt-6 justify-end gap-3">
{#if usarServidorExterno}
<button
class="btn btn-outline btn-info"
@@ -623,31 +640,31 @@
</div>
<!-- Informações -->
<div class="card bg-base-100 shadow-xl mt-6">
<div class="card bg-base-100 mt-6 shadow-xl">
<div class="card-body">
<h2 class="card-title mb-4">Informações</h2>
<div class="alert alert-info">
<AlertCircle class="h-6 w-6" />
<div>
<p>
<strong>Nota:</strong> O sistema usa uma API HTTP para sincronização de tempo como
aproximação do protocolo NTP. Para sincronização NTP real, seria necessário uma biblioteca
específica.
<strong>Nota:</strong> O sistema usa uma API HTTP para sincronização de tempo como aproximação
do protocolo NTP. Para sincronização NTP real, seria necessário uma biblioteca específica.
</p>
<p class="text-sm mt-1">
<p class="mt-1 text-sm">
Servidores NTP recomendados: pool.ntp.org, time.google.com, time.windows.com, ntp.br
</p>
<p class="text-sm mt-2">
<strong>Como funciona:</strong> O servidor NTP configurado é mapeado para uma API HTTP que retorna UTC.
O GMT offset configurado é então aplicado no frontend para exibir o horário correto.
<p class="mt-2 text-sm">
<strong>Como funciona:</strong> O servidor NTP configurado é mapeado para uma API HTTP que
retorna UTC. O GMT offset configurado é então aplicado no frontend para exibir o horário
correto.
</p>
<p class="text-sm mt-1">
<strong>Importante:</strong> Todos os servidores NTP retornam tempo em UTC. O GMT offset é aplicado
apenas uma vez no frontend para ajustar ao fuso horário local (ex: GMT-3 para Brasília).
<p class="mt-1 text-sm">
<strong>Importante:</strong> Todos os servidores NTP retornam tempo em UTC. O GMT offset
é aplicado apenas uma vez no frontend para ajustar ao fuso horário local (ex: GMT-3 para
Brasília).
</p>
</div>
</div>
</div>
</div>
</div>

View File

@@ -15,23 +15,25 @@
});
</script>
<div class="container mx-auto px-4 py-6 max-w-7xl">
<div class="container mx-auto max-w-7xl px-4 py-6">
<!-- Header -->
<div class="flex items-center justify-between mb-8">
<div class="mb-8 flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="p-3 bg-primary/10 rounded-xl">
<Shield class="h-8 w-8 text-primary" strokeWidth={2} />
<div class="bg-primary/10 rounded-xl p-3">
<Shield class="text-primary h-8 w-8" strokeWidth={2} />
</div>
<div>
<h1 class="text-3xl font-bold text-base-content">LGPD - Proteção de Dados</h1>
<p class="text-base-content/60 mt-1">Gestão de conformidade com a Lei Geral de Proteção de Dados</p>
<h1 class="text-base-content text-3xl font-bold">LGPD - Proteção de Dados</h1>
<p class="text-base-content/60 mt-1">
Gestão de conformidade com a Lei Geral de Proteção de Dados
</p>
</div>
</div>
</div>
<!-- Stats Cards -->
{#if estatisticas === undefined}
<div class="flex justify-center items-center py-20">
<div class="flex items-center justify-center py-20">
<span class="loading loading-spinner loading-lg text-primary"></span>
</div>
{:else if estatisticas?.error}
@@ -40,7 +42,7 @@
<span>Erro ao carregar estatísticas: {estatisticas.error}</span>
</div>
{:else if estatisticas?.data}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div class="mb-8 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
<StatsCard
title="Solicitações Pendentes"
value={estatisticas.data.solicitacoesPendentes}
@@ -76,10 +78,10 @@
{/if}
<!-- Ações Rápidas -->
<div class="card bg-base-100 shadow-xl mb-8">
<div class="card bg-base-100 mb-8 shadow-xl">
<div class="card-body">
<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">
<h2 class="card-title mb-4 text-2xl">Ações Rápidas</h2>
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<a href={resolve('/ti/lgpd/solicitacoes')} class="btn btn-primary btn-lg">
<FileText class="h-5 w-5" strokeWidth={2} />
Gerenciar Solicitações
@@ -100,14 +102,14 @@
<!-- Informações -->
{#if estatisticas?.data}
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<!-- Solicitações por Tipo -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-xl mb-4">Solicitações por Tipo</h2>
<h2 class="card-title mb-4 text-xl">Solicitações por Tipo</h2>
<div class="space-y-2">
{#each Object.entries(estatisticas.data.solicitacoesPorTipo) as [tipo, quantidade]}
<div class="flex justify-between items-center p-2 bg-base-200 rounded">
<div class="bg-base-200 flex items-center justify-between rounded p-2">
<span class="text-sm font-medium">{tipo}</span>
<span class="badge badge-primary">{quantidade}</span>
</div>
@@ -119,7 +121,7 @@
<!-- Resumo -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-xl mb-4">Resumo</h2>
<h2 class="card-title mb-4 text-xl">Resumo</h2>
<div class="space-y-3">
<div class="flex justify-between">
<span>Total de ROTs:</span>
@@ -127,7 +129,7 @@
</div>
<div class="flex justify-between">
<span>ROTs Ativos:</span>
<span class="font-semibold text-success">{estatisticas.data.rotsAtivos}</span>
<span class="text-success font-semibold">{estatisticas.data.rotsAtivos}</span>
</div>
<div class="flex justify-between">
<span>Total de Consentimentos:</span>
@@ -135,7 +137,9 @@
</div>
<div class="flex justify-between">
<span>Consentimentos Ativos:</span>
<span class="font-semibold text-success">{estatisticas.data.consentimentosAtivos}</span>
<span class="text-success font-semibold"
>{estatisticas.data.consentimentosAtivos}</span
>
</div>
</div>
</div>
@@ -143,4 +147,3 @@
</div>
{/if}
</div>

View File

@@ -2,7 +2,16 @@
import { resolve } from '$app/paths';
import { useQuery, useConvexClient } from 'convex-svelte';
import { api } from '@sgse-app/backend/convex/_generated/api';
import { Shield, Save, Mail, Phone, User, Calendar, ToggleLeft, ToggleRight } from 'lucide-svelte';
import {
Shield,
Save,
Mail,
Phone,
User,
Calendar,
ToggleLeft,
ToggleRight
} from 'lucide-svelte';
import { toast } from 'svelte-sonner';
const client = useConvexClient();
@@ -24,7 +33,8 @@
encarregadoNome = config.data.encarregadoNome || '';
encarregadoEmail = config.data.encarregadoEmail || '';
encarregadoTelefone = config.data.encarregadoTelefone || '';
encarregadoHorarioAtendimento = config.data.encarregadoHorarioAtendimento || 'Segunda a Sexta, das 8h às 17h';
encarregadoHorarioAtendimento =
config.data.encarregadoHorarioAtendimento || 'Segunda a Sexta, das 8h às 17h';
prazoRespostaPadrao = config.data.prazoRespostaPadrao;
diasAlertaVencimento = config.data.diasAlertaVencimento;
termoObrigatorio = config.data.termoObrigatorio;
@@ -57,32 +67,32 @@
}
</script>
<div class="container mx-auto px-4 py-6 max-w-4xl">
<div class="container mx-auto max-w-4xl px-4 py-6">
<!-- Header -->
<div class="flex items-center justify-between mb-8">
<div class="mb-8 flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="p-3 bg-primary/10 rounded-xl">
<Shield class="h-8 w-8 text-primary" strokeWidth={2} />
<div class="bg-primary/10 rounded-xl p-3">
<Shield class="text-primary h-8 w-8" strokeWidth={2} />
</div>
<div>
<h1 class="text-3xl font-bold text-base-content">Configurações LGPD</h1>
<h1 class="text-base-content text-3xl font-bold">Configurações LGPD</h1>
<p class="text-base-content/60 mt-1">Configure as definições de proteção de dados</p>
</div>
</div>
</div>
{#if config === undefined}
<div class="flex justify-center items-center py-20">
<div class="flex items-center justify-center py-20">
<span class="loading loading-spinner loading-lg text-primary"></span>
</div>
{:else}
<!-- Encarregado de Dados -->
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card bg-base-100 mb-6 shadow-xl">
<div class="card-body">
<h2 class="card-title text-2xl mb-4">Encarregado de Proteção de Dados (DPO)</h2>
<h2 class="card-title mb-4 text-2xl">Encarregado de Proteção de Dados (DPO)</h2>
<p class="text-base-content/60 mb-6">
Configure os dados de contato do Encarregado de Proteção de Dados, responsável por
atender solicitações e questões relacionadas à LGPD.
Configure os dados de contato do Encarregado de Proteção de Dados, responsável por atender
solicitações e questões relacionadas à LGPD.
</p>
<div class="space-y-4">
@@ -91,7 +101,9 @@
<span class="label-text font-semibold">Nome do Encarregado</span>
</label>
<div class="relative">
<User class="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-base-content/40" />
<User
class="text-base-content/40 absolute top-1/2 left-3 h-5 w-5 -translate-y-1/2 transform"
/>
<input
type="text"
bind:value={encarregadoNome}
@@ -106,7 +118,9 @@
<span class="label-text font-semibold">E-mail</span>
</label>
<div class="relative">
<Mail class="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-base-content/40" />
<Mail
class="text-base-content/40 absolute top-1/2 left-3 h-5 w-5 -translate-y-1/2 transform"
/>
<input
type="email"
bind:value={encarregadoEmail}
@@ -121,7 +135,9 @@
<span class="label-text font-semibold">Telefone</span>
</label>
<div class="relative">
<Phone class="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-base-content/40" />
<Phone
class="text-base-content/40 absolute top-1/2 left-3 h-5 w-5 -translate-y-1/2 transform"
/>
<input
type="text"
bind:value={encarregadoTelefone}
@@ -136,7 +152,9 @@
<span class="label-text font-semibold">Horário de Atendimento</span>
</label>
<div class="relative">
<Calendar class="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-base-content/40" />
<Calendar
class="text-base-content/40 absolute top-1/2 left-3 h-5 w-5 -translate-y-1/2 transform"
/>
<input
type="text"
bind:value={encarregadoHorarioAtendimento}
@@ -155,16 +173,16 @@
</div>
<!-- Configurações de Termo de Consentimento -->
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card bg-base-100 mb-6 shadow-xl">
<div class="card-body">
<h2 class="card-title text-2xl mb-4">Termo de Consentimento</h2>
<h2 class="card-title mb-4 text-2xl">Termo de Consentimento</h2>
<p class="text-base-content/60 mb-6">
Configure se o termo de consentimento é obrigatório para acesso ao sistema.
</p>
<div class="space-y-4">
<div class="form-control">
<label class="cursor-pointer label justify-start gap-4">
<label class="label cursor-pointer justify-start gap-4">
<input
type="checkbox"
bind:checked={termoObrigatorio}
@@ -172,9 +190,9 @@
/>
<div>
<span class="label-text font-semibold">Termo de Consentimento Obrigatório</span>
<p class="text-sm text-base-content/60">
Quando habilitado, os usuários precisam aceitar o termo de consentimento
antes de acessar o sistema.
<p class="text-base-content/60 text-sm">
Quando habilitado, os usuários precisam aceitar o termo de consentimento antes de
acessar o sistema.
</p>
</div>
</label>
@@ -202,9 +220,9 @@
</div>
<!-- Configurações de Prazos -->
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card bg-base-100 mb-6 shadow-xl">
<div class="card-body">
<h2 class="card-title text-2xl mb-4">Configurações de Prazos</h2>
<h2 class="card-title mb-4 text-2xl">Configurações de Prazos</h2>
<p class="text-base-content/60 mb-6">
Configure os prazos para resposta de solicitações e alertas de vencimento.
</p>
@@ -215,7 +233,9 @@
<span class="label-text font-semibold">Prazo Padrão para Resposta (dias)</span>
</label>
<div class="relative">
<Calendar class="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-base-content/40" />
<Calendar
class="text-base-content/40 absolute top-1/2 left-3 h-5 w-5 -translate-y-1/2 transform"
/>
<input
type="number"
bind:value={prazoRespostaPadrao}
@@ -236,7 +256,9 @@
<span class="label-text font-semibold">Dias para Alerta de Vencimento</span>
</label>
<div class="relative">
<Calendar class="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-base-content/40" />
<Calendar
class="text-base-content/40 absolute top-1/2 left-3 h-5 w-5 -translate-y-1/2 transform"
/>
<input
type="number"
bind:value={diasAlertaVencimento}
@@ -270,4 +292,3 @@
</div>
{/if}
</div>

View File

@@ -10,7 +10,7 @@
const client = useConvexClient();
const registros = useQuery(api.lgpd.listarRegistrosTratamento, { ativo: undefined });
let mostrarFormulario = $state(false);
let mostrarFormulario = $state(false);
let finalidade = $state('');
let baseLegal = $state('');
let categoriasDados = $state<string[]>([]);
@@ -95,15 +95,15 @@ let mostrarFormulario = $state(false);
}
</script>
<div class="container mx-auto px-4 py-6 max-w-7xl">
<div class="container mx-auto max-w-7xl px-4 py-6">
<!-- Header -->
<div class="flex items-center justify-between mb-8">
<div class="mb-8 flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="p-3 bg-primary/10 rounded-xl">
<Shield class="h-8 w-8 text-primary" strokeWidth={2} />
<div class="bg-primary/10 rounded-xl p-3">
<Shield class="text-primary h-8 w-8" strokeWidth={2} />
</div>
<div>
<h1 class="text-3xl font-bold text-base-content">Registros de Tratamento (ROT)</h1>
<h1 class="text-base-content text-3xl font-bold">Registros de Tratamento (ROT)</h1>
<p class="text-base-content/60 mt-1">
Gerencie os registros de operações de tratamento de dados pessoais
</p>
@@ -117,9 +117,9 @@ let mostrarFormulario = $state(false);
<!-- Formulário -->
{#if mostrarFormulario}
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card bg-base-100 mb-6 shadow-xl">
<div class="card-body">
<h2 class="card-title text-2xl mb-4">Criar Novo Registro de Tratamento</h2>
<h2 class="card-title mb-4 text-2xl">Criar Novo Registro de Tratamento</h2>
<div class="space-y-4">
<div class="form-control">
@@ -152,7 +152,7 @@ let mostrarFormulario = $state(false);
</label>
<div class="flex flex-wrap gap-2">
{#each categoriasDadosDisponiveis as categoria}
<label class="cursor-pointer label gap-2">
<label class="label cursor-pointer gap-2">
<input
type="checkbox"
checked={categoriasDados.includes(categoria)}
@@ -171,7 +171,7 @@ let mostrarFormulario = $state(false);
</label>
<div class="flex flex-wrap gap-2">
{#each categoriasTitularesDisponiveis as categoria}
<label class="cursor-pointer label gap-2">
<label class="label cursor-pointer gap-2">
<input
type="checkbox"
checked={categoriasTitulares.includes(categoria)}
@@ -191,12 +191,11 @@ let mostrarFormulario = $state(false);
</label>
<div class="flex flex-wrap gap-2">
{#each medidasSegurancaDisponiveis as medida}
<label class="cursor-pointer label gap-2">
<label class="label cursor-pointer gap-2">
<input
type="checkbox"
checked={medidasSeguranca.includes(medida)}
onchange={() =>
(medidasSeguranca = toggleArrayItem(medidasSeguranca, medida))}
onchange={() => (medidasSeguranca = toggleArrayItem(medidasSeguranca, medida))}
class="checkbox checkbox-primary checkbox-sm"
/>
<span class="label-text text-sm">{medida}</span>
@@ -209,16 +208,11 @@ let mostrarFormulario = $state(false);
<label class="label">
<span class="label-text font-semibold">Prazo de Retenção (dias)</span>
</label>
<input
type="number"
bind:value={prazoRetencao}
min="1"
class="input input-bordered"
/>
<input type="number" bind:value={prazoRetencao} min="1" class="input input-bordered" />
</div>
<div class="form-control">
<label class="cursor-pointer label justify-start gap-4">
<label class="label cursor-pointer justify-start gap-4">
<input
type="checkbox"
bind:checked={compartilhamentoTerceiros}
@@ -241,10 +235,7 @@ let mostrarFormulario = $state(false);
</div>
<div class="flex justify-end gap-4">
<button
onclick={() => (mostrarFormulario = false)}
class="btn btn-ghost"
>
<button onclick={() => (mostrarFormulario = false)} class="btn btn-ghost">
Cancelar
</button>
<button onclick={salvar} disabled={carregando} class="btn btn-primary">
@@ -265,25 +256,25 @@ let mostrarFormulario = $state(false);
<!-- Lista de ROTs -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-2xl mb-4">Registros de Tratamento</h2>
<h2 class="card-title mb-4 text-2xl">Registros de Tratamento</h2>
{#if registros === undefined}
<div class="flex justify-center items-center py-20">
<div class="flex items-center justify-center py-20">
<span class="loading loading-spinner loading-lg text-primary"></span>
</div>
{:else if registros.length === 0}
<div class="text-center py-10">
<FileText class="h-16 w-16 text-base-content/30 mx-auto mb-4" />
<div class="py-10 text-center">
<FileText class="text-base-content/30 mx-auto mb-4 h-16 w-16" />
<p class="text-base-content/60">Nenhum registro de tratamento encontrado</p>
</div>
{:else}
<div class="space-y-4">
{#each registros as registro}
<div class="border border-base-300 rounded-lg p-4">
<div class="flex items-start justify-between mb-3">
<div class="border-base-300 rounded-lg border p-4">
<div class="mb-3 flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center gap-3 mb-2">
<h3 class="font-semibold text-lg">{registro.finalidade}</h3>
<div class="mb-2 flex items-center gap-3">
<h3 class="text-lg font-semibold">{registro.finalidade}</h3>
{#if registro.ativo}
<span class="badge badge-success">Ativo</span>
{:else}
@@ -291,9 +282,10 @@ let mostrarFormulario = $state(false);
{/if}
</div>
<div class="space-y-2 text-sm text-base-content/70">
<div class="text-base-content/70 space-y-2 text-sm">
<div>
<span class="font-semibold">Base Legal:</span> {registro.baseLegal}
<span class="font-semibold">Base Legal:</span>
{registro.baseLegal}
</div>
<div>
<span class="font-semibold">Categorias de Dados:</span>{' '}
@@ -322,7 +314,8 @@ let mostrarFormulario = $state(false);
</div>
{/if}
<div>
<span class="font-semibold">Responsável:</span> {registro.responsavelNome}
<span class="font-semibold">Responsável:</span>
{registro.responsavelNome}
</div>
<div>
<span class="font-semibold">Criado em:</span>{' '}
@@ -338,4 +331,3 @@ let mostrarFormulario = $state(false);
</div>
</div>
</div>

View File

@@ -32,13 +32,13 @@
let termoBusca = $state('');
const client = useConvexClient();
// Query reativa que atualiza quando os filtros mudam
const solicitacoesQuery = $derived({
status: statusFiltro || undefined,
tipo: tipoFiltro || undefined
});
const solicitacoes = useQuery(api.lgpd.listarSolicitacoes, solicitacoesQuery);
let solicitacaoSelecionada = $state<string | null>(null);
@@ -91,7 +91,7 @@
function filtrarSolicitacoes() {
// Verificar se solicitacoes existe e é um array
if (!solicitacoes || !Array.isArray(solicitacoes)) return [];
// Se não há termo de busca, retorna todas as solicitações
if (!termoBusca || termoBusca.trim() === '') return solicitacoes;
@@ -102,8 +102,8 @@
(s.usuarioNome?.toLowerCase().includes(busca) ?? false) ||
(s.usuarioEmail?.toLowerCase().includes(busca) ?? false) ||
(s.usuarioMatricula?.toLowerCase().includes(busca) ?? false) ||
(getTipoLabel(s.tipo).toLowerCase().includes(busca)) ||
(getStatusBadge(s.status).label.toLowerCase().includes(busca))
getTipoLabel(s.tipo).toLowerCase().includes(busca) ||
getStatusBadge(s.status).label.toLowerCase().includes(busca)
);
}
@@ -136,24 +136,26 @@
const solicitacoesFiltradas = $derived(filtrarSolicitacoes());
</script>
<div class="container mx-auto px-4 py-6 max-w-7xl">
<div class="container mx-auto max-w-7xl px-4 py-6">
<!-- Header -->
<div class="flex items-center justify-between mb-8">
<div class="mb-8 flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="p-3 bg-primary/10 rounded-xl">
<Shield class="h-8 w-8 text-primary" strokeWidth={2} />
<div class="bg-primary/10 rounded-xl p-3">
<Shield class="text-primary h-8 w-8" strokeWidth={2} />
</div>
<div>
<h1 class="text-3xl font-bold text-base-content">Gestão de Solicitações LGPD</h1>
<p class="text-base-content/60 mt-1">Responda e gerencie solicitações de direitos dos titulares</p>
<h1 class="text-base-content text-3xl font-bold">Gestão de Solicitações LGPD</h1>
<p class="text-base-content/60 mt-1">
Responda e gerencie solicitações de direitos dos titulares
</p>
</div>
</div>
</div>
<!-- Filtros -->
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card bg-base-100 mb-6 shadow-xl">
<div class="card-body">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="grid grid-cols-1 gap-4 md:grid-cols-4">
<div class="form-control">
<label class="label">
<span class="label-text font-semibold">Status</span>
@@ -187,7 +189,9 @@
<span class="label-text font-semibold">Buscar</span>
</label>
<div class="relative">
<Search class="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-base-content/40" />
<Search
class="text-base-content/40 absolute top-1/2 left-3 h-5 w-5 -translate-y-1/2 transform"
/>
<input
type="text"
bind:value={termoBusca}
@@ -203,7 +207,7 @@
<!-- Lista de Solicitações -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<div class="flex items-center justify-between mb-4">
<div class="mb-4 flex items-center justify-between">
<h2 class="card-title text-2xl">Solicitações</h2>
{#if solicitacoes && Array.isArray(solicitacoes)}
<div class="badge badge-outline">
@@ -213,12 +217,12 @@
</div>
{#if solicitacoes === undefined || solicitacoes === null}
<div class="flex justify-center items-center py-20">
<div class="flex items-center justify-center py-20">
<span class="loading loading-spinner loading-lg text-primary"></span>
</div>
{:else if !Array.isArray(solicitacoes) || solicitacoes.length === 0}
<div class="text-center py-10">
<FileText class="h-16 w-16 text-base-content/30 mx-auto mb-4" />
<div class="py-10 text-center">
<FileText class="text-base-content/30 mx-auto mb-4 h-16 w-16" />
<p class="text-base-content/60">
{#if statusFiltro || tipoFiltro}
Nenhuma solicitação encontrada com os filtros aplicados
@@ -240,13 +244,12 @@
{/if}
</div>
{:else if solicitacoesFiltradas.length === 0 && termoBusca}
<div class="text-center py-10">
<Search class="h-16 w-16 text-base-content/30 mx-auto mb-4" />
<p class="text-base-content/60">Nenhuma solicitação encontrada com o termo "{termoBusca}"</p>
<button
onclick={() => (termoBusca = '')}
class="btn btn-sm btn-outline mt-4"
>
<div class="py-10 text-center">
<Search class="text-base-content/30 mx-auto mb-4 h-16 w-16" />
<p class="text-base-content/60">
Nenhuma solicitação encontrada com o termo "{termoBusca}"
</p>
<button onclick={() => (termoBusca = '')} class="btn btn-sm btn-outline mt-4">
Limpar Busca
</button>
</div>
@@ -255,32 +258,34 @@
{#each solicitacoesFiltradas as solicitacao}
{@const statusInfo = getStatusBadge(solicitacao.status)}
{@const StatusIcon = getStatusIcon(solicitacao.status)}
<div class="border border-base-300 rounded-lg p-4">
<div class="flex items-start justify-between mb-3">
<div class="border-base-300 rounded-lg border p-4">
<div class="mb-3 flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center gap-3 mb-2">
<StatusIcon class="h-5 w-5 text-base-content/60" />
<h3 class="font-semibold text-lg">
<div class="mb-2 flex items-center gap-3">
<StatusIcon class="text-base-content/60 h-5 w-5" />
<h3 class="text-lg font-semibold">
{getTipoLabel(solicitacao.tipo)}
</h3>
<span class="badge {statusInfo.class}">{statusInfo.label}</span>
</div>
<div class="space-y-1 text-sm text-base-content/70">
<div class="text-base-content/70 space-y-1 text-sm">
<div>
<span class="font-semibold">Solicitante:</span> {solicitacao.usuarioNome}
<span class="font-semibold">Solicitante:</span>
{solicitacao.usuarioNome}
{#if solicitacao.usuarioMatricula}
({solicitacao.usuarioMatricula})
{/if}
</div>
<div>
<span class="font-semibold">E-mail:</span> {solicitacao.usuarioEmail}
<span class="font-semibold">E-mail:</span>
{solicitacao.usuarioEmail}
</div>
{#if solicitacao.consentimentoTermo}
<div class="flex items-center gap-2">
<span class="font-semibold">Termo de Consentimento:</span>
<span class="badge badge-success badge-sm">Aceito</span>
<span class="text-xs text-base-content/60">
<span class="text-base-content/60 text-xs">
(v.{solicitacao.consentimentoTermo.versao} em{' '}
{format(new Date(solicitacao.consentimentoTermo.aceitoEm), 'dd/MM/yyyy', {
locale: ptBR
@@ -302,11 +307,9 @@
{#if solicitacao.respondidoEm}
<div>
<span class="font-semibold">Respondida em:</span>{' '}
{format(
new Date(solicitacao.respondidoEm),
"dd/MM/yyyy 'às' HH:mm",
{ locale: ptBR }
)}
{format(new Date(solicitacao.respondidoEm), "dd/MM/yyyy 'às' HH:mm", {
locale: ptBR
})}
</div>
{#if solicitacao.respondidoPorNome}
<div>
@@ -346,7 +349,7 @@
{#if solicitacaoSelecionada}
<div class="modal modal-open">
<div class="modal-box">
<h3 class="font-bold text-lg mb-4">Responder Solicitação</h3>
<h3 class="mb-4 text-lg font-bold">Responder Solicitação</h3>
<div class="form-control mb-4">
<label class="label">
@@ -381,7 +384,11 @@
>
Cancelar
</button>
<button onclick={responder} disabled={!resposta.trim() || carregando} class="btn btn-primary">
<button
onclick={responder}
disabled={!resposta.trim() || carregando}
class="btn btn-primary"
>
{#if carregando}
<span class="loading loading-spinner loading-sm"></span>
Enviando...
@@ -395,4 +402,3 @@
</div>
{/if}
</div>

View File

@@ -24,7 +24,9 @@
</svg>
</div>
<div>
<h1 class="text-primary text-4xl font-bold">Monitoramento SGSE - Sistema de Gerenciamento de Secretaria</h1>
<h1 class="text-primary text-4xl font-bold">
Monitoramento SGSE - Sistema de Gerenciamento de Secretaria
</h1>
<p class="text-base-content/60 mt-2 text-lg">
Sistema de monitoramento técnico em tempo real
</p>

File diff suppressed because it is too large Load Diff

View File

@@ -70,7 +70,7 @@
processando = true;
const resultado = await client.mutation(api.templatesMensagens.excluirTemplate, {
templateId,
excluidoPorId: currentUser.data._id,
excluidoPorId: currentUser.data._id
});
if (resultado.sucesso) {
@@ -99,12 +99,12 @@
<div class="container mx-auto max-w-7xl px-4 py-8">
<div
class="rounded-2xl bg-base-100/80 shadow-xl border border-base-200/60 p-6 lg:p-8 space-y-6 backdrop-blur"
class="bg-base-100/80 border-base-200/60 space-y-6 rounded-2xl border p-6 shadow-xl backdrop-blur lg:p-8"
>
<!-- Header -->
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div class="flex items-center gap-4">
<div class="bg-gradient-to-br from-info/15 via-primary/10 to-secondary/10 rounded-2xl p-3">
<div class="from-info/15 via-primary/10 to-secondary/10 rounded-2xl bg-gradient-to-br p-3">
<svg
xmlns="http://www.w3.org/2000/svg"
class="text-info h-9 w-9"
@@ -123,8 +123,10 @@
<div>
<h1 class="text-base-content text-3xl font-bold">Gerenciar Templates</h1>
<p class="text-base-content/60 mt-1 text-sm lg:text-base">
Crie, edite e organize templates de <span class="font-semibold">chat</span> (texto puro) e
<span class="font-semibold">email HTML padronizado</span> usados em todas as notificações do SGSE.
Crie, edite e organize templates de <span class="font-semibold">chat</span> (texto puro)
e
<span class="font-semibold">email HTML padronizado</span> usados em todas as notificações
do SGSE.
</p>
</div>
</div>
@@ -136,7 +138,12 @@
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" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 19l-7-7m0 0l7-7m-7 7h18"
/>
</svg>
Voltar para Notificações
</a>
@@ -151,14 +158,18 @@
class:alert-info={mensagem.tipo === 'info'}
>
<span class="font-medium">{mensagem.texto}</span>
<button type="button" class="btn btn-sm btn-circle btn-ghost" onclick={() => (mensagem = null)}>
<button
type="button"
class="btn btn-sm btn-circle btn-ghost"
onclick={() => (mensagem = null)}
>
</button>
</div>
{/if}
<!-- Filtros e Busca -->
<div class="card bg-base-100 shadow-sm border border-base-200">
<div class="card bg-base-100 border-base-200 border shadow-sm">
<div class="card-body">
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="form-control">
@@ -188,7 +199,7 @@
</div>
<!-- Lista de Templates -->
<div class="card bg-base-100 shadow-sm border border-base-200">
<div class="card bg-base-100 border-base-200 border shadow-sm">
<div class="card-body">
<div class="mb-4 flex items-center justify-between">
<h2 class="card-title">Templates ({templatesFiltrados.length})</h2>
@@ -200,7 +211,12 @@
viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 4v16m8-8H4"
/>
</svg>
Novo Template
</a>
@@ -231,7 +247,7 @@
</td>
<td>
<div class="font-medium">{template.nome}</div>
<div class="text-sm text-base-content/60">{template.titulo}</div>
<div class="text-base-content/60 text-sm">{template.titulo}</div>
</td>
<td>
{#if template.tipo === 'sistema'}
@@ -251,7 +267,7 @@
{#if template.variaveis && template.variaveis.length > 0}
<div class="flex flex-wrap gap-1">
{#each template.variaveis.slice(0, 3) as variavel}
<span class="badge badge-sm">{{variavel}}</span>
<span class="badge badge-sm">{{ variavel }}</span>
{/each}
{#if template.variaveis.length > 3}
<span class="badge badge-sm">+{template.variaveis.length - 3}</span>
@@ -319,4 +335,3 @@
</div>
</div>
</div>

View File

@@ -9,21 +9,18 @@
const client = useConvexClient();
const currentUser = useQuery(api.auth.getCurrentUser as FunctionReference<'query'>);
const templateIdParam = $derived($page.params.id ?? '');
// Query específica para buscar o template por ID
const templateQuery = useQuery(
api.templatesMensagens.obterTemplatePorId,
() => {
if (!templateIdParam) return 'skip';
// Validar se o ID tem o formato correto do Convex
if (typeof templateIdParam === 'string' && templateIdParam.length > 0) {
return { templateId: templateIdParam as Id<'templatesMensagens'> };
}
return 'skip';
const templateQuery = useQuery(api.templatesMensagens.obterTemplatePorId, () => {
if (!templateIdParam) return 'skip';
// Validar se o ID tem o formato correto do Convex
if (typeof templateIdParam === 'string' && templateIdParam.length > 0) {
return { templateId: templateIdParam as Id<'templatesMensagens'> };
}
);
return 'skip';
});
// Extrair template da query
const template = $derived.by(() => {
@@ -31,7 +28,11 @@
// useQuery retorna os dados diretamente
if (templateQuery && typeof templateQuery === 'object') {
// Se tem propriedade data, usar ela
if ('data' in templateQuery && templateQuery.data !== undefined && templateQuery.data !== null) {
if (
'data' in templateQuery &&
templateQuery.data !== undefined &&
templateQuery.data !== null
) {
return templateQuery.data as Doc<'templatesMensagens'> | null;
}
// Caso contrário, assumir que é o próprio template
@@ -134,11 +135,11 @@
<div class="container mx-auto max-w-4xl px-4 py-8">
<div
class="rounded-2xl bg-base-100/80 shadow-xl border border-base-200/60 p-6 lg:p-8 space-y-6 backdrop-blur"
class="bg-base-100/80 border-base-200/60 space-y-6 rounded-2xl border p-6 shadow-xl backdrop-blur lg:p-8"
>
<div class="mb-4 flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="bg-gradient-to-br from-info/15 via-primary/10 to-secondary/10 rounded-2xl p-3">
<div class="from-info/15 via-primary/10 to-secondary/10 rounded-2xl bg-gradient-to-br p-3">
<svg
xmlns="http://www.w3.org/2000/svg"
class="text-info h-9 w-9"
@@ -158,7 +159,8 @@
<h1 class="text-base-content text-3xl font-bold">Editar Template</h1>
<p class="text-base-content/60 mt-1 text-sm lg:text-base">
Ajuste o texto base usado em <span class="font-semibold">chat</span> e na versão HTML de
<span class="font-semibold">email</span>. Templates de sistema podem ter restrições de edição.
<span class="font-semibold">email</span>. Templates de sistema podem ter restrições de
edição.
</p>
</div>
</div>
@@ -172,7 +174,11 @@
</div>
{:else if erroTemplate}
<div class="alert alert-error">
<span>Erro ao carregar template: {typeof erroTemplate === 'string' ? erroTemplate : erroTemplate?.message || 'Erro desconhecido'}</span>
<span
>Erro ao carregar template: {typeof erroTemplate === 'string'
? erroTemplate
: erroTemplate?.message || 'Erro desconhecido'}</span
>
<a href={resolve('/ti/notificacoes/templates')} class="btn btn-sm btn-outline">
Voltar para Templates
</a>
@@ -180,7 +186,7 @@
{:else if !template}
<div class="alert alert-error">
<span>Template não encontrado. Verifique se o ID está correto.</span>
<div class="text-xs mt-2 opacity-70">ID: {templateIdParam}</div>
<div class="mt-2 text-xs opacity-70">ID: {templateIdParam}</div>
<a href={resolve('/ti/notificacoes/templates')} class="btn btn-sm btn-outline mt-2">
Voltar para Templates
</a>
@@ -206,117 +212,117 @@
</div>
{/if}
<div class="card bg-base-100 shadow-sm border border-base-200">
<div class="card bg-base-100 border-base-200 border shadow-sm">
<div class="card-body space-y-4">
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="form-control">
<label class="label" for="nome">
<span class="label-text font-medium">Nome *</span>
</label>
<input
id="nome"
type="text"
bind:value={nome}
class="input input-bordered"
maxlength="100"
/>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="form-control">
<label class="label" for="nome">
<span class="label-text font-medium">Nome *</span>
</label>
<input
id="nome"
type="text"
bind:value={nome}
class="input input-bordered"
maxlength="100"
/>
</div>
<div class="form-control">
<label class="label" for="titulo">
<span class="label-text font-medium">Título *</span>
</label>
<input
id="titulo"
type="text"
bind:value={titulo}
class="input input-bordered"
maxlength="200"
/>
</div>
</div>
<div class="form-control">
<label class="label" for="titulo">
<span class="label-text font-medium">Título *</span>
<label class="label" for="categoria">
<span class="label-text font-medium">Categoria</span>
</label>
<input
id="titulo"
type="text"
bind:value={titulo}
class="input input-bordered"
maxlength="200"
/>
</div>
</div>
<div class="form-control">
<label class="label" for="categoria">
<span class="label-text font-medium">Categoria</span>
</label>
<select id="categoria" bind:value={categoria} class="select select-bordered max-w-xs">
<option value="email">Email</option>
<option value="chat">Chat</option>
<option value="ambos">Ambos</option>
</select>
<label class="label">
<span class="label-text-alt">
<span class="font-semibold">Chat:</span> usa o texto puro do corpo. <span
class="font-semibold">Email:</span
> usa uma versão HTML profissional gerada automaticamente com cabeçalho e assinatura SGSE.
</span>
</label>
</div>
<div class="form-control">
<label class="label" for="corpo">
<span class="label-text font-medium">Corpo da Mensagem *</span>
</label>
<textarea
id="corpo"
bind:value={corpo}
class="textarea textarea-bordered h-40"
placeholder="Digite o conteúdo em TEXTO. Você pode usar &#123;&#123;variavel&#125;&#125; para valores dinâmicos."
></textarea>
<label class="label">
<span class="label-text-alt">
Este texto será usado diretamente nas mensagens de
<span class="font-semibold">chat</span>. Para
<span class="font-semibold">email</span>, o sistema gera automaticamente um layout HTML
padronizado com logo e assinatura.
</span>
</label>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="form-control">
<label class="label" for="variaveis">
<span class="label-text font-medium">Variáveis (opcional)</span>
</label>
<input
id="variaveis"
type="text"
bind:value={variaveisTexto}
class="input input-bordered"
placeholder="nome, data, valor"
/>
<label class="label" for="variaveis">
<select id="categoria" bind:value={categoria} class="select select-bordered max-w-xs">
<option value="email">Email</option>
<option value="chat">Chat</option>
<option value="ambos">Ambos</option>
</select>
<label class="label">
<span class="label-text-alt">
Liste as variáveis que podem ser usadas no corpo (separadas por vírgula ou ponto e
vírgula).
<span class="font-semibold">Chat:</span> usa o texto puro do corpo.
<span class="font-semibold">Email:</span> usa uma versão HTML profissional gerada automaticamente
com cabeçalho e assinatura SGSE.
</span>
</label>
</div>
<div class="form-control">
<label class="label" for="tags">
<span class="label-text font-medium">Tags (opcional)</span>
</label>
<input
id="tags"
type="text"
bind:value={tagsTexto}
class="input input-bordered"
placeholder="avisos, chamados, rh"
/>
</div>
</div>
<div class="mt-6 flex justify-end gap-3">
<a href={resolve('/ti/notificacoes/templates')} class="btn btn-ghost"> Cancelar </a>
<button class="btn btn-primary" onclick={salvar} disabled={salvando}>
{#if salvando}
<span class="loading loading-spinner loading-sm"></span>
Salvando...
{:else}
Salvar Alterações
{/if}
</button>
</div>
<div class="form-control">
<label class="label" for="corpo">
<span class="label-text font-medium">Corpo da Mensagem *</span>
</label>
<textarea
id="corpo"
bind:value={corpo}
class="textarea textarea-bordered h-40"
placeholder="Digite o conteúdo em TEXTO. Você pode usar &#123;&#123;variavel&#125;&#125; para valores dinâmicos."
></textarea>
<label class="label">
<span class="label-text-alt">
Este texto será usado diretamente nas mensagens de
<span class="font-semibold">chat</span>. Para
<span class="font-semibold">email</span>, o sistema gera automaticamente um layout
HTML padronizado com logo e assinatura.
</span>
</label>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="form-control">
<label class="label" for="variaveis">
<span class="label-text font-medium">Variáveis (opcional)</span>
</label>
<input
id="variaveis"
type="text"
bind:value={variaveisTexto}
class="input input-bordered"
placeholder="nome, data, valor"
/>
<label class="label" for="variaveis">
<span class="label-text-alt">
Liste as variáveis que podem ser usadas no corpo (separadas por vírgula ou ponto e
vírgula).
</span>
</label>
</div>
<div class="form-control">
<label class="label" for="tags">
<span class="label-text font-medium">Tags (opcional)</span>
</label>
<input
id="tags"
type="text"
bind:value={tagsTexto}
class="input input-bordered"
placeholder="avisos, chamados, rh"
/>
</div>
</div>
<div class="mt-6 flex justify-end gap-3">
<a href={resolve('/ti/notificacoes/templates')} class="btn btn-ghost"> Cancelar </a>
<button class="btn btn-primary" onclick={salvar} disabled={salvando}>
{#if salvando}
<span class="loading loading-spinner loading-sm"></span>
Salvando...
{:else}
Salvar Alterações
{/if}
</button>
</div>
</div>
</div>
{/if}

View File

@@ -1,181 +1,192 @@
<script lang="ts">
import { useQuery, useConvexClient } from "convex-svelte";
import { api } from "@sgse-app/backend/convex/_generated/api";
import StatsCard from "$lib/components/ti/StatsCard.svelte";
import { BarChart3, Users, CheckCircle2, Ban, Clock, Plus, Layers, FileText, Info, Shield, AlertTriangle } from "lucide-svelte";
import { useQuery, useConvexClient } from 'convex-svelte';
import { api } from '@sgse-app/backend/convex/_generated/api';
import StatsCard from '$lib/components/ti/StatsCard.svelte';
import {
BarChart3,
Users,
CheckCircle2,
Ban,
Clock,
Plus,
Layers,
FileText,
Info,
Shield,
AlertTriangle
} from 'lucide-svelte';
import { resolve } from "$app/paths";
const client = useConvexClient();
const usuariosQuery = useQuery(api.usuarios.listar, {});
const estatisticasLGPD = useQuery(api.lgpd.obterEstatisticasLGPD, {});
import { resolve } from '$app/paths';
const client = useConvexClient();
const usuariosQuery = useQuery(api.usuarios.listar, {});
const estatisticasLGPD = useQuery(api.lgpd.obterEstatisticasLGPD, {});
// Verificar se está carregando
const carregando = $derived(usuariosQuery === undefined);
// Verificar se está carregando
const carregando = $derived(usuariosQuery === undefined);
// Extrair dados dos usuários
const usuarios = $derived(usuariosQuery?.data ?? []);
// 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;
// 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
};
}
// 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
};
}
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
};
});
return {
total: usuarios.length,
ativos,
bloqueados,
inativos
};
});
</script>
<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">
<BarChart3 class="h-8 w-8 text-primary" strokeWidth={2} />
</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>
</div>
</div>
</div>
<div class="container mx-auto max-w-7xl px-4 py-6">
<!-- Header -->
<div class="mb-8 flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="bg-primary/10 rounded-xl p-3">
<BarChart3 class="text-primary h-8 w-8" strokeWidth={2} />
</div>
<div>
<h1 class="text-base-content text-3xl font-bold">Dashboard Administrativo TI</h1>
<p class="text-base-content/60 mt-1">Painel de controle e monitoramento do sistema</p>
</div>
</div>
</div>
<!-- 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={Users}
color="primary"
/>
<!-- Stats Cards -->
{#if stats}
<div class="mb-8 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
<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={CheckCircle2}
color="success"
/>
<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={CheckCircle2}
color="success"
/>
<StatsCard
title="Usuários Bloqueados"
value={stats.bloqueados}
description="Requerem atenção"
Icon={Ban}
color="error"
/>
<StatsCard
title="Usuários Bloqueados"
value={stats.bloqueados}
description="Requerem atenção"
Icon={Ban}
color="error"
/>
<StatsCard
title="Usuários Inativos"
value={stats.inativos}
description="Desativados"
Icon={Clock}
color="warning"
/>
</div>
{:else}
<div class="flex justify-center items-center py-20">
<span class="loading loading-spinner loading-lg text-primary"></span>
</div>
{/if}
<StatsCard
title="Usuários Inativos"
value={stats.inativos}
description="Desativados"
Icon={Clock}
color="warning"
/>
</div>
{:else}
<div class="flex items-center justify-center py-20">
<span class="loading loading-spinner loading-lg text-primary"></span>
</div>
{/if}
<!-- LGPD Stats Cards -->
{#if estatisticasLGPD}
<div class="card bg-base-100 shadow-xl mb-8">
<div class="card-body">
<div class="flex items-center justify-between mb-4">
<h2 class="card-title text-2xl">LGPD - Proteção de Dados</h2>
<a href={resolve("/ti/lgpd")} class="btn btn-sm btn-primary">
<Shield class="h-4 w-4" />
Acessar LGPD
</a>
</div>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<StatsCard
title="Solicitações Pendentes"
value={estatisticasLGPD.solicitacoesPendentes}
description="Aguardando resposta"
Icon={AlertTriangle}
color="warning"
/>
<!-- LGPD Stats Cards -->
{#if estatisticasLGPD}
<div class="card bg-base-100 mb-8 shadow-xl">
<div class="card-body">
<div class="mb-4 flex items-center justify-between">
<h2 class="card-title text-2xl">LGPD - Proteção de Dados</h2>
<a href={resolve('/ti/lgpd')} class="btn btn-sm btn-primary">
<Shield class="h-4 w-4" />
Acessar LGPD
</a>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-4">
<StatsCard
title="Solicitações Pendentes"
value={estatisticasLGPD.solicitacoesPendentes}
description="Aguardando resposta"
Icon={AlertTriangle}
color="warning"
/>
<StatsCard
title="Solicitações Vencendo"
value={estatisticasLGPD.solicitacoesVencendo}
description="Prazo próximo"
Icon={AlertTriangle}
color="error"
/>
<StatsCard
title="Solicitações Vencendo"
value={estatisticasLGPD.solicitacoesVencendo}
description="Prazo próximo"
Icon={AlertTriangle}
color="error"
/>
<StatsCard
title="Total de Solicitações"
value={estatisticasLGPD.totalSolicitacoes}
description="Todas as solicitações"
Icon={FileText}
color="info"
/>
<StatsCard
title="Total de Solicitações"
value={estatisticasLGPD.totalSolicitacoes}
description="Todas as solicitações"
Icon={FileText}
color="info"
/>
<StatsCard
title="Consentimentos Ativos"
value={estatisticasLGPD.consentimentosAtivos}
description="Consentimentos válidos"
Icon={CheckCircle2}
color="success"
/>
</div>
</div>
</div>
{/if}
<StatsCard
title="Consentimentos Ativos"
value={estatisticasLGPD.consentimentosAtivos}
description="Consentimentos válidos"
Icon={CheckCircle2}
color="success"
/>
</div>
</div>
</div>
{/if}
<!-- Ações Rápidas -->
<div class="card bg-base-100 shadow-xl mb-8">
<div class="card-body">
<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={resolve("/ti/usuarios")} class="btn btn-primary">
<Plus class="h-5 w-5" strokeWidth={2} />
Criar Usuário
</a>
<!-- Ações Rápidas -->
<div class="card bg-base-100 mb-8 shadow-xl">
<div class="card-body">
<h2 class="card-title mb-4 text-2xl">Ações Rápidas</h2>
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<a href={resolve('/ti/usuarios')} class="btn btn-primary">
<Plus class="h-5 w-5" strokeWidth={2} />
Criar Usuário
</a>
<a href={resolve("/ti/perfis")} class="btn btn-secondary">
<Layers class="h-5 w-5" strokeWidth={2} />
Gerenciar Perfis
</a>
<a href={resolve('/ti/perfis')} class="btn btn-secondary">
<Layers class="h-5 w-5" strokeWidth={2} />
Gerenciar Perfis
</a>
<a href={resolve("/ti/auditoria")} class="btn btn-accent">
<FileText class="h-5 w-5" strokeWidth={2} />
Ver Logs
</a>
<a href={resolve('/ti/auditoria')} class="btn btn-accent">
<FileText class="h-5 w-5" strokeWidth={2} />
Ver Logs
</a>
<a href={resolve("/ti/lgpd")} class="btn btn-info">
<Shield class="h-5 w-5" strokeWidth={2} />
LGPD
</a>
</div>
</div>
</div>
<a href={resolve('/ti/lgpd')} class="btn btn-info">
<Shield class="h-5 w-5" strokeWidth={2} />
LGPD
</a>
</div>
</div>
</div>
<!-- Informação Sistema -->
<div class="alert alert-info">
<Info class="stroke-current shrink-0 w-6 h-6" strokeWidth={2} />
<span>SGSE - Sistema de Gerenciamento de Secretaria - Versão 2.0 com controle avançado de acesso</span>
</div>
<!-- Informação Sistema -->
<div class="alert alert-info">
<Info class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
<span
>SGSE - Sistema de Gerenciamento de Secretaria - Versão 2.0 com controle avançado de acesso</span
>
</div>
</div>

View File

@@ -202,8 +202,8 @@
<div>
<h3 class="font-bold">Perfis com níveis legados</h3>
<p class="text-sm">
Existem {stats.niveisLegado} perfis com nível acima de 1. Esses perfis continuarão
sendo tratados como nível 1 (administrativo) após a migração.
Existem {stats.niveisLegado} perfis com nível acima de 1. Esses perfis continuarão sendo
tratados como nível 1 (administrativo) após a migração.
</p>
</div>
</div>

View File

@@ -95,7 +95,8 @@
<span>
A personalização por usuário foi substituída por <strong>permissões por ação</strong>
por perfil. Utilize o
<a href={resolve('/ti/painel-permissoes')} class="link link-primary">Painel de Permissões</a> para configurar.
<a href={resolve('/ti/painel-permissoes')} class="link link-primary">Painel de Permissões</a> para
configurar.
</span>
</div>
</ProtectedRoute>