feat: Add 'atas' (minutes/records) management feature, and implement various improvements across UI, backend logic, and authentication.

This commit is contained in:
2025-12-02 16:37:48 -03:00
parent 05e7f1181d
commit 4bd9e21748
265 changed files with 29156 additions and 26460 deletions

View File

@@ -1,32 +1,32 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { useQuery, useConvexClient } from 'convex-svelte';
import { api } from '@sgse-app/backend/convex/_generated/api';
import {
Clock,
Filter,
Download,
Printer,
BarChart3,
Users,
CheckCircle2,
XCircle,
TrendingUp,
TrendingDown,
FileText
} from 'lucide-svelte';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import { formatarHoraPonto, getTipoRegistroLabel, formatarDataDDMMAAAA } from '$lib/utils/ponto';
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 { Chart, registerables } from 'chart.js';
import { useConvexClient, useQuery } from 'convex-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 {
BarChart3,
CheckCircle2,
Clock,
Download,
FileText,
Filter,
Printer,
TrendingDown,
TrendingUp,
Users,
XCircle
} from 'lucide-svelte';
import Papa from 'papaparse';
import { onDestroy, onMount } from 'svelte';
import { toast } from 'svelte-sonner';
import logoGovPE from '$lib/assets/logo_governo_PE.png';
import LocalizacaoIcon from '$lib/components/ponto/LocalizacaoIcon.svelte';
import PrintPontoModal from '$lib/components/ponto/PrintPontoModal.svelte';
import SaldoDiarioBadge from '$lib/components/ponto/SaldoDiarioBadge.svelte';
import SaldoDiarioComparativoBadge from '$lib/components/ponto/SaldoDiarioComparativoBadge.svelte';
import { formatarDataDDMMAAAA, formatarHoraPonto, getTipoRegistroLabel } from '$lib/utils/ponto';
Chart.register(...registerables);
@@ -43,7 +43,7 @@
let funcionarioIdFiltro = $state<Id<'funcionarios'> | ''>('');
let statusFiltro = $state<'todos' | 'dentro' | 'fora'>('todos');
let localizacaoFiltro = $state<'todos' | 'dentro' | 'fora'>('todos');
let carregando = $state(false);
const carregando = $state(false);
let mostrarModalImpressao = $state(false);
let funcionarioParaImprimir = $state<Id<'funcionarios'> | ''>('');
let mostrarModalDetalhes = $state(false);
@@ -162,7 +162,7 @@
borderWidth: 1,
padding: 12,
callbacks: {
label: function (context) {
label: (context) => {
const label = context.dataset.label || '';
const value = context.parsed.y;
const total = estatisticas.totalRegistros;
@@ -302,7 +302,11 @@
const agrupados: Record<
string,
{
funcionario: { nome: string; matricula?: string; descricaoCargo?: string } | null;
funcionario: {
nome: string;
matricula?: string;
descricaoCargo?: string;
} | null;
funcionarioId: Id<'funcionarios'>;
registrosPorData: Record<
string,
@@ -539,16 +543,31 @@
registros: Array<{ tipo: string; hora: number; minuto: number; _id?: any }>
): Map<
number,
{ saldoMinutos: number; horas: number; minutos: number; positivo: boolean; parNumero: number }
{
saldoMinutos: number;
horas: number;
minutos: number;
positivo: boolean;
parNumero: number;
}
> {
const saldos = new Map<
number,
{ saldoMinutos: number; horas: number; minutos: number; positivo: boolean; parNumero: number }
{
saldoMinutos: number;
horas: number;
minutos: number;
positivo: boolean;
parNumero: number;
}
>();
if (registros.length === 0) return saldos;
// Criar array com índices originais
const registrosComIndice = registros.map((r, idx) => ({ ...r, originalIndex: idx }));
const registrosComIndice = registros.map((r, idx) => ({
...r,
originalIndex: idx
}));
// Ordenar registros por hora e minuto para processar em ordem cronológica
const registrosOrdenados = [...registrosComIndice].sort((a, b) => {
@@ -603,9 +622,12 @@
return saldos;
}
function calcularSaldoDiario(
registros: Array<{ tipo: string; hora: number; minuto: number }>
): { saldoMinutos: number; horas: number; minutos: number; positivo: boolean } | null {
function calcularSaldoDiario(registros: Array<{ tipo: string; hora: number; minuto: number }>): {
saldoMinutos: number;
horas: number;
minutos: number;
positivo: boolean;
} | null {
if (registros.length === 0) return null;
// Ordenar registros por hora e minuto
@@ -652,11 +674,23 @@
registros: Array<{ tipo: string; hora: number; minuto: number }>
): Map<
number,
{ saldoMinutos: number; horas: number; minutos: number; parIndex: number; tamanhoPar: number }
{
saldoMinutos: number;
horas: number;
minutos: number;
parIndex: number;
tamanhoPar: number;
}
> {
const saldos = new Map<
number,
{ saldoMinutos: number; horas: number; minutos: number; parIndex: number; tamanhoPar: number }
{
saldoMinutos: number;
horas: number;
minutos: number;
parIndex: number;
tamanhoPar: number;
}
>();
if (registros.length === 0) return saldos;
@@ -670,7 +704,12 @@
});
let parIndex = 0;
let entradaAtual: { tipo: string; hora: number; minuto: number; index: number } | null = null;
let entradaAtual: {
tipo: string;
hora: number;
minuto: number;
index: number;
} | null = null;
let indicesPar: number[] = [];
for (let i = 0; i < registrosOrdenados.length; i++) {
@@ -790,7 +829,12 @@
});
let parIndex = 0;
let entradaAtual: { tipo: string; hora: number; minuto: number; index: number } | null = null;
let entradaAtual: {
tipo: string;
hora: number;
minuto: number;
index: number;
} | null = null;
let indicesPar: number[] = [];
for (let i = 0; i < registrosOrdenados.length; i++) {
@@ -913,8 +957,18 @@
return [
{ tipo: 'entrada', hora: horaEntrada, minuto: minutoEntrada, data },
{ tipo: 'saida_almoco', hora: horaSaidaAlmoco, minuto: minutoSaidaAlmoco, data },
{ tipo: 'retorno_almoco', hora: horaRetornoAlmoco, minuto: minutoRetornoAlmoco, data },
{
tipo: 'saida_almoco',
hora: horaSaidaAlmoco,
minuto: minutoSaidaAlmoco,
data
},
{
tipo: 'retorno_almoco',
hora: horaRetornoAlmoco,
minuto: minutoRetornoAlmoco,
data
},
{ tipo: 'saida', hora: horaSaida, minuto: minutoSaida, data }
];
}
@@ -2016,7 +2070,7 @@
theme: 'grid',
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
styles: { fontSize: 9 },
didParseCell: function (data) {
didParseCell: (data) => {
// Aplicar cor vermelha para registros não marcados
if (data.row.raw && (data.row.raw as any)._naoMarcado) {
// Aplicar cor vermelha nas colunas Tipo e Horário
@@ -2309,7 +2363,7 @@
0: { fontStyle: 'bold', cellWidth: 80 },
1: { cellWidth: 'auto' }
},
didParseCell: function (data) {
didParseCell: (data) => {
// Ignorar linhas separadoras vazias
if (data.cell.text[0] === '' && data.column.index === 0) {
data.cell.styles.fillColor = [240, 240, 240]; // Cor de fundo cinza claro
@@ -2481,11 +2535,14 @@
columnStyles: {
0: { cellWidth: 30 }, // Data
1: { cellWidth: 40 }, // Tipo
2: { cellWidth: 50, cellPadding: { top: 2, bottom: 2, left: 2, right: 2 } }, // Detalhes - maior largura
2: {
cellWidth: 50,
cellPadding: { top: 2, bottom: 2, left: 2, right: 2 }
}, // Detalhes - maior largura
3: { cellWidth: 40 }, // Motivo
4: { cellWidth: 'auto' } // Observações
},
didParseCell: function (data) {
didParseCell: (data) => {
// Permitir quebra de linha na coluna Detalhes (índice 2)
if (data.column.index === 2) {
data.cell.styles.overflow = 'linebreak';
@@ -2598,7 +2655,9 @@
async function imprimirDetalhesRegistro(registroId: Id<'registrosPonto'>) {
try {
// Buscar dados completos do registro
const registro = await client.query(api.pontos.obterRegistro, { registroId });
const registro = await client.query(api.pontos.obterRegistro, {
registroId
});
if (!registro) {
alert('Registro não encontrado');
@@ -2632,7 +2691,9 @@
doc.setFontSize(18);
doc.setTextColor(41, 128, 185);
doc.setFont('helvetica', 'bold');
doc.text('DETALHES DO REGISTRO DE PONTO', 105, yPosition, { align: 'center' });
doc.text('DETALHES DO REGISTRO DE PONTO', 105, yPosition, {
align: 'center'
});
// Linha decorativa abaixo do título
doc.setDrawColor(41, 128, 185);
@@ -3068,7 +3129,7 @@
// Análise de Propriedades GPS em tabela
const propriedadesData: any[][] = [];
let propriedadesGPS = 0;
let propriedadesTotais = 5;
const propriedadesTotais = 5;
if (
registro.altitude !== null &&
@@ -3575,7 +3636,9 @@
} catch (error) {
console.warn('Erro ao adicionar imagem ao PDF:', error);
doc.setFontSize(10);
doc.text('Foto não disponível para impressão', 105, yPosition, { align: 'center' });
doc.text('Foto não disponível para impressão', 105, yPosition, {
align: 'center'
});
yPosition += 6;
}
}