feat: implement end-to-end encryption for chat messages and files, including key management and decryption functionality; enhance chat components to support encrypted content display

This commit is contained in:
2025-12-09 01:31:09 -03:00
parent cae6d886de
commit e6f380d7cc
14 changed files with 1443 additions and 203 deletions

View File

@@ -75,3 +75,6 @@

View File

@@ -101,15 +101,22 @@
carregando = true;
try {
if (!authStore.token) {
throw new Error('Token não encontrado');
}
const resultado = await convex.mutation(api.autenticacao.alterarSenha, {
token: authStore.token,
// Construir objeto de argumentos, incluindo token apenas se existir
const args: {
senhaAtual: string;
novaSenha: string;
token?: string;
} = {
senhaAtual: senhaAtual,
novaSenha: novaSenha
});
};
// Adicionar token apenas se existir
if (authStore.token) {
args.token = authStore.token;
}
const resultado = await convex.mutation(api.autenticacao.alterarSenha, args);
if (resultado.sucesso) {
notice = {

View File

@@ -19,49 +19,27 @@
} from 'lucide-svelte';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import { formatarHoraPonto, getTipoRegistroLabel, formatarDataDDMMAAAA } from '$lib/utils/ponto';
import {
adicionarLogo,
adicionarCabecalho,
adicionarDadosFuncionario,
adicionarResumoPeriodo,
adicionarSaldosPeriodo,
adicionarLegenda,
adicionarRodape,
verificarNovaPagina,
type SectionsPDF,
type FuncionarioPDF,
type ConfigPontoPDF
} from '$lib/utils/fichaPontoPDF';
import { type SectionsPDF } from '$lib/utils/fichaPontoPDF';
// Importar módulos extraídos
import type { TipoDia, SaldoDiario, RegistroPonto, DiaFichaPonto, ResumoPeriodo } from '$lib/utils/ponto/tipos';
import { formatarDataParaExibicao, formatarDataParaBackend, formatarSaldoHoras, formatarSaldoDiario, formatarMinutos, formatarHoras } from '$lib/utils/ponto/formatacao';
import { validarPeriodo } from '$lib/utils/ponto/validacao';
import { calcularSaldosParciais, calcularSaldoDiario, calcularSaldosPorPar, calcularSaldoComparativoPorPar } from '$lib/utils/ponto/calculos';
import { agruparRegistrosPorFuncionario, gerarDiasPeriodo, gerarRegistrosEsperados, processarDadosFichaPonto } from '$lib/utils/ponto/processamento';
import { registroFoiMarcado } from '$lib/utils/ponto/validacao';
import {
formatarDataParaExibicao,
formatarDataParaBackend,
formatarSaldoHoras
} from '$lib/utils/ponto/formatacao';
import { calcularSaldosParciais } from '$lib/utils/ponto/calculos';
import { agruparRegistrosPorFuncionario } from '$lib/utils/ponto/processamento';
import { gerarPDFComSelecao } from '$lib/utils/ponto/pdf/geradorPDF';
import { imprimirDetalhesRegistro } from '$lib/utils/ponto/pdf/geradorDetalhesPDF';
import HeaderRegistroPontos from '$lib/components/ponto/registro-pontos/HeaderRegistroPontos.svelte';
import EstatisticasCards from '$lib/components/ponto/registro-pontos/EstatisticasCards.svelte';
import GraficoEstatisticas from '$lib/components/ponto/registro-pontos/GraficoEstatisticas.svelte';
import { maskDate, validateDate, onlyDigits } from '$lib/utils/masks';
import LocalizacaoIcon from '$lib/components/ponto/LocalizacaoIcon.svelte';
import SaldoDiarioBadge from '$lib/components/ponto/SaldoDiarioBadge.svelte';
import SaldoDiarioComparativoBadge from '$lib/components/ponto/SaldoDiarioComparativoBadge.svelte';
import jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable';
import logoGovPE from '$lib/assets/logo_governo_PE.png';
import PrintPontoModal from '$lib/components/ponto/PrintPontoModal.svelte';
import { toast } from 'svelte-sonner';
import { Chart, registerables } from 'chart.js';
import Papa from 'papaparse';
const client = useConvexClient();
// ============================================
@@ -72,8 +50,7 @@
// Estados
// Expandir período padrão para últimos 30 dias para facilitar visualização
const hoje = new Date();
const trintaDiasAtras = new Date(hoje);
trintaDiasAtras.setDate(hoje.getDate() - 30);
const trintaDiasAtras = new Date(hoje.getTime() - 30 * 24 * 60 * 60 * 1000);
// Funções de formatação importadas de $lib/utils/ponto/formatacao
// Wrapper para formatarDataParaBackend que precisa de onlyDigits e validateDate
@@ -85,8 +62,8 @@
let dataFimInterno = $state(hoje.toISOString().split('T')[0]!);
// Valores para exibição (dd/mm/yyyy)
let dataInicioExibicao = $state(formatarDataParaExibicao(dataInicioInterno));
let dataFimExibicao = $state(formatarDataParaExibicao(dataFimInterno));
let dataInicioExibicao = $derived(formatarDataParaExibicao(dataInicioInterno));
let dataFimExibicao = $derived(formatarDataParaExibicao(dataFimInterno));
// Valores para backend (yyyy-mm-dd) - derivados dos valores internos
const dataInicio = $derived(dataInicioInterno);
@@ -94,6 +71,7 @@
let funcionarioIdFiltro = $state<Id<'funcionarios'> | ''>('');
let statusFiltro = $state<'todos' | 'dentro' | 'fora'>('todos');
let localizacaoFiltro = $state<'todos' | 'dentro' | 'fora'>('todos');
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let carregando = $state(false);
let mostrarModalImpressao = $state(false);
let funcionarioParaImprimir = $state<Id<'funcionarios'> | ''>('');
@@ -105,7 +83,7 @@
funcionarioParaImprimir = funcionarioId;
mostrarModalImpressao = true;
};
let chartCanvas: HTMLCanvasElement;
let chartCanvas = $state<HTMLCanvasElement | undefined>(undefined);
let chartInstance: Chart | null = null;
// Parâmetros reativos para queries
@@ -125,7 +103,10 @@
// Queries
const funcionariosQuery = useQuery(api.funcionarios.getAll, {});
// useQuery do Convex-Svelte lida corretamente com valores $derived reativos
// svelte-ignore state_referenced_locally
const registrosQuery = useQuery(api.pontos.listarRegistrosPeriodo, registrosParams);
// svelte-ignore state_referenced_locally
const estatisticasQuery = useQuery(api.pontos.obterEstatisticas, estatisticasParams);
const configQuery = useQuery(api.configuracaoPonto.obterConfiguracao, {});
@@ -143,11 +124,12 @@
// Debug: Log dos dados recebidos
$effect(() => {
if (registrosQuery !== undefined) {
const params = registrosParams;
console.log('[Frontend] registrosQuery:', {
isLoading: registrosQuery?.isLoading,
error: registrosQuery?.error,
dataLength: registrosQuery?.data?.length ?? 0,
params: registrosParams
params
});
}
if (registros && registros.length > 0) {
@@ -186,7 +168,7 @@
return;
}
const ctx = chartCanvas.getContext('2d');
const ctx = chartCanvas?.getContext('2d');
if (!ctx) {
return;
}
@@ -229,6 +211,9 @@
label: function (context) {
const label = context.dataset.label || '';
const value = context.parsed.y;
if (value === null || value === undefined) {
return `${label}: 0 (0.0%)`;
}
const total = estatisticas.totalRegistros;
const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : '0.0';
return `${label}: ${value} (${percentage}%)`;
@@ -376,7 +361,7 @@
return resultado;
});
// Agrupar registros por funcionário e data usando função extraída
// Agrupar registros por funcionário e data usando função extraída
const registrosAgrupados = $derived.by(() => {
return agruparRegistrosPorFuncionario(registrosFiltrados, config || undefined);
});
@@ -606,43 +591,6 @@
// Funções formatarMinutos e formatarHoras importadas de $lib/utils/ponto/formatacao
/**
* Obter nome do dia da semana em português
*/
function obterDiaSemana(data: string): string {
const dias = ['Domingo', 'Segunda', 'Terça', 'Quarta', 'Quinta', 'Sexta', 'Sábado'];
const dataObj = new Date(data + 'T00:00:00');
return dias[dataObj.getDay()] || '';
}
/**
* Obter cor de fundo baseado no tipo de dia
*/
function obterCorFundo(tipoDia: TipoDia, temInconsistencia: boolean): string {
if (temInconsistencia) return '#FFF3E0'; // Laranja claro
if (tipoDia === 'atestado') return '#E3F2FD'; // Azul claro
if (tipoDia === 'ausencia') return '#FFF9C4'; // Amarelo claro
if (tipoDia === 'abonado') return '#E8F5E9'; // Verde claro
if (tipoDia === 'nao_computado' || tipoDia === 'ferias') return '#F5F5F5'; // Cinza claro
return '#FFFFFF'; // Branco
}
/**
* Obter ícone baseado no tipo de dia
*/
function obterIconeTipo(tipoDia: TipoDia): string {
const icones: Record<TipoDia, string> = {
normal: '',
atestado: '🏥',
ausencia: '🚫',
licenca: '📋',
abonado: '✅',
nao_computado: '⏸',
ferias: '🏖',
inconsistente: '⚠'
};
return icones[tipoDia] || '';
}
// Função validarPeriodo importada de $lib/utils/ponto/validacao
@@ -1085,8 +1033,7 @@
// Função para limpar todos os filtros
function limparFiltros() {
const hoje = new Date();
const trintaDiasAtras = new Date(hoje);
trintaDiasAtras.setDate(hoje.getDate() - 30);
const trintaDiasAtras = new Date(hoje.getTime() - 30 * 24 * 60 * 60 * 1000);
const dataInicioStr = trintaDiasAtras.toISOString().split('T')[0]!;
const dataFimStr = hoje.toISOString().split('T')[0]!;
@@ -1149,8 +1096,7 @@
// Gerar CSV usando Papa Parse
const csv = Papa.unparse(csvData, {
header: true,
delimiter: ';',
encoding: 'UTF-8'
delimiter: ';'
});
// Adicionar BOM para Excel reconhecer UTF-8 corretamente
@@ -1184,7 +1130,7 @@
if (!funcionarioParaImprimir) return;
const funcionarioId = funcionarioParaImprimir;
await gerarPDFComSelecao(
client,
sections,
@@ -1199,7 +1145,9 @@
funcionarioParaImprimir = '';
toast.success('PDF gerado com sucesso!');
},
(value: boolean) => { carregando = value; }
(value: boolean) => {
carregando = value;
}
);
}
@@ -3070,11 +3018,8 @@
// Wrapper para a função importada imprimirDetalhesRegistro
async function imprimirDetalhesRegistroWrapper(registroId: Id<'registrosPonto'>) {
await imprimirDetalhesRegistro(
client,
registroId,
logoGovPE,
(message: string) => toast.error(message)
await imprimirDetalhesRegistro(client, registroId, logoGovPE, (message: string) =>
toast.error(message)
);
}
@@ -4491,7 +4436,7 @@
class="select select-bordered select-sm focus:select-primary"
>
<option value="">Todos os funcionários</option>
{#each funcionarios as funcionario}
{#each funcionarios as funcionario (funcionario._id)}
<option value={funcionario._id}>{funcionario.nome}</option>
{/each}
</select>
@@ -4628,7 +4573,7 @@
</div>
{:else}
<div class="space-y-4">
{#each registrosAgrupados as grupo}
{#each registrosAgrupados as grupo (grupo.funcionarioId)}
<div
class="card bg-base-100 border-base-300 border shadow-md transition-all duration-200 hover:shadow-lg"
>
@@ -4702,16 +4647,16 @@
{/if}
{/key}
<button
class="btn btn-primary btn-sm gap-2"
onclick={(e) => {
e.preventDefault();
abrirModalImpressao(grupo.funcionarioId);
}}
>
<Printer class="h-4 w-4" />
Imprimir Ficha
</button>
<button
class="btn btn-primary btn-sm gap-2"
onclick={(e) => {
e.preventDefault();
abrirModalImpressao(grupo.funcionarioId);
}}
>
<Printer class="h-4 w-4" />
Imprimir Ficha
</button>
</div>
</div>
@@ -4758,13 +4703,20 @@
</tr>
</thead>
<tbody>
{#each Object.values(grupo.registrosPorData) as grupoData, dataIndex}
{#each Object.values(grupo.registrosPorData) as grupoData, dataIndex (grupoData.data)}
{@const totalRegistros = grupoData.registros.length}
{@const dataFormatada = formatarDataDDMMAAAA(grupoData.data)}
{@const saldosParciais = calcularSaldosParciais(grupoData.registros)}
{@const saldosParciais = calcularSaldosParciais(
grupoData.registros.map((r) => ({
tipo: r.tipo,
hora: r.hora,
minuto: r.minuto,
_id: r._id
}))
)}
{@const isUltimoDia =
dataIndex === Object.values(grupo.registrosPorData).length - 1}
{#each grupoData.registros as registro, index}
{#each grupoData.registros as registro, index (registro._id)}
{@const saldoParcial = saldosParciais.get(index)}
<tr
class="hover:bg-base-200/50 transition-colors {dataIndex % 2 === 0
@@ -4874,14 +4826,16 @@
class="modal modal-open"
onclick={(e) => e.target === e.currentTarget && fecharModalDetalhes()}
>
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div
class="modal-box flex max-h-[90vh] max-w-4xl flex-col overflow-hidden"
onclick={(e) => e.stopPropagation()}
role="document"
>
<div
class="border-base-300 mb-4 flex flex-shrink-0 items-center justify-between border-b pb-4"
>
<h3 class="text-xl font-bold">Detalhes do Registro de Ponto</h3>
<h3 id="modal-title" class="text-xl font-bold">Detalhes do Registro de Ponto</h3>
<button class="btn btn-sm btn-circle btn-ghost" onclick={fecharModalDetalhes}>
<X class="h-4 w-4" />
</button>
@@ -5034,10 +4988,14 @@
</div>
<div class="border-base-300 mt-4 flex flex-shrink-0 justify-end gap-2 border-t pt-4">
{#if registroDetalhes}
{#if registroDetalhes && registroDetalhesId}
<button
class="btn btn-primary gap-2"
onclick={() => imprimirDetalhesRegistroWrapper(registroDetalhesId)}
onclick={() => {
if (registroDetalhesId) {
imprimirDetalhesRegistroWrapper(registroDetalhesId);
}
}}
>
<Printer class="h-4 w-4" />
Imprimir PDF
@@ -5046,6 +5004,17 @@
<button class="btn btn-outline" onclick={fecharModalDetalhes}>Fechar</button>
</div>
</div>
<form method="dialog" class="modal-backdrop" onclick={fecharModalDetalhes}></form>
<button
type="button"
class="modal-backdrop"
onclick={fecharModalDetalhes}
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
fecharModalDetalhes();
}
}}
aria-label="Fechar modal"
></button>
</dialog>
{/if}

View File

@@ -75,3 +75,6 @@