feat: enhance point registration and management features

- Added functionality to capture and display images during point registration, improving user experience.
- Implemented error handling for image uploads and webcam access, ensuring smoother operation.
- Introduced a justification field for point registration, allowing users to provide context for their entries.
- Enhanced the backend to support new features, including image handling and justification storage.
- Updated UI components for better layout and responsiveness, improving overall usability.
This commit is contained in:
2025-11-18 15:28:26 -03:00
parent f0c6e4468f
commit b01d2d6786
10 changed files with 941 additions and 187 deletions

View File

@@ -117,7 +117,7 @@
</div>
<!-- Widget Gestão de Pontos -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mt-8">
<div class="mt-8">
<WidgetGestaoPontos />
</div>

View File

@@ -2,7 +2,7 @@
import { onMount } 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 } from 'lucide-svelte';
import { Clock, Filter, Download, Printer, BarChart3, Users, CheckCircle2, XCircle, TrendingUp, TrendingDown } from 'lucide-svelte';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import { formatarHoraPonto, getTipoRegistroLabel } from '$lib/utils/ponto';
import jsPDF from 'jspdf';
@@ -17,18 +17,22 @@
let funcionarioIdFiltro = $state<Id<'funcionarios'> | ''>('');
let carregando = $state(false);
// Queries
const funcionariosQuery = useQuery(api.funcionarios.getAll, {});
const registrosQuery = useQuery(api.pontos.listarRegistrosPeriodo, {
// Parâmetros reativos para queries
const registrosParams = $derived({
funcionarioId: funcionarioIdFiltro || undefined,
dataInicio,
dataFim,
});
const estatisticasQuery = useQuery(api.pontos.obterEstatisticas, {
const estatisticasParams = $derived({
dataInicio,
dataFim,
});
// Queries
const funcionariosQuery = useQuery(api.funcionarios.getAll, {});
const registrosQuery = useQuery(api.pontos.listarRegistrosPeriodo, registrosParams);
const estatisticasQuery = useQuery(api.pontos.obterEstatisticas, estatisticasParams);
const funcionarios = $derived(funcionariosQuery?.data || []);
const registros = $derived(registrosQuery?.data || []);
const estatisticas = $derived(estatisticasQuery?.data);
@@ -39,6 +43,7 @@
string,
{
funcionario: { nome: string; matricula?: string; descricaoCargo?: string } | null;
funcionarioId: Id<'funcionarios'>;
registros: typeof registros;
}
> = {};
@@ -48,6 +53,7 @@
if (!agrupados[key]) {
agrupados[key] = {
funcionario: registro.funcionario,
funcionarioId: registro.funcionarioId,
registros: [],
};
}
@@ -57,6 +63,19 @@
return Object.values(agrupados);
});
// Query para banco de horas de cada funcionário
const funcionariosComBancoHoras = $derived.by(() => {
return registrosAgrupados.map((grupo) => grupo.funcionarioId);
});
// Função para formatar saldo de horas
function formatarSaldoHoras(minutos: number): string {
const horas = Math.floor(Math.abs(minutos) / 60);
const mins = Math.abs(minutos) % 60;
const sinal = minutos >= 0 ? '+' : '-';
return `${sinal}${horas}h ${mins}min`;
}
async function imprimirFichaPonto(funcionarioId: Id<'funcionarios'>) {
const registrosFuncionario = registros.filter((r) => r.funcionarioId === funcionarioId);
if (registrosFuncionario.length === 0) {
@@ -297,7 +316,7 @@
<div class="card bg-base-200">
<div class="card-body">
<div class="flex items-center justify-between mb-4">
<div>
<div class="flex-1">
<h3 class="font-bold text-lg">
{grupo.funcionario?.nome || 'Funcionário não encontrado'}
</h3>
@@ -307,9 +326,39 @@
</p>
{/if}
</div>
<!-- Banco de Horas -->
{#key grupo.funcionarioId}
{@const bancoHorasQuery = useQuery(
api.pontos.obterBancoHorasFuncionario,
{ funcionarioId: grupo.funcionarioId }
)}
{@const bancoHoras = bancoHorasQuery?.data}
{@const saldoAcumulado = bancoHoras?.saldoAcumuladoMinutos ?? 0}
{@const saldoPositivo = saldoAcumulado >= 0}
{#if bancoHoras}
<div class="mx-4 rounded-lg border-2 p-3 {saldoPositivo ? 'border-success bg-success/10' : 'border-error bg-error/10'}">
<div class="flex items-center gap-2">
{#if saldoPositivo}
<TrendingUp class="h-5 w-5 text-success" />
{:else}
<TrendingDown class="h-5 w-5 text-error" />
{/if}
<div>
<p class="text-xs font-semibold opacity-70">Banco de Horas</p>
<p class="text-lg font-bold">
{formatarSaldoHoras(saldoAcumulado)}
</p>
</div>
</div>
</div>
{/if}
{/key}
<button
class="btn btn-sm btn-primary"
onclick={() => imprimirFichaPonto(grupo.registros[0]!.funcionarioId)}
onclick={() => imprimirFichaPonto(grupo.funcionarioId)}
>
<Printer class="h-4 w-4" />
Imprimir Ficha

View File

@@ -115,7 +115,7 @@
</div>
<!-- Widget Gestão de Pontos -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mt-8">
<div class="mt-8">
<WidgetGestaoPontos />
</div>