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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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®ion=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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
|
||||
@@ -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 {{variavel}} 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 {{variavel}} 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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user