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:
@@ -75,3 +75,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user