refactor: update ErrorModal and RegistroPonto components for improved UI and functionality
- Refactored ErrorModal to use a div-based layout with enhanced animations and accessibility features. - Updated RegistroPonto to include a new loading state and improved modal handling for webcam capture. - Enhanced styling for better visual consistency and user experience across modals and registration cards. - Introduced comparative balance calculations in RegistroPonto for better visibility of time discrepancies.
This commit is contained in:
@@ -23,38 +23,41 @@
|
|||||||
details.match(/^\d+\./); // Começa com número (lista numerada)
|
details.match(/^\d+\./); // Começa com número (lista numerada)
|
||||||
});
|
});
|
||||||
|
|
||||||
let modalRef: HTMLDialogElement;
|
|
||||||
|
|
||||||
function handleClose() {
|
function handleClose() {
|
||||||
open = false;
|
open = false;
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (open && modalRef) {
|
|
||||||
modalRef.showModal();
|
|
||||||
} else if (!open && modalRef) {
|
|
||||||
modalRef.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if open}
|
{#if open}
|
||||||
<dialog
|
<div
|
||||||
bind:this={modalRef}
|
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||||
class="modal"
|
style="animation: fadeIn 0.2s ease-out;"
|
||||||
onclick={(e) => e.target === e.currentTarget && handleClose()}
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="modal-error-title"
|
||||||
>
|
>
|
||||||
<div class="modal-box max-w-2xl" onclick={(e) => e.stopPropagation()}>
|
<!-- Backdrop -->
|
||||||
<!-- Header -->
|
<div
|
||||||
<div class="border-base-300 flex items-center justify-between border-b px-6 py-4">
|
class="absolute inset-0 bg-black/50 backdrop-blur-sm transition-opacity duration-200"
|
||||||
<h2 id="modal-title" class="text-error flex items-center gap-2 text-xl font-bold">
|
onclick={handleClose}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- Modal Box -->
|
||||||
|
<div
|
||||||
|
class="relative bg-base-100 rounded-2xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-hidden flex flex-col z-10 transform transition-all duration-300"
|
||||||
|
style="animation: slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);"
|
||||||
|
onclick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<!-- Header fixo -->
|
||||||
|
<div class="flex items-center justify-between px-6 py-4 border-b border-base-300 flex-shrink-0">
|
||||||
|
<h2 id="modal-error-title" class="text-error flex items-center gap-2 text-xl font-bold">
|
||||||
<AlertCircle class="h-6 w-6" strokeWidth={2.5} />
|
<AlertCircle class="h-6 w-6" strokeWidth={2.5} />
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-circle btn-ghost"
|
class="btn btn-sm btn-circle btn-ghost hover:bg-base-300"
|
||||||
onclick={handleClose}
|
onclick={handleClose}
|
||||||
aria-label="Fechar"
|
aria-label="Fechar"
|
||||||
>
|
>
|
||||||
@@ -62,8 +65,8 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content com rolagem -->
|
||||||
<div class="px-6 py-6">
|
<div class="flex-1 overflow-y-auto px-6 py-6 modal-scroll">
|
||||||
<!-- Mensagem principal -->
|
<!-- Mensagem principal -->
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<p class="text-base-content text-base leading-relaxed font-medium">{message}</p>
|
<p class="text-base-content text-base leading-relaxed font-medium">{message}</p>
|
||||||
@@ -96,14 +99,59 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer fixo -->
|
||||||
<div class="modal-action border-base-300 border-t px-6 pb-6 pt-4">
|
<div class="flex justify-end px-6 py-4 border-t border-base-300 flex-shrink-0">
|
||||||
<button class="btn btn-primary btn-md" onclick={handleClose}> Entendi, obrigado </button>
|
<button class="btn btn-primary" onclick={handleClose}>Entendi, obrigado</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="dialog" class="modal-backdrop">
|
|
||||||
<button type="button" onclick={handleClose}>fechar</button>
|
|
||||||
</form>
|
|
||||||
</dialog>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px) scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar customizada para os modais */
|
||||||
|
:global(.modal-scroll) {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: hsl(var(--bc) / 0.3) transparent;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.modal-scroll::-webkit-scrollbar) {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.modal-scroll::-webkit-scrollbar-track) {
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.modal-scroll::-webkit-scrollbar-thumb) {
|
||||||
|
background-color: hsl(var(--bc) / 0.3);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.modal-scroll::-webkit-scrollbar-thumb:hover) {
|
||||||
|
background-color: hsl(var(--bc) / 0.5);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
getTipoRegistroLabel,
|
getTipoRegistroLabel,
|
||||||
getProximoTipoRegistro
|
getProximoTipoRegistro
|
||||||
} from '$lib/utils/ponto';
|
} from '$lib/utils/ponto';
|
||||||
import { LogIn, LogOut, Clock, CheckCircle2, XCircle, TrendingUp, TrendingDown, Printer } from 'lucide-svelte';
|
import { LogIn, LogOut, Clock, CheckCircle2, XCircle, TrendingUp, TrendingDown, Printer, Camera } from 'lucide-svelte';
|
||||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
import jsPDF from 'jspdf';
|
import jsPDF from 'jspdf';
|
||||||
import logoGovPE from '$lib/assets/logo_governo_PE.png';
|
import logoGovPE from '$lib/assets/logo_governo_PE.png';
|
||||||
@@ -64,6 +64,7 @@
|
|||||||
let justificativa = $state('');
|
let justificativa = $state('');
|
||||||
let mostrandoModalConfirmacao = $state(false);
|
let mostrandoModalConfirmacao = $state(false);
|
||||||
let dataHoraAtual = $state<{ data: string; hora: string } | null>(null);
|
let dataHoraAtual = $state<{ data: string; hora: string } | null>(null);
|
||||||
|
let aguardandoProcessamento = $state(false);
|
||||||
|
|
||||||
const registrosHoje = $derived(registrosHojeQuery?.data || []);
|
const registrosHoje = $derived(registrosHojeQuery?.data || []);
|
||||||
const config = $derived(configQuery?.data);
|
const config = $derived(configQuery?.data);
|
||||||
@@ -312,6 +313,7 @@
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erro ao registrar ponto:', error);
|
console.error('Erro ao registrar ponto:', error);
|
||||||
|
aguardandoProcessamento = false;
|
||||||
let mensagemErro = 'Erro desconhecido ao registrar ponto';
|
let mensagemErro = 'Erro desconhecido ao registrar ponto';
|
||||||
let detalhesErro = 'Tente novamente em alguns instantes.';
|
let detalhesErro = 'Tente novamente em alguns instantes.';
|
||||||
|
|
||||||
@@ -389,6 +391,7 @@
|
|||||||
} finally {
|
} finally {
|
||||||
registrando = false;
|
registrando = false;
|
||||||
coletandoInfo = false;
|
coletandoInfo = false;
|
||||||
|
aguardandoProcessamento = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -514,6 +517,7 @@
|
|||||||
|
|
||||||
function confirmarRegistro() {
|
function confirmarRegistro() {
|
||||||
mostrandoModalConfirmacao = false;
|
mostrandoModalConfirmacao = false;
|
||||||
|
aguardandoProcessamento = true;
|
||||||
registrarPonto();
|
registrarPonto();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -753,25 +757,7 @@
|
|||||||
return !registrando && !coletandoInfo && config !== undefined && !estaDispensado && temFuncionarioAssociado;
|
return !registrando && !coletandoInfo && config !== undefined && !estaDispensado && temFuncionarioAssociado;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Referência para o modal
|
// Os modais agora são centralizados automaticamente via CSS (fixed inset-0 flex items-center justify-center)
|
||||||
let modalRef: HTMLDivElement | null = $state(null);
|
|
||||||
|
|
||||||
// Efeito para garantir que o modal fique visível quando abrir
|
|
||||||
$effect(() => {
|
|
||||||
if (mostrandoWebcam && modalRef) {
|
|
||||||
// Aguardar um frame para garantir que o DOM foi atualizado
|
|
||||||
setTimeout(() => {
|
|
||||||
if (modalRef) {
|
|
||||||
modalRef.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
||||||
// Também garantir que o modal-box esteja visível
|
|
||||||
const modalBox = modalRef.querySelector('.modal-box');
|
|
||||||
if (modalBox) {
|
|
||||||
(modalBox as HTMLElement).scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Solicitar permissões automaticamente ao montar o componente
|
// Solicitar permissões automaticamente ao montar o componente
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
@@ -922,42 +908,27 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Botões de Registro -->
|
<!-- Card de Registro de Ponto Modernizado -->
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<div class="card bg-gradient-to-br from-base-100 via-base-100 to-primary/5 border border-base-300 shadow-2xl max-w-2xl mx-auto">
|
||||||
<div class="card-body items-center">
|
<div class="card-body p-6">
|
||||||
<h2 class="card-title mb-4">Registrar Ponto</h2>
|
<!-- Cabeçalho -->
|
||||||
<div class="mb-6 w-full">
|
<div class="flex items-center justify-center gap-3 mb-6">
|
||||||
|
<div class="p-2.5 bg-primary/10 rounded-xl">
|
||||||
|
<Clock class="h-6 w-6 text-primary" strokeWidth={2.5} />
|
||||||
|
</div>
|
||||||
|
<h2 class="card-title text-2xl font-black text-base-content">Registrar Ponto</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Relógio Sincronizado -->
|
||||||
|
<div class="mb-5 flex justify-center">
|
||||||
|
<div class="card bg-gradient-to-br from-primary/10 to-primary/5 border-2 border-primary/20 shadow-lg rounded-2xl p-5 w-full max-w-sm">
|
||||||
<RelogioSincronizado />
|
<RelogioSincronizado />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-full flex-col items-center gap-4">
|
|
||||||
{#if sucesso}
|
|
||||||
<div class="alert alert-success w-full">
|
|
||||||
<CheckCircle2 class="h-5 w-5" />
|
|
||||||
<span>{sucesso}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="mb-4 text-center">
|
|
||||||
<p class="text-lg font-semibold">Próximo registro: {tipoLabel}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Campo de Justificativa (Opcional) -->
|
|
||||||
<div class="w-full">
|
|
||||||
<label for="justificativa" class="label">
|
|
||||||
<span class="label-text">Justificativa (Opcional)</span>
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
id="justificativa"
|
|
||||||
class="textarea textarea-bordered w-full"
|
|
||||||
placeholder="Digite uma justificativa para este registro de ponto (opcional)"
|
|
||||||
bind:value={justificativa}
|
|
||||||
disabled={registrando}
|
|
||||||
rows="3"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Botão de Registro -->
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary btn-lg"
|
class="btn btn-primary w-full shadow-lg hover:shadow-xl transition-all duration-300 font-semibold rounded-xl gap-2 mb-5"
|
||||||
onclick={iniciarRegistroComFoto}
|
onclick={iniciarRegistroComFoto}
|
||||||
disabled={!podeRegistrar}
|
disabled={!podeRegistrar}
|
||||||
title={!temFuncionarioAssociado
|
title={!temFuncionarioAssociado
|
||||||
@@ -987,6 +958,45 @@
|
|||||||
Registrar Saída
|
Registrar Saída
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- Campo de Justificativa -->
|
||||||
|
<div class="mb-5">
|
||||||
|
<label for="justificativa" class="label pb-1.5">
|
||||||
|
<span class="label-text font-semibold text-xs">Justificativa <span class="text-base-content/50 font-normal">(Opcional)</span></span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="justificativa"
|
||||||
|
class="textarea textarea-bordered w-full focus:textarea-primary focus:ring-2 focus:ring-primary/20 rounded-xl resize-none text-sm"
|
||||||
|
placeholder="Digite uma justificativa para este registro de ponto (opcional)"
|
||||||
|
bind:value={justificativa}
|
||||||
|
disabled={registrando}
|
||||||
|
rows="2"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mensagem de Sucesso -->
|
||||||
|
{#if sucesso}
|
||||||
|
<div class="alert alert-success shadow-lg mb-5 rounded-xl">
|
||||||
|
<CheckCircle2 class="h-4 w-4" />
|
||||||
|
<span class="font-semibold text-sm">{sucesso}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Próximo Registro -->
|
||||||
|
<div class="card bg-gradient-to-br from-info/10 to-info/5 border-2 border-info/20 shadow-md rounded-xl p-4">
|
||||||
|
<div class="flex items-center justify-center gap-2">
|
||||||
|
<div class="p-1.5 bg-info/20 rounded-lg">
|
||||||
|
{#if proximoTipo === 'entrada' || proximoTipo === 'retorno_almoco'}
|
||||||
|
<LogIn class="h-4 w-4 text-info" strokeWidth={2.5} />
|
||||||
|
{:else}
|
||||||
|
<LogOut class="h-4 w-4 text-info" strokeWidth={2.5} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="text-xs font-semibold text-base-content/60 uppercase tracking-wide mb-0.5">Próximo Registro</p>
|
||||||
|
<p class="text-base font-bold text-base-content">{tipoLabel}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1320,15 +1330,43 @@
|
|||||||
<!-- Modal Webcam -->
|
<!-- Modal Webcam -->
|
||||||
{#if mostrandoWebcam}
|
{#if mostrandoWebcam}
|
||||||
<div
|
<div
|
||||||
bind:this={modalRef}
|
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||||
class="modal modal-open"
|
style="animation: fadeIn 0.2s ease-out;"
|
||||||
style="display: flex; align-items: center; justify-content: center; position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 9999;"
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="modal-webcam-title"
|
||||||
>
|
>
|
||||||
<div class="modal-box max-w-2xl w-[95%] max-h-[90vh] overflow-y-auto relative" style="margin: auto; position: relative;">
|
<!-- Backdrop -->
|
||||||
<div class="sticky top-0 bg-base-100 z-10 pb-3 mb-4 border-b border-base-300 -mx-6 px-6">
|
<div
|
||||||
<h3 class="text-lg font-bold">Capturar Foto</h3>
|
class="absolute inset-0 bg-black/50 backdrop-blur-sm transition-opacity duration-200"
|
||||||
|
onclick={handleWebcamCancel}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- Modal Box -->
|
||||||
|
<div
|
||||||
|
class="relative bg-base-100 rounded-2xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-hidden flex flex-col z-10 transform transition-all duration-300"
|
||||||
|
style="animation: slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);"
|
||||||
|
onclick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<!-- Header fixo -->
|
||||||
|
<div class="flex items-center justify-between px-6 py-4 border-b border-base-300 flex-shrink-0">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="p-2 bg-primary/10 rounded-lg">
|
||||||
|
<Camera class="h-5 w-5 text-primary" strokeWidth={2} />
|
||||||
</div>
|
</div>
|
||||||
<div class="min-h-[200px] flex items-center justify-center py-4">
|
<h3 id="modal-webcam-title" class="text-xl font-bold text-base-content">Capturar Foto</h3>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-circle btn-ghost hover:bg-base-300"
|
||||||
|
onclick={handleWebcamCancel}
|
||||||
|
>
|
||||||
|
<XCircle class="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Conteúdo com rolagem -->
|
||||||
|
<div class="flex-1 overflow-y-auto px-6 py-4 modal-scroll">
|
||||||
|
<div class="min-h-[200px] flex items-center justify-center">
|
||||||
<WebcamCapture
|
<WebcamCapture
|
||||||
onCapture={handleWebcamCapture}
|
onCapture={handleWebcamCapture}
|
||||||
onCancel={handleWebcamCancel}
|
onCancel={handleWebcamCancel}
|
||||||
@@ -1338,31 +1376,65 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form
|
</div>
|
||||||
method="dialog"
|
</div>
|
||||||
class="modal-backdrop"
|
{/if}
|
||||||
onsubmit={(e) => {
|
|
||||||
e.preventDefault();
|
<!-- Modal de Aguardando Processamento -->
|
||||||
handleWebcamCancel();
|
{#if aguardandoProcessamento}
|
||||||
}}
|
<div
|
||||||
|
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||||
|
style="animation: fadeIn 0.2s ease-out;"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="modal-aguardando-title"
|
||||||
>
|
>
|
||||||
<button type="submit" aria-label="Fechar modal">fechar</button>
|
<!-- Backdrop -->
|
||||||
</form>
|
<div class="absolute inset-0 bg-black/50 backdrop-blur-sm transition-opacity duration-200"></div>
|
||||||
|
|
||||||
|
<!-- Modal Box -->
|
||||||
|
<div
|
||||||
|
class="relative bg-base-100 rounded-2xl shadow-2xl max-w-md w-full z-10 transform transition-all duration-300 p-8"
|
||||||
|
style="animation: slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col items-center gap-4 text-center">
|
||||||
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||||
|
<h3 id="modal-aguardando-title" class="text-xl font-bold text-base-content">Processando Registro</h3>
|
||||||
|
<p class="text-base-content/70">Por favor, aguarde enquanto processamos seu registro de ponto...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Modal de Confirmação -->
|
<!-- Modal de Confirmação -->
|
||||||
{#if mostrandoModalConfirmacao && imagemCapturada && dataHoraAtual}
|
{#if mostrandoModalConfirmacao && imagemCapturada && dataHoraAtual}
|
||||||
<div class="modal modal-open" style="display: flex; align-items: center; justify-content: center;">
|
<div
|
||||||
<div class="modal-box max-w-3xl w-[95%] max-h-[85vh] overflow-hidden flex flex-col" style="margin: auto; max-height: 85vh;">
|
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||||
|
style="animation: fadeIn 0.2s ease-out;"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="modal-confirmacao-title"
|
||||||
|
>
|
||||||
|
<!-- Backdrop -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-black/50 backdrop-blur-sm transition-opacity duration-200"
|
||||||
|
onclick={cancelarRegistro}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- Modal Box -->
|
||||||
|
<div
|
||||||
|
class="relative bg-base-100 rounded-2xl shadow-2xl max-w-3xl w-full max-h-[90vh] overflow-hidden flex flex-col z-10 transform transition-all duration-300"
|
||||||
|
style="animation: slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);"
|
||||||
|
onclick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
<!-- Header fixo -->
|
<!-- Header fixo -->
|
||||||
<div class="flex items-center justify-between mb-6 pb-4 border-b border-base-300 flex-shrink-0">
|
<div class="flex items-center justify-between px-6 py-4 border-b border-base-300 flex-shrink-0">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="p-2 bg-primary/10 rounded-lg">
|
<div class="p-2 bg-primary/10 rounded-lg">
|
||||||
<Clock class="h-6 w-6 text-primary" strokeWidth={2} />
|
<Clock class="h-6 w-6 text-primary" strokeWidth={2} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-xl text-base-content">Confirmar Registro de Ponto</h3>
|
<h3 id="modal-confirmacao-title" class="font-bold text-xl text-base-content">Confirmar Registro de Ponto</h3>
|
||||||
<p class="text-sm text-base-content/70">Verifique as informações antes de confirmar</p>
|
<p class="text-sm text-base-content/70">Verifique as informações antes de confirmar</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1376,7 +1448,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Conteúdo com rolagem -->
|
<!-- Conteúdo com rolagem -->
|
||||||
<div class="flex-1 overflow-y-auto pr-2 space-y-6">
|
<div class="flex-1 overflow-y-auto px-6 py-4 space-y-6 modal-scroll">
|
||||||
<!-- Card da Imagem -->
|
<!-- Card da Imagem -->
|
||||||
<div class="card bg-gradient-to-br from-base-200 to-base-300 shadow-lg border-2 border-primary/20">
|
<div class="card bg-gradient-to-br from-base-200 to-base-300 shadow-lg border-2 border-primary/20">
|
||||||
<div class="card-body p-6">
|
<div class="card-body p-6">
|
||||||
@@ -1483,9 +1555,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer fixo com botões -->
|
<!-- Footer fixo com botões -->
|
||||||
<div class="flex justify-end gap-3 pt-4 mt-4 border-t border-base-300 flex-shrink-0">
|
<div class="flex justify-end gap-3 px-6 py-4 border-t border-base-300 flex-shrink-0">
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline btn-lg"
|
class="btn btn-outline"
|
||||||
onclick={cancelarRegistro}
|
onclick={cancelarRegistro}
|
||||||
disabled={registrando}
|
disabled={registrando}
|
||||||
>
|
>
|
||||||
@@ -1493,7 +1565,7 @@
|
|||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary btn-lg gap-2"
|
class="btn btn-primary gap-2"
|
||||||
onclick={confirmarRegistro}
|
onclick={confirmarRegistro}
|
||||||
disabled={registrando}
|
disabled={registrando}
|
||||||
>
|
>
|
||||||
@@ -1507,7 +1579,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-backdrop" onclick={cancelarRegistro}></div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -1524,3 +1595,52 @@
|
|||||||
onClose={fecharModalErro}
|
onClose={fecharModalErro}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px) scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar customizada para os modais */
|
||||||
|
:global(.modal-scroll) {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: hsl(var(--bc) / 0.3) transparent;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.modal-scroll::-webkit-scrollbar) {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.modal-scroll::-webkit-scrollbar-track) {
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.modal-scroll::-webkit-scrollbar-thumb) {
|
||||||
|
background-color: hsl(var(--bc) / 0.3);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.modal-scroll::-webkit-scrollbar-thumb:hover) {
|
||||||
|
background-color: hsl(var(--bc) / 0.5);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -113,13 +113,28 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col items-center gap-2">
|
<div class="flex flex-col items-center gap-4 w-full">
|
||||||
<div class="text-4xl font-bold font-mono text-primary">{horaFormatada}</div>
|
<!-- Hora -->
|
||||||
<div class="text-sm text-base-content/70 capitalize">{dataFormatada}</div>
|
<div class="text-5xl font-black font-mono text-primary tracking-tight drop-shadow-sm">
|
||||||
<div class="flex items-center gap-2 text-xs">
|
{horaFormatada}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Data -->
|
||||||
|
<div class="text-base font-semibold text-base-content/80 capitalize">
|
||||||
|
{dataFormatada}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status de Sincronização -->
|
||||||
|
<div class="flex items-center gap-2 px-4 py-2 rounded-full {
|
||||||
|
sincronizado
|
||||||
|
? 'bg-success/20 text-success border border-success/30'
|
||||||
|
: erro
|
||||||
|
? 'bg-warning/20 text-warning border border-warning/30'
|
||||||
|
: 'bg-base-300/50 text-base-content/60 border border-base-300'
|
||||||
|
}">
|
||||||
{#if sincronizado}
|
{#if sincronizado}
|
||||||
<CheckCircle2 class="h-4 w-4 text-success" />
|
<CheckCircle2 class="h-4 w-4" strokeWidth={2.5} />
|
||||||
<span class="text-success">
|
<span class="text-sm font-semibold">
|
||||||
{#if usandoServidorExterno}
|
{#if usandoServidorExterno}
|
||||||
Sincronizado com servidor NTP
|
Sincronizado com servidor NTP
|
||||||
{:else}
|
{:else}
|
||||||
@@ -127,11 +142,11 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{:else if erro}
|
{:else if erro}
|
||||||
<AlertCircle class="h-4 w-4 text-warning" />
|
<AlertCircle class="h-4 w-4" strokeWidth={2.5} />
|
||||||
<span class="text-warning">{erro}</span>
|
<span class="text-sm font-semibold">{erro}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<Clock class="h-4 w-4 text-base-content/50" />
|
<Clock class="h-4 w-4" strokeWidth={2.5} />
|
||||||
<span class="text-base-content/50">Usando relógio do PC</span>
|
<span class="text-sm font-semibold">Usando relógio do PC</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
interface Props {
|
||||||
|
saldo?: {
|
||||||
|
trabalhadoMinutos: number;
|
||||||
|
esperadoMinutos: number;
|
||||||
|
diferencaMinutos: number;
|
||||||
|
} | null;
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
}
|
||||||
|
|
||||||
|
let { saldo, size = 'md' }: Props = $props();
|
||||||
|
|
||||||
|
function formatarMinutos(minutos: number): { horas: number; minutos: number } {
|
||||||
|
const horas = Math.floor(Math.abs(minutos) / 60);
|
||||||
|
const mins = Math.abs(minutos) % 60;
|
||||||
|
return { horas, minutos: mins };
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizeClasses = {
|
||||||
|
sm: 'text-xs px-2 py-1',
|
||||||
|
md: 'text-sm px-3 py-1.5',
|
||||||
|
lg: 'text-base px-4 py-2'
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if saldo}
|
||||||
|
{@const trabalhado = formatarMinutos(saldo.trabalhadoMinutos)}
|
||||||
|
{@const diferenca = formatarMinutos(saldo.diferencaMinutos)}
|
||||||
|
{@const sinalDiferenca = saldo.diferencaMinutos >= 0 ? '+' : '-'}
|
||||||
|
{@const isNegativo = saldo.diferencaMinutos < 0}
|
||||||
|
|
||||||
|
<div class="inline-flex items-center gap-1.5 {sizeClasses[size]} rounded-lg font-semibold shadow-sm border {
|
||||||
|
isNegativo
|
||||||
|
? 'bg-red-50 border-red-200 text-red-700 dark:bg-red-900/20 dark:border-red-800 dark:text-red-400'
|
||||||
|
: 'bg-green-50 border-green-200 text-green-700 dark:bg-green-900/20 dark:border-green-800 dark:text-green-400'
|
||||||
|
}">
|
||||||
|
<span class="font-bold">+{trabalhado.horas}h {trabalhado.minutos}min</span>
|
||||||
|
<span class="text-base-content/50">/</span>
|
||||||
|
<span class={isNegativo ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-400'}>
|
||||||
|
{sinalDiferenca}{diferenca.horas}h {diferenca.minutos}min
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<span class="badge badge-ghost {sizeClasses[size]}">-</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
import { formatarHoraPonto, getTipoRegistroLabel, formatarDataDDMMAAAA } from '$lib/utils/ponto';
|
import { formatarHoraPonto, getTipoRegistroLabel, formatarDataDDMMAAAA } from '$lib/utils/ponto';
|
||||||
import LocalizacaoIcon from '$lib/components/ponto/LocalizacaoIcon.svelte';
|
import LocalizacaoIcon from '$lib/components/ponto/LocalizacaoIcon.svelte';
|
||||||
import SaldoDiarioBadge from '$lib/components/ponto/SaldoDiarioBadge.svelte';
|
import SaldoDiarioBadge from '$lib/components/ponto/SaldoDiarioBadge.svelte';
|
||||||
|
import SaldoDiarioComparativoBadge from '$lib/components/ponto/SaldoDiarioComparativoBadge.svelte';
|
||||||
import jsPDF from 'jspdf';
|
import jsPDF from 'jspdf';
|
||||||
import autoTable from 'jspdf-autotable';
|
import autoTable from 'jspdf-autotable';
|
||||||
import logoGovPE from '$lib/assets/logo_governo_PE.png';
|
import logoGovPE from '$lib/assets/logo_governo_PE.png';
|
||||||
@@ -278,6 +279,7 @@
|
|||||||
|
|
||||||
// Agrupar registros por funcionário e data
|
// Agrupar registros por funcionário e data
|
||||||
const registrosAgrupados = $derived.by(() => {
|
const registrosAgrupados = $derived.by(() => {
|
||||||
|
const configData = config;
|
||||||
const agrupados: Record<
|
const agrupados: Record<
|
||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
@@ -289,6 +291,7 @@
|
|||||||
data: string;
|
data: string;
|
||||||
registros: Array<typeof registros[number]>;
|
registros: Array<typeof registros[number]>;
|
||||||
saldoDiario?: { saldoMinutos: number; horas: number; minutos: number; positivo: boolean };
|
saldoDiario?: { saldoMinutos: number; horas: number; minutos: number; positivo: boolean };
|
||||||
|
saldoDiarioComparativo?: { trabalhadoMinutos: number; esperadoMinutos: number; diferencaMinutos: number };
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
}
|
}
|
||||||
@@ -380,17 +383,54 @@
|
|||||||
return a.minuto - b.minuto;
|
return a.minuto - b.minuto;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Usar saldo diário da query se disponível, senão calcular
|
// Calcular saldo diário comparativo usando a soma dos saldos parciais
|
||||||
|
if (configData) {
|
||||||
|
// Calcular saldos parciais (Par 1, Par 2, etc.)
|
||||||
|
const saldosParciais = calcularSaldosParciais(grupoData.registros);
|
||||||
|
|
||||||
|
// Somar todos os saldos parciais para obter o total trabalhado
|
||||||
|
let totalTrabalhado = 0;
|
||||||
|
for (const saldoParcial of saldosParciais.values()) {
|
||||||
|
totalTrabalhado += saldoParcial.saldoMinutos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcular carga horária diária total esperada (soma dos dois pares)
|
||||||
|
const [horaEntradaConfig, minutoEntradaConfig] = configData.horarioEntrada.split(':').map(Number);
|
||||||
|
const [horaSaidaAlmocoConfig, minutoSaidaAlmocoConfig] = configData.horarioSaidaAlmoco.split(':').map(Number);
|
||||||
|
const [horaRetornoAlmocoConfig, minutoRetornoAlmocoConfig] = configData.horarioRetornoAlmoco.split(':').map(Number);
|
||||||
|
const [horaSaidaConfig, minutoSaidaConfig] = configData.horarioSaida.split(':').map(Number);
|
||||||
|
|
||||||
|
// Par 1: entrada -> saida_almoco
|
||||||
|
const minutosPar1EsperadoConfig = (horaSaidaAlmocoConfig * 60 + minutoSaidaAlmocoConfig) - (horaEntradaConfig * 60 + minutoEntradaConfig);
|
||||||
|
const minutosPar1EsperadoAjustadoConfig = minutosPar1EsperadoConfig < 0 ? minutosPar1EsperadoConfig + 24 * 60 : minutosPar1EsperadoConfig;
|
||||||
|
|
||||||
|
// Par 2: retorno_almoco -> saida
|
||||||
|
const minutosPar2EsperadoConfig = (horaSaidaConfig * 60 + minutoSaidaConfig) - (horaRetornoAlmocoConfig * 60 + minutoRetornoAlmocoConfig);
|
||||||
|
const minutosPar2EsperadoAjustadoConfig = minutosPar2EsperadoConfig < 0 ? minutosPar2EsperadoConfig + 24 * 60 : minutosPar2EsperadoConfig;
|
||||||
|
|
||||||
|
const cargaHorariaDiariaEsperadaMinutos = minutosPar1EsperadoAjustadoConfig + minutosPar2EsperadoAjustadoConfig;
|
||||||
|
|
||||||
|
// Calcular diferença em relação à carga horária diária total configurada
|
||||||
|
const diferencaMinutos = totalTrabalhado - cargaHorariaDiariaEsperadaMinutos;
|
||||||
|
|
||||||
|
// Armazenar saldo comparativo
|
||||||
|
grupoData.saldoDiarioComparativo = {
|
||||||
|
trabalhadoMinutos: totalTrabalhado,
|
||||||
|
esperadoMinutos: cargaHorariaDiariaEsperadaMinutos,
|
||||||
|
diferencaMinutos: diferencaMinutos
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Fallback: usar cálculo simples se não houver configuração
|
||||||
const primeiroRegistro = grupoData.registros[0];
|
const primeiroRegistro = grupoData.registros[0];
|
||||||
if (primeiroRegistro && 'saldoDiario' in primeiroRegistro && primeiroRegistro.saldoDiario) {
|
if (primeiroRegistro && 'saldoDiario' in primeiroRegistro && primeiroRegistro.saldoDiario) {
|
||||||
grupoData.saldoDiario = primeiroRegistro.saldoDiario;
|
grupoData.saldoDiario = primeiroRegistro.saldoDiario;
|
||||||
} else {
|
} else {
|
||||||
// Calcular saldo diário como diferença entre saída e entrada
|
|
||||||
grupoData.saldoDiario = calcularSaldoDiario(grupoData.registros);
|
grupoData.saldoDiario = calcularSaldoDiario(grupoData.registros);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return resultado;
|
return resultado;
|
||||||
});
|
});
|
||||||
@@ -1463,6 +1503,27 @@
|
|||||||
esperadoMinutos: saldoDiarioTotalEsperadoMinutos
|
esperadoMinutos: saldoDiarioTotalEsperadoMinutos
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Calcular carga horária diária total esperada para inicializar saldo acumulado
|
||||||
|
const [horaEntrada, minutoEntrada] = config.horarioEntrada.split(':').map(Number);
|
||||||
|
const [horaSaidaAlmoco, minutoSaidaAlmoco] = config.horarioSaidaAlmoco.split(':').map(Number);
|
||||||
|
const [horaRetornoAlmoco, minutoRetornoAlmoco] = config.horarioRetornoAlmoco.split(':').map(Number);
|
||||||
|
const [horaSaida, minutoSaida] = config.horarioSaida.split(':').map(Number);
|
||||||
|
|
||||||
|
const minutosPar1Esperado = (horaSaidaAlmoco * 60 + minutoSaidaAlmoco) - (horaEntrada * 60 + minutoEntrada);
|
||||||
|
const minutosPar1EsperadoAjustado = minutosPar1Esperado < 0 ? minutosPar1Esperado + 24 * 60 : minutosPar1Esperado;
|
||||||
|
|
||||||
|
const minutosPar2Esperado = (horaSaida * 60 + minutoSaida) - (horaRetornoAlmoco * 60 + minutoRetornoAlmoco);
|
||||||
|
const minutosPar2EsperadoAjustado = minutosPar2Esperado < 0 ? minutosPar2Esperado + 24 * 60 : minutosPar2Esperado;
|
||||||
|
|
||||||
|
const cargaHorariaDiariaTotalMinutos = minutosPar1EsperadoAjustado + minutosPar2EsperadoAjustado;
|
||||||
|
|
||||||
|
// Inicializar saldo diário acumulado com a carga horária total diária
|
||||||
|
let saldoDiarioAcumuladoMinutos = cargaHorariaDiariaTotalMinutos;
|
||||||
|
|
||||||
|
// Rastrear quais pares já foram processados para evitar decrementar múltiplas vezes
|
||||||
|
// Usar string como chave: "tipo-parIndex" ou "tipo-indice" para pares incompletos
|
||||||
|
const paresProcessadosParaSaldo = new Set<string>();
|
||||||
|
|
||||||
// Criar linhas da tabela
|
// Criar linhas da tabela
|
||||||
for (let i = 0; i < todosRegistros.length; i++) {
|
for (let i = 0; i < todosRegistros.length; i++) {
|
||||||
const reg = todosRegistros[i];
|
const reg = todosRegistros[i];
|
||||||
@@ -1496,23 +1557,63 @@
|
|||||||
// Verificar se há saldo esperado (par incompleto)
|
// Verificar se há saldo esperado (par incompleto)
|
||||||
const saldoEsperado = saldosEsperadosPorPar.get(i);
|
const saldoEsperado = saldosEsperadosPorPar.get(i);
|
||||||
if (saldoEsperado) {
|
if (saldoEsperado) {
|
||||||
// Par incompleto: mostrar saldo comparativo em vermelho
|
// Par incompleto: decrementar saldo acumulado
|
||||||
linha._saldoVermelho = true;
|
|
||||||
const sinalDiferenca = saldoEsperado.diferencaMinutos >= 0 ? '+' : '-';
|
// Decrementar saldo acumulado apenas uma vez por par
|
||||||
|
const chavePar = `${reg.tipo}-incompleto-${i}`;
|
||||||
|
if (!paresProcessadosParaSaldo.has(chavePar)) {
|
||||||
|
saldoDiarioAcumuladoMinutos -= saldoEsperado.trabalhadoMinutos;
|
||||||
|
paresProcessadosParaSaldo.add(chavePar);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcular saldo acumulado formatado
|
||||||
|
// Se saldoDiarioAcumuladoMinutos > 0: ainda falta trabalhar (mostrar como negativo)
|
||||||
|
// Se saldoDiarioAcumuladoMinutos < 0: trabalhou mais que o esperado (mostrar como positivo)
|
||||||
|
const saldoAcumuladoHoras = Math.floor(Math.abs(saldoDiarioAcumuladoMinutos) / 60);
|
||||||
|
const saldoAcumuladoMinutosResto = Math.abs(saldoDiarioAcumuladoMinutos) % 60;
|
||||||
|
// Inverter sinal: positivo quando trabalhou mais, negativo quando ainda falta
|
||||||
|
const sinalSaldo = saldoDiarioAcumuladoMinutos < 0 ? '+' : '-';
|
||||||
|
const trabalhouMaisQueEsperado = saldoDiarioAcumuladoMinutos < 0;
|
||||||
|
|
||||||
|
// Marcar linha para aplicar cor no saldo
|
||||||
|
if (trabalhouMaisQueEsperado) {
|
||||||
|
linha._saldoPositivo = true; // Verde: trabalhou mais que o esperado
|
||||||
|
} else {
|
||||||
|
linha._saldoNegativo = true; // Vermelho: ainda falta trabalhar
|
||||||
|
}
|
||||||
|
|
||||||
linha.push({
|
linha.push({
|
||||||
content: `+${saldoEsperado.trabalhadoHoras}h ${saldoEsperado.trabalhadoMinutosResto}min / ${sinalDiferenca}${saldoEsperado.diferencaHoras}h ${saldoEsperado.diferencaMinutosResto}min`,
|
content: `${saldoEsperado.trabalhadoHoras}h ${saldoEsperado.trabalhadoMinutosResto}min parcial | Saldo: ${sinalSaldo}${saldoAcumuladoHoras}h ${saldoAcumuladoMinutosResto}min`,
|
||||||
rowSpan: saldoEsperado.tamanhoPar
|
rowSpan: saldoEsperado.tamanhoPar
|
||||||
});
|
});
|
||||||
} else if (indexReal >= 0) {
|
} else if (indexReal >= 0) {
|
||||||
const saldoPar = saldosComparativosPorPar.get(indexReal);
|
const saldoPar = saldosComparativosPorPar.get(indexReal);
|
||||||
if (saldoPar) {
|
if (saldoPar) {
|
||||||
const sinalDiferenca = saldoPar.diferencaMinutos >= 0 ? '+' : '-';
|
// Decrementar saldo acumulado apenas uma vez por par
|
||||||
// Aplicar cor vermelha se diferença for negativa
|
const chavePar = `${reg.tipo}-${saldoPar.parIndex}`;
|
||||||
if (saldoPar.diferencaMinutos < 0) {
|
if (!paresProcessadosParaSaldo.has(chavePar)) {
|
||||||
linha._saldoVermelho = true;
|
saldoDiarioAcumuladoMinutos -= saldoPar.trabalhadoMinutos;
|
||||||
|
paresProcessadosParaSaldo.add(chavePar);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calcular saldo acumulado formatado
|
||||||
|
// Se saldoDiarioAcumuladoMinutos > 0: ainda falta trabalhar (mostrar como negativo)
|
||||||
|
// Se saldoDiarioAcumuladoMinutos < 0: trabalhou mais que o esperado (mostrar como positivo)
|
||||||
|
const saldoAcumuladoHoras = Math.floor(Math.abs(saldoDiarioAcumuladoMinutos) / 60);
|
||||||
|
const saldoAcumuladoMinutosResto = Math.abs(saldoDiarioAcumuladoMinutos) % 60;
|
||||||
|
// Inverter sinal: positivo quando trabalhou mais, negativo quando ainda falta
|
||||||
|
const sinalSaldo = saldoDiarioAcumuladoMinutos < 0 ? '+' : '-';
|
||||||
|
const trabalhouMaisQueEsperado = saldoDiarioAcumuladoMinutos < 0;
|
||||||
|
|
||||||
|
// Marcar linha para aplicar cor no saldo
|
||||||
|
if (trabalhouMaisQueEsperado) {
|
||||||
|
linha._saldoPositivo = true; // Verde: trabalhou mais que o esperado
|
||||||
|
} else {
|
||||||
|
linha._saldoNegativo = true; // Vermelho: ainda falta trabalhar
|
||||||
|
}
|
||||||
|
|
||||||
linha.push({
|
linha.push({
|
||||||
content: `+${saldoPar.trabalhadoHoras}h ${saldoPar.trabalhadoMinutosResto}min / ${sinalDiferenca}${saldoPar.diferencaHoras}h ${saldoPar.diferencaMinutosResto}min`,
|
content: `${saldoPar.trabalhadoHoras}h ${saldoPar.trabalhadoMinutosResto}min parcial | Saldo: ${sinalSaldo}${saldoAcumuladoHoras}h ${saldoAcumuladoMinutosResto}min`,
|
||||||
rowSpan: saldoPar.tamanhoPar
|
rowSpan: saldoPar.tamanhoPar
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -1537,54 +1638,122 @@
|
|||||||
// Verificar se há saldo esperado para este par
|
// Verificar se há saldo esperado para este par
|
||||||
const saldoEsperado = saldosEsperadosPorPar.get(i);
|
const saldoEsperado = saldosEsperadosPorPar.get(i);
|
||||||
if (saldoEsperado) {
|
if (saldoEsperado) {
|
||||||
// Par incompleto ou completamente não marcado: mostrar saldo em vermelho
|
// Par incompleto ou completamente não marcado: decrementar saldo acumulado
|
||||||
linha._saldoVermelho = true;
|
|
||||||
const sinalDiferenca = saldoEsperado.diferencaMinutos >= 0 ? '+' : '-';
|
// Decrementar saldo acumulado apenas uma vez por par
|
||||||
|
const chavePar = `${reg.tipo}-esperado-${i}`;
|
||||||
|
if (!paresProcessadosParaSaldo.has(chavePar)) {
|
||||||
|
// Se não há tempo trabalhado, decrementar o tempo esperado completo
|
||||||
|
if (saldoEsperado.trabalhadoMinutos === 0) {
|
||||||
|
saldoDiarioAcumuladoMinutos -= saldoEsperado.esperadoMinutos;
|
||||||
|
} else {
|
||||||
|
saldoDiarioAcumuladoMinutos -= saldoEsperado.trabalhadoMinutos;
|
||||||
|
}
|
||||||
|
paresProcessadosParaSaldo.add(chavePar);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcular saldo acumulado formatado
|
||||||
|
// Se saldoDiarioAcumuladoMinutos > 0: ainda falta trabalhar (mostrar como negativo)
|
||||||
|
// Se saldoDiarioAcumuladoMinutos < 0: trabalhou mais que o esperado (mostrar como positivo)
|
||||||
|
const saldoAcumuladoHoras = Math.floor(Math.abs(saldoDiarioAcumuladoMinutos) / 60);
|
||||||
|
const saldoAcumuladoMinutosResto = Math.abs(saldoDiarioAcumuladoMinutos) % 60;
|
||||||
|
// Inverter sinal: positivo quando trabalhou mais, negativo quando ainda falta
|
||||||
|
const sinalSaldo = saldoDiarioAcumuladoMinutos < 0 ? '+' : '-';
|
||||||
|
const trabalhouMaisQueEsperado = saldoDiarioAcumuladoMinutos < 0;
|
||||||
|
|
||||||
|
// Marcar linha para aplicar cor no saldo
|
||||||
|
if (trabalhouMaisQueEsperado) {
|
||||||
|
linha._saldoPositivo = true; // Verde: trabalhou mais que o esperado
|
||||||
|
} else {
|
||||||
|
linha._saldoNegativo = true; // Vermelho: ainda falta trabalhar
|
||||||
|
}
|
||||||
|
|
||||||
// Se par completamente não marcado (trabalhado = 0), mostrar apenas diferença negativa
|
// Se par completamente não marcado (trabalhado = 0), mostrar apenas diferença negativa
|
||||||
if (saldoEsperado.trabalhadoMinutos === 0) {
|
if (saldoEsperado.trabalhadoMinutos === 0) {
|
||||||
linha.push({
|
linha.push({
|
||||||
content: `+0h 0min / ${sinalDiferenca}${saldoEsperado.diferencaHoras}h ${saldoEsperado.diferencaMinutosResto}min`,
|
content: `0h 0min parcial | Saldo: ${sinalSaldo}${saldoAcumuladoHoras}h ${saldoAcumuladoMinutosResto}min`,
|
||||||
rowSpan: saldoEsperado.tamanhoPar
|
rowSpan: saldoEsperado.tamanhoPar
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
linha.push({
|
linha.push({
|
||||||
content: `+${saldoEsperado.trabalhadoHoras}h ${saldoEsperado.trabalhadoMinutosResto}min / ${sinalDiferenca}${saldoEsperado.diferencaHoras}h ${saldoEsperado.diferencaMinutosResto}min`,
|
content: `${saldoEsperado.trabalhadoHoras}h ${saldoEsperado.trabalhadoMinutosResto}min parcial | Saldo: ${sinalSaldo}${saldoAcumuladoHoras}h ${saldoAcumuladoMinutosResto}min`,
|
||||||
rowSpan: saldoEsperado.tamanhoPar
|
rowSpan: saldoEsperado.tamanhoPar
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (regsReais.length === 0) {
|
} else if (regsReais.length === 0) {
|
||||||
// Dia sem registros: calcular saldo esperado completo com diferença negativa
|
// Dia sem registros: calcular saldo esperado completo e decrementar saldo acumulado
|
||||||
if (reg.tipo === 'entrada') {
|
if (reg.tipo === 'entrada') {
|
||||||
// Par 1 completo esperado
|
// Par 1 completo esperado
|
||||||
const [horaEntrada, minutoEntrada] = config.horarioEntrada.split(':').map(Number);
|
|
||||||
const [horaSaidaAlmoco, minutoSaidaAlmoco] = config.horarioSaidaAlmoco.split(':').map(Number);
|
|
||||||
const minutosEntrada = horaEntrada * 60 + minutoEntrada;
|
const minutosEntrada = horaEntrada * 60 + minutoEntrada;
|
||||||
const minutosSaida = horaSaidaAlmoco * 60 + minutoSaidaAlmoco;
|
const minutosSaida = horaSaidaAlmoco * 60 + minutoSaidaAlmoco;
|
||||||
let saldoMinutos = minutosSaida - minutosEntrada;
|
let saldoMinutos = minutosSaida - minutosEntrada;
|
||||||
if (saldoMinutos < 0) saldoMinutos += 24 * 60;
|
if (saldoMinutos < 0) saldoMinutos += 24 * 60;
|
||||||
const horas = Math.floor(saldoMinutos / 60);
|
const horas = Math.floor(saldoMinutos / 60);
|
||||||
const minutos = saldoMinutos % 60;
|
const minutos = saldoMinutos % 60;
|
||||||
linha._saldoVermelho = true;
|
|
||||||
// Para dia sem registros, mostrar 0h trabalhado e diferença negativa
|
// Decrementar saldo acumulado apenas uma vez por par
|
||||||
|
const chavePar = `entrada-sem-registros-${i}`;
|
||||||
|
if (!paresProcessadosParaSaldo.has(chavePar)) {
|
||||||
|
saldoDiarioAcumuladoMinutos -= saldoMinutos;
|
||||||
|
paresProcessadosParaSaldo.add(chavePar);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcular saldo acumulado formatado
|
||||||
|
// Se saldoDiarioAcumuladoMinutos > 0: ainda falta trabalhar (mostrar como negativo)
|
||||||
|
// Se saldoDiarioAcumuladoMinutos < 0: trabalhou mais que o esperado (mostrar como positivo)
|
||||||
|
const saldoAcumuladoHoras = Math.floor(Math.abs(saldoDiarioAcumuladoMinutos) / 60);
|
||||||
|
const saldoAcumuladoMinutosResto = Math.abs(saldoDiarioAcumuladoMinutos) % 60;
|
||||||
|
// Inverter sinal: positivo quando trabalhou mais, negativo quando ainda falta
|
||||||
|
const sinalSaldo = saldoDiarioAcumuladoMinutos < 0 ? '+' : '-';
|
||||||
|
const trabalhouMaisQueEsperado = saldoDiarioAcumuladoMinutos < 0;
|
||||||
|
|
||||||
|
// Marcar linha para aplicar cor no saldo
|
||||||
|
if (trabalhouMaisQueEsperado) {
|
||||||
|
linha._saldoPositivo = true; // Verde: trabalhou mais que o esperado
|
||||||
|
} else {
|
||||||
|
linha._saldoNegativo = true; // Vermelho: ainda falta trabalhar
|
||||||
|
}
|
||||||
|
|
||||||
|
// Para dia sem registros, mostrar 0h trabalhado
|
||||||
linha.push({
|
linha.push({
|
||||||
content: `+0h 0min / -${horas}h ${minutos}min`,
|
content: `0h 0min parcial | Saldo: ${sinalSaldo}${saldoAcumuladoHoras}h ${saldoAcumuladoMinutosResto}min`,
|
||||||
rowSpan: 2 // entrada + saida_almoco
|
rowSpan: 2 // entrada + saida_almoco
|
||||||
});
|
});
|
||||||
} else if (reg.tipo === 'retorno_almoco') {
|
} else if (reg.tipo === 'retorno_almoco') {
|
||||||
// Par 2 completo esperado
|
// Par 2 completo esperado
|
||||||
const [horaRetornoAlmoco, minutoRetornoAlmoco] = config.horarioRetornoAlmoco.split(':').map(Number);
|
|
||||||
const [horaSaida, minutoSaida] = config.horarioSaida.split(':').map(Number);
|
|
||||||
const minutosEntrada = horaRetornoAlmoco * 60 + minutoRetornoAlmoco;
|
const minutosEntrada = horaRetornoAlmoco * 60 + minutoRetornoAlmoco;
|
||||||
const minutosSaida = horaSaida * 60 + minutoSaida;
|
const minutosSaida = horaSaida * 60 + minutoSaida;
|
||||||
let saldoMinutos = minutosSaida - minutosEntrada;
|
let saldoMinutos = minutosSaida - minutosEntrada;
|
||||||
if (saldoMinutos < 0) saldoMinutos += 24 * 60;
|
if (saldoMinutos < 0) saldoMinutos += 24 * 60;
|
||||||
const horas = Math.floor(saldoMinutos / 60);
|
const horas = Math.floor(saldoMinutos / 60);
|
||||||
const minutos = saldoMinutos % 60;
|
const minutos = saldoMinutos % 60;
|
||||||
linha._saldoVermelho = true;
|
|
||||||
// Para dia sem registros, mostrar 0h trabalhado e diferença negativa
|
// Decrementar saldo acumulado apenas uma vez por par
|
||||||
|
const chavePar = `retorno_almoco-sem-registros-${i}`;
|
||||||
|
if (!paresProcessadosParaSaldo.has(chavePar)) {
|
||||||
|
saldoDiarioAcumuladoMinutos -= saldoMinutos;
|
||||||
|
paresProcessadosParaSaldo.add(chavePar);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcular saldo acumulado formatado
|
||||||
|
// Se saldoDiarioAcumuladoMinutos > 0: ainda falta trabalhar (mostrar como negativo)
|
||||||
|
// Se saldoDiarioAcumuladoMinutos < 0: trabalhou mais que o esperado (mostrar como positivo)
|
||||||
|
const saldoAcumuladoHoras = Math.floor(Math.abs(saldoDiarioAcumuladoMinutos) / 60);
|
||||||
|
const saldoAcumuladoMinutosResto = Math.abs(saldoDiarioAcumuladoMinutos) % 60;
|
||||||
|
// Inverter sinal: positivo quando trabalhou mais, negativo quando ainda falta
|
||||||
|
const sinalSaldo = saldoDiarioAcumuladoMinutos < 0 ? '+' : '-';
|
||||||
|
const trabalhouMaisQueEsperado = saldoDiarioAcumuladoMinutos < 0;
|
||||||
|
|
||||||
|
// Marcar linha para aplicar cor no saldo
|
||||||
|
if (trabalhouMaisQueEsperado) {
|
||||||
|
linha._saldoPositivo = true; // Verde: trabalhou mais que o esperado
|
||||||
|
} else {
|
||||||
|
linha._saldoNegativo = true; // Vermelho: ainda falta trabalhar
|
||||||
|
}
|
||||||
|
|
||||||
|
// Para dia sem registros, mostrar 0h trabalhado
|
||||||
linha.push({
|
linha.push({
|
||||||
content: `+0h 0min / -${horas}h ${minutos}min`,
|
content: `0h 0min parcial | Saldo: ${sinalSaldo}${saldoAcumuladoHoras}h ${saldoAcumuladoMinutosResto}min`,
|
||||||
rowSpan: 2 // retorno_almoco + saida
|
rowSpan: 2 // retorno_almoco + saida
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -1599,13 +1768,34 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (saidaEsperadaExiste) {
|
if (saidaEsperadaExiste) {
|
||||||
// Par completamente não marcado: calcular saldo negativo
|
// Par completamente não marcado: calcular saldo negativo e decrementar saldo acumulado
|
||||||
const saldoEsperadoCompleto = saldosEsperadosPorPar.get(i);
|
const saldoEsperadoCompleto = saldosEsperadosPorPar.get(i);
|
||||||
if (saldoEsperadoCompleto) {
|
if (saldoEsperadoCompleto) {
|
||||||
linha._saldoVermelho = true;
|
// Decrementar saldo acumulado apenas uma vez por par
|
||||||
const sinalDiferenca = saldoEsperadoCompleto.diferencaMinutos >= 0 ? '+' : '-';
|
const chavePar = `${reg.tipo}-nao-marcado-${i}`;
|
||||||
|
if (!paresProcessadosParaSaldo.has(chavePar)) {
|
||||||
|
saldoDiarioAcumuladoMinutos -= saldoEsperadoCompleto.esperadoMinutos;
|
||||||
|
paresProcessadosParaSaldo.add(chavePar);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcular saldo acumulado formatado
|
||||||
|
// Se saldoDiarioAcumuladoMinutos > 0: ainda falta trabalhar (mostrar como negativo)
|
||||||
|
// Se saldoDiarioAcumuladoMinutos < 0: trabalhou mais que o esperado (mostrar como positivo)
|
||||||
|
const saldoAcumuladoHoras = Math.floor(Math.abs(saldoDiarioAcumuladoMinutos) / 60);
|
||||||
|
const saldoAcumuladoMinutosResto = Math.abs(saldoDiarioAcumuladoMinutos) % 60;
|
||||||
|
// Inverter sinal: positivo quando trabalhou mais, negativo quando ainda falta
|
||||||
|
const sinalSaldo = saldoDiarioAcumuladoMinutos < 0 ? '+' : '-';
|
||||||
|
const trabalhouMaisQueEsperado = saldoDiarioAcumuladoMinutos < 0;
|
||||||
|
|
||||||
|
// Marcar linha para aplicar cor no saldo
|
||||||
|
if (trabalhouMaisQueEsperado) {
|
||||||
|
linha._saldoPositivo = true; // Verde: trabalhou mais que o esperado
|
||||||
|
} else {
|
||||||
|
linha._saldoNegativo = true; // Vermelho: ainda falta trabalhar
|
||||||
|
}
|
||||||
|
|
||||||
linha.push({
|
linha.push({
|
||||||
content: `+0h 0min / ${sinalDiferenca}${saldoEsperadoCompleto.diferencaHoras}h ${saldoEsperadoCompleto.diferencaMinutosResto}min`,
|
content: `0h 0min parcial | Saldo: ${sinalSaldo}${saldoAcumuladoHoras}h ${saldoAcumuladoMinutosResto}min`,
|
||||||
rowSpan: saldoEsperadoCompleto.tamanhoPar
|
rowSpan: saldoEsperadoCompleto.tamanhoPar
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -1664,11 +1854,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Aplicar cor vermelha na coluna de saldo diário quando marcado
|
// Aplicar cor vermelha na coluna de saldo diário quando marcado
|
||||||
if (data.row.raw && (data.row.raw as any)._saldoVermelho) {
|
// Aplicar cor baseada no saldo acumulado
|
||||||
// Coluna de saldo diário (índice 3 se saldoDiario estiver ativo)
|
if (data.row.raw) {
|
||||||
|
const rowData = data.row.raw as any;
|
||||||
const indiceSaldoDiario = sections.saldoDiario ? 3 : -1;
|
const indiceSaldoDiario = sections.saldoDiario ? 3 : -1;
|
||||||
|
|
||||||
if (data.column.index === indiceSaldoDiario) {
|
if (data.column.index === indiceSaldoDiario) {
|
||||||
|
if (rowData._saldoNegativo) {
|
||||||
|
// Saldo negativo: cor vermelha
|
||||||
data.cell.styles.textColor = [200, 0, 0];
|
data.cell.styles.textColor = [200, 0, 0];
|
||||||
|
} else if (rowData._saldoPositivo) {
|
||||||
|
// Saldo positivo: cor verde
|
||||||
|
data.cell.styles.textColor = [0, 128, 0];
|
||||||
|
} else if (rowData._saldoVermelho) {
|
||||||
|
// Fallback para compatibilidade: cor vermelha
|
||||||
|
data.cell.styles.textColor = [200, 0, 0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3467,23 +3668,23 @@
|
|||||||
<div class="mb-6 pb-4 border-b border-base-300">
|
<div class="mb-6 pb-4 border-b border-base-300">
|
||||||
<div class="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-4">
|
<div class="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-4">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="flex items-center gap-3 mb-3">
|
<div class="flex items-center gap-3 mb-2">
|
||||||
<div class="p-2 bg-primary/10 rounded-lg">
|
<div class="p-2 bg-primary/10 rounded-lg">
|
||||||
<Users class="h-5 w-5 text-primary" strokeWidth={2.5} />
|
<Users class="h-5 w-5 text-primary" strokeWidth={2.5} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-xl text-base-content">
|
<h3 class="font-bold text-lg text-base-content">
|
||||||
{grupo.funcionario?.nome || 'Funcionário não encontrado'}
|
{grupo.funcionario?.nome || 'Funcionário não encontrado'}
|
||||||
</h3>
|
</h3>
|
||||||
{#if grupo.funcionario?.matricula}
|
{#if grupo.funcionario?.matricula}
|
||||||
<p class="text-sm text-base-content/70 mt-1">
|
<p class="text-sm text-base-content/70 mt-1">
|
||||||
Matrícula: <span class="font-semibold">{grupo.funcionario.matricula}</span>
|
<span class="font-medium">Matrícula:</span> <span class="font-semibold">{grupo.funcionario.matricula}</span>
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if grupo.funcionario?.descricaoCargo}
|
{#if grupo.funcionario?.descricaoCargo}
|
||||||
<p class="text-sm text-base-content/60 ml-11">
|
<p class="text-sm text-base-content/60 ml-11 font-medium">
|
||||||
{grupo.funcionario.descricaoCargo}
|
{grupo.funcionario.descricaoCargo}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -3533,29 +3734,31 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="overflow-x-auto max-h-[600px] overflow-y-auto border border-base-300 rounded-xl shadow-inner bg-base-100/50">
|
<div class="overflow-x-auto max-h-[600px] overflow-y-auto border border-base-300 rounded-xl shadow-inner bg-base-100/50">
|
||||||
<table class="table table-zebra">
|
<table class="table table-zebra w-full">
|
||||||
<thead class="sticky top-0 z-10 shadow-md bg-gradient-to-r from-base-300 to-base-200">
|
<thead class="sticky top-0 z-10 shadow-md bg-gradient-to-r from-base-300/95 to-base-200/95 backdrop-blur-sm">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Data</th>
|
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400 text-sm">Data</th>
|
||||||
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Tipo</th>
|
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400 text-sm">Tipo</th>
|
||||||
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Horário</th>
|
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400 text-sm">Horário</th>
|
||||||
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Saldo Parcial</th>
|
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400 text-sm">Saldo Parcial</th>
|
||||||
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Saldo Diário</th>
|
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400 text-sm">Saldo Diário</th>
|
||||||
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Localização</th>
|
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400 text-sm">Localização</th>
|
||||||
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Status</th>
|
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400 text-sm">Status</th>
|
||||||
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Ações</th>
|
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400 text-sm">Ações</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each Object.values(grupo.registrosPorData) as grupoData}
|
{#each Object.values(grupo.registrosPorData) as grupoData, dataIndex}
|
||||||
{@const totalRegistros = grupoData.registros.length}
|
{@const totalRegistros = grupoData.registros.length}
|
||||||
{@const dataFormatada = formatarDataDDMMAAAA(grupoData.data)}
|
{@const dataFormatada = formatarDataDDMMAAAA(grupoData.data)}
|
||||||
{@const saldosParciais = calcularSaldosParciais(grupoData.registros)}
|
{@const saldosParciais = calcularSaldosParciais(grupoData.registros)}
|
||||||
|
{@const isUltimoDia = dataIndex === Object.values(grupo.registrosPorData).length - 1}
|
||||||
{#each grupoData.registros as registro, index}
|
{#each grupoData.registros as registro, index}
|
||||||
{@const saldoParcial = saldosParciais.get(index)}
|
{@const saldoParcial = saldosParciais.get(index)}
|
||||||
<tr>
|
<tr class="hover:bg-base-200/50 transition-colors {dataIndex % 2 === 0 ? 'bg-base-100/40' : 'bg-base-50/60'} {!isUltimoDia && index === totalRegistros - 1 ? 'border-b-4 border-base-300' : ''}">
|
||||||
<td class="whitespace-nowrap">{dataFormatada}</td>
|
<td class="whitespace-nowrap font-semibold text-sm">{dataFormatada}</td>
|
||||||
<td class="whitespace-nowrap">
|
<td class="whitespace-nowrap">
|
||||||
|
<span class="badge badge-outline badge-sm font-medium text-xs">
|
||||||
{config
|
{config
|
||||||
? getTipoRegistroLabel(registro.tipo, {
|
? getTipoRegistroLabel(registro.tipo, {
|
||||||
nomeEntrada: config.nomeEntrada,
|
nomeEntrada: config.nomeEntrada,
|
||||||
@@ -3564,20 +3767,27 @@
|
|||||||
nomeSaida: config.nomeSaida,
|
nomeSaida: config.nomeSaida,
|
||||||
})
|
})
|
||||||
: getTipoRegistroLabel(registro.tipo)}
|
: getTipoRegistroLabel(registro.tipo)}
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="whitespace-nowrap">{formatarHoraPonto(registro.hora, registro.minuto)}</td>
|
<td class="whitespace-nowrap font-mono text-sm font-medium">{formatarHoraPonto(registro.hora, registro.minuto)}</td>
|
||||||
<td class="whitespace-nowrap">
|
<td class="whitespace-nowrap">
|
||||||
{#if saldoParcial}
|
{#if saldoParcial}
|
||||||
<span class="badge badge-info badge-sm font-semibold">
|
<span class="badge badge-info badge-sm font-semibold shadow-sm text-xs">
|
||||||
Par {saldoParcial.parNumero}: +{saldoParcial.horas}h {saldoParcial.minutos}min
|
Par {saldoParcial.parNumero}: +{saldoParcial.horas}h {saldoParcial.minutos}min
|
||||||
</span>
|
</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="text-base-content/40">-</span>
|
<span class="text-base-content/30 text-sm">-</span>
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
{#if index === 0}
|
{#if index === 0}
|
||||||
<td class="whitespace-nowrap" rowspan={totalRegistros}>
|
<td class="whitespace-nowrap" rowspan={totalRegistros}>
|
||||||
|
{#if grupoData.saldoDiarioComparativo}
|
||||||
|
<SaldoDiarioComparativoBadge saldo={grupoData.saldoDiarioComparativo} size="md" />
|
||||||
|
{:else if grupoData.saldoDiario}
|
||||||
<SaldoDiarioBadge saldo={grupoData.saldoDiario} size="md" />
|
<SaldoDiarioBadge saldo={grupoData.saldoDiario} size="md" />
|
||||||
|
{:else}
|
||||||
|
<span class="badge badge-ghost badge-lg">-</span>
|
||||||
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
{/if}
|
{/if}
|
||||||
<td class="whitespace-nowrap">
|
<td class="whitespace-nowrap">
|
||||||
@@ -3585,19 +3795,19 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="whitespace-nowrap">
|
<td class="whitespace-nowrap">
|
||||||
<span
|
<span
|
||||||
class="badge badge-lg font-semibold {registro.dentroDoPrazo ? 'badge-success shadow-sm' : 'badge-error shadow-sm'}"
|
class="badge badge-sm font-semibold {registro.dentroDoPrazo ? 'badge-success shadow-sm' : 'badge-error shadow-sm'}"
|
||||||
>
|
>
|
||||||
{registro.dentroDoPrazo ? '✓ Dentro do Prazo' : '✗ Fora do Prazo'}
|
{registro.dentroDoPrazo ? '✓ Dentro do Prazo' : '✗ Fora do Prazo'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="whitespace-nowrap">
|
<td class="whitespace-nowrap">
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-outline btn-primary gap-2 hover:btn-primary hover:shadow-md transition-all"
|
class="btn btn-sm btn-outline btn-primary gap-2 hover:btn-primary hover:shadow-md transition-all text-xs"
|
||||||
onclick={() => abrirModalDetalhes(registro._id)}
|
onclick={() => abrirModalDetalhes(registro._id)}
|
||||||
title="Ver Detalhes"
|
title="Ver Detalhes"
|
||||||
>
|
>
|
||||||
<FileText class="h-4 w-4" />
|
<FileText class="h-3 w-3" />
|
||||||
Detalhes
|
<span class="text-xs">Detalhes</span>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
Reference in New Issue
Block a user