feat: Add 'atas' (minutes/records) management feature, and implement various improvements across UI, backend logic, and authentication.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user