Merge pull request #41 from killer-cf/call-audio-video-jitsi
Call audio video jitsi
This commit is contained in:
@@ -34,9 +34,9 @@
|
|||||||
inset: -2px;
|
inset: -2px;
|
||||||
border-radius: 1.15rem;
|
border-radius: 1.15rem;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 0 0 1px rgba(15, 23, 42, 0.04),
|
0 0 0 1px hsl(var(--bc) / 0.04),
|
||||||
0 14px 32px -22px rgba(15, 23, 42, 0.45),
|
0 14px 32px -22px hsl(var(--bc) / 0.45),
|
||||||
0 6px 18px -16px rgba(102, 126, 234, 0.35);
|
0 6px 18px -16px hsl(var(--p) / 0.35);
|
||||||
opacity: 0.55;
|
opacity: 0.55;
|
||||||
transition: opacity 220ms ease, transform 220ms ease;
|
transition: opacity 220ms ease, transform 220ms ease;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.12), rgba(118, 75, 162, 0.12));
|
background: linear-gradient(135deg, hsl(var(--p) / 0.12), hsl(var(--s) / 0.12));
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0.96);
|
transform: scale(0.96);
|
||||||
transition: opacity 220ms ease, transform 220ms ease;
|
transition: opacity 220ms ease, transform 220ms ease;
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
|
|
||||||
:where(.card, .card-hover):hover {
|
:where(.card, .card-hover):hover {
|
||||||
transform: translateY(-6px);
|
transform: translateY(-6px);
|
||||||
box-shadow: 0 20px 45px -20px rgba(15, 23, 42, 0.35);
|
box-shadow: 0 20px 45px -20px hsl(var(--bc) / 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
:where(.card, .card-hover):hover::before {
|
:where(.card, .card-hover):hover::before {
|
||||||
@@ -156,6 +156,15 @@ html[data-theme="sgse-blue"] body,
|
|||||||
--tab-radius: 0.5rem;
|
--tab-radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Garantir que todas as variáveis CSS sejam aplicadas em todos os elementos */
|
||||||
|
html[data-theme] {
|
||||||
|
color-scheme: var(--color-scheme, light);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme] * {
|
||||||
|
color-scheme: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
html[data-theme="sgse-green"],
|
html[data-theme="sgse-green"],
|
||||||
html[data-theme="sgse-green"] body,
|
html[data-theme="sgse-green"] body,
|
||||||
[data-theme="sgse-green"] {
|
[data-theme="sgse-green"] {
|
||||||
@@ -388,4 +397,84 @@ html[data-theme="sgse-corporate"] body,
|
|||||||
--border-btn: 1px;
|
--border-btn: 1px;
|
||||||
--tab-border: 1px;
|
--tab-border: 1px;
|
||||||
--tab-radius: 0.5rem;
|
--tab-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tema Light customizado para garantir funcionamento completo */
|
||||||
|
html[data-theme="light"],
|
||||||
|
html[data-theme="light"] body,
|
||||||
|
[data-theme="light"] {
|
||||||
|
color-scheme: light;
|
||||||
|
--p: 217 91% 60%;
|
||||||
|
--pf: 217 91% 50%;
|
||||||
|
--pc: 0 0% 100%;
|
||||||
|
--s: 217 91% 60%;
|
||||||
|
--sf: 217 91% 50%;
|
||||||
|
--sc: 0 0% 100%;
|
||||||
|
--a: 217 91% 60%;
|
||||||
|
--af: 217 91% 50%;
|
||||||
|
--ac: 0 0% 100%;
|
||||||
|
--n: 217 20% 17%;
|
||||||
|
--nf: 217 20% 10%;
|
||||||
|
--nc: 0 0% 100%;
|
||||||
|
--b1: 0 0% 100%;
|
||||||
|
--b2: 217 20% 95%;
|
||||||
|
--b3: 217 20% 90%;
|
||||||
|
--bc: 217 20% 17%;
|
||||||
|
--in: 217 91% 60%;
|
||||||
|
--inc: 0 0% 100%;
|
||||||
|
--su: 142 76% 36%;
|
||||||
|
--suc: 0 0% 100%;
|
||||||
|
--wa: 38 92% 50%;
|
||||||
|
--wac: 0 0% 100%;
|
||||||
|
--er: 0 84% 60%;
|
||||||
|
--erc: 0 0% 100%;
|
||||||
|
--rounded-box: 1rem;
|
||||||
|
--rounded-btn: 0.5rem;
|
||||||
|
--rounded-badge: 1.9rem;
|
||||||
|
--animation-btn: 0.25s;
|
||||||
|
--animation-input: 0.2s;
|
||||||
|
--btn-focus-scale: 0.95;
|
||||||
|
--border-btn: 1px;
|
||||||
|
--tab-border: 1px;
|
||||||
|
--tab-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tema Dark customizado para garantir funcionamento completo */
|
||||||
|
html[data-theme="dark"],
|
||||||
|
html[data-theme="dark"] body,
|
||||||
|
[data-theme="dark"] {
|
||||||
|
color-scheme: dark;
|
||||||
|
--p: 217 91% 60%;
|
||||||
|
--pf: 217 91% 50%;
|
||||||
|
--pc: 0 0% 100%;
|
||||||
|
--s: 217 91% 60%;
|
||||||
|
--sf: 217 91% 50%;
|
||||||
|
--sc: 0 0% 100%;
|
||||||
|
--a: 217 91% 60%;
|
||||||
|
--af: 217 91% 50%;
|
||||||
|
--ac: 0 0% 100%;
|
||||||
|
--n: 217 30% 15%;
|
||||||
|
--nf: 217 30% 8%;
|
||||||
|
--nc: 0 0% 100%;
|
||||||
|
--b1: 217 30% 10%;
|
||||||
|
--b2: 217 30% 15%;
|
||||||
|
--b3: 217 30% 20%;
|
||||||
|
--bc: 217 10% 90%;
|
||||||
|
--in: 217 91% 60%;
|
||||||
|
--inc: 0 0% 100%;
|
||||||
|
--su: 142 76% 36%;
|
||||||
|
--suc: 0 0% 100%;
|
||||||
|
--wa: 38 92% 50%;
|
||||||
|
--wac: 0 0% 100%;
|
||||||
|
--er: 0 84% 60%;
|
||||||
|
--erc: 0 0% 100%;
|
||||||
|
--rounded-box: 1rem;
|
||||||
|
--rounded-btn: 0.5rem;
|
||||||
|
--rounded-badge: 1.9rem;
|
||||||
|
--animation-btn: 0.25s;
|
||||||
|
--animation-input: 0.2s;
|
||||||
|
--btn-focus-scale: 0.95;
|
||||||
|
--border-btn: 1px;
|
||||||
|
--tab-border: 1px;
|
||||||
|
--tab-radius: 0.5rem;
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -237,18 +237,36 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="modal modal-open" style="display: flex; align-items: center; justify-content: center;">
|
<div
|
||||||
<div class="modal-box max-w-2xl 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;"
|
||||||
|
onkeydown={(e) => e.key === 'Escape' && onClose()}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="modal-comprovante-title"
|
||||||
|
>
|
||||||
|
<!-- Backdrop com blur -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-black/50 backdrop-blur-sm transition-opacity duration-200"
|
||||||
|
onclick={onClose}
|
||||||
|
></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 -->
|
<!-- Header fixo -->
|
||||||
<div class="flex items-center justify-between mb-4 pb-4 border-b border-base-300 flex-shrink-0">
|
<div class="flex items-center justify-between mb-4 pb-4 border-b border-base-300 flex-shrink-0">
|
||||||
<h3 class="font-bold text-lg">Comprovante de Registro de Ponto</h3>
|
<h3 id="modal-comprovante-title" class="font-bold text-lg">Comprovante de Registro de Ponto</h3>
|
||||||
<button class="btn btn-sm btn-circle btn-ghost hover:bg-base-300" onclick={onClose}>
|
<button class="btn btn-sm btn-circle btn-ghost hover:bg-base-300" onclick={onClose}>
|
||||||
<X class="h-5 w-5" />
|
<X class="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Conteúdo com rolagem -->
|
<!-- Conteúdo com rolagem -->
|
||||||
<div class="flex-1 overflow-y-auto pr-2">
|
<div class="flex-1 overflow-y-auto pr-2 modal-scroll">
|
||||||
{#if registroQuery === undefined}
|
{#if registroQuery === undefined}
|
||||||
<div class="flex justify-center items-center py-8">
|
<div class="flex justify-center items-center py-8">
|
||||||
<span class="loading loading-spinner loading-lg"></span>
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
@@ -339,6 +357,54 @@
|
|||||||
<button class="btn btn-outline" onclick={onClose}>Fechar</button>
|
<button class="btn btn-outline" onclick={onClose}>Fechar</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-backdrop" onclick={onClose}></div>
|
|
||||||
</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>
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -21,17 +21,26 @@
|
|||||||
|
|
||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
|
|
||||||
|
// Chave de refresh para forçar atualização das queries após registro
|
||||||
|
let refreshKey = $state(0);
|
||||||
|
|
||||||
// Queries
|
// Queries
|
||||||
const currentUser = useQuery(api.auth.getCurrentUser, {});
|
const currentUser = useQuery(api.auth.getCurrentUser, {});
|
||||||
const configQuery = useQuery(api.configuracaoPonto.obterConfiguracao, {});
|
const configQuery = useQuery(api.configuracaoPonto.obterConfiguracao, {});
|
||||||
const registrosHojeQuery = useQuery(api.pontos.listarRegistrosDia, {});
|
|
||||||
|
|
||||||
// Query para histórico e saldo do dia
|
// Query para histórico e saldo do dia
|
||||||
const funcionarioId = $derived(currentUser?.data?.funcionarioId ?? null);
|
const funcionarioId = $derived(currentUser?.data?.funcionarioId ?? null);
|
||||||
const dataHoje = $derived(new Date().toISOString().split('T')[0]!);
|
const dataHoje = $derived(new Date().toISOString().split('T')[0]!);
|
||||||
|
|
||||||
|
// Usar refreshKey para forçar atualização após registro
|
||||||
|
const registrosHojeQuery = useQuery(
|
||||||
|
api.pontos.listarRegistrosDia,
|
||||||
|
{ data: dataHoje, _refresh: refreshKey }
|
||||||
|
);
|
||||||
|
|
||||||
const historicoSaldoQuery = useQuery(
|
const historicoSaldoQuery = useQuery(
|
||||||
api.pontos.obterHistoricoESaldoDia,
|
api.pontos.obterHistoricoESaldoDia,
|
||||||
funcionarioId && dataHoje ? { funcionarioId, data: dataHoje } : 'skip'
|
funcionarioId && dataHoje ? { funcionarioId, data: dataHoje, _refresh: refreshKey } : 'skip'
|
||||||
);
|
);
|
||||||
|
|
||||||
// Query para verificar dispensa ativa
|
// Query para verificar dispensa ativa
|
||||||
@@ -55,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);
|
||||||
@@ -284,12 +294,26 @@
|
|||||||
justificativa = ''; // Limpar justificativa após registro
|
justificativa = ''; // Limpar justificativa após registro
|
||||||
mostrandoModalConfirmacao = false;
|
mostrandoModalConfirmacao = false;
|
||||||
|
|
||||||
|
// Forçar atualização das queries para mostrar o novo registro
|
||||||
|
refreshKey++;
|
||||||
|
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.log('[RegistroPonto] Registro bem-sucedido, refreshKey incrementado:', refreshKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aguardar um pouco para garantir que o backend processou o registro
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
|
||||||
|
// Forçar mais uma atualização após o delay para garantir sincronização
|
||||||
|
refreshKey++;
|
||||||
|
|
||||||
// Mostrar comprovante após 1 segundo
|
// Mostrar comprovante após 1 segundo
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
mostrandoComprovante = true;
|
mostrandoComprovante = true;
|
||||||
}, 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.';
|
||||||
|
|
||||||
@@ -367,6 +391,7 @@
|
|||||||
} finally {
|
} finally {
|
||||||
registrando = false;
|
registrando = false;
|
||||||
coletandoInfo = false;
|
coletandoInfo = false;
|
||||||
|
aguardandoProcessamento = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,6 +517,7 @@
|
|||||||
|
|
||||||
function confirmarRegistro() {
|
function confirmarRegistro() {
|
||||||
mostrandoModalConfirmacao = false;
|
mostrandoModalConfirmacao = false;
|
||||||
|
aguardandoProcessamento = true;
|
||||||
registrarPonto();
|
registrarPonto();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -731,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 () => {
|
||||||
@@ -799,7 +807,7 @@
|
|||||||
{ tipo: 'saida', horario: config.horarioSaida, label: config.nomeSaida || 'Saída 2' }
|
{ tipo: 'saida', horario: config.horarioSaida, label: config.nomeSaida || 'Saída 2' }
|
||||||
];
|
];
|
||||||
|
|
||||||
return horarios.map((h) => {
|
const resultado = horarios.map((h) => {
|
||||||
const registro = registrosHoje.find((r) => r.tipo === h.tipo);
|
const registro = registrosHoje.find((r) => r.tipo === h.tipo);
|
||||||
return {
|
return {
|
||||||
...h,
|
...h,
|
||||||
@@ -808,6 +816,17 @@
|
|||||||
dentroDoPrazo: registro?.dentroDoPrazo ?? null
|
dentroDoPrazo: registro?.dentroDoPrazo ?? null
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Log para debug (apenas em desenvolvimento)
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.log('[RegistroPonto] mapaHorarios atualizado:', {
|
||||||
|
totalRegistrosHoje: registrosHoje.length,
|
||||||
|
horariosComRegistro: resultado.filter(h => h.registrado).length,
|
||||||
|
registrosHoje: registrosHoje.map(r => ({ tipo: r.tipo, hora: `${r.hora}:${r.minuto}` }))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultado;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dados do histórico e saldo
|
// Dados do histórico e saldo
|
||||||
@@ -889,71 +908,95 @@
|
|||||||
</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">
|
||||||
<RelogioSincronizado />
|
<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>
|
</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">
|
<!-- Relógio Sincronizado -->
|
||||||
<p class="text-lg font-semibold">Próximo registro: {tipoLabel}</p>
|
<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 />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Campo de Justificativa (Opcional) -->
|
<!-- Botão de Registro -->
|
||||||
<div class="w-full">
|
<button
|
||||||
<label for="justificativa" class="label">
|
class="btn btn-primary w-full shadow-lg hover:shadow-xl transition-all duration-300 font-semibold rounded-xl gap-2 mb-5"
|
||||||
<span class="label-text">Justificativa (Opcional)</span>
|
onclick={iniciarRegistroComFoto}
|
||||||
</label>
|
disabled={!podeRegistrar}
|
||||||
<textarea
|
title={!temFuncionarioAssociado
|
||||||
id="justificativa"
|
? 'Você não possui funcionário associado à sua conta'
|
||||||
class="textarea textarea-bordered w-full"
|
: estaDispensado
|
||||||
placeholder="Digite uma justificativa para este registro de ponto (opcional)"
|
? 'Você está dispensado de registrar ponto no momento'
|
||||||
bind:value={justificativa}
|
: ''}
|
||||||
disabled={registrando}
|
>
|
||||||
rows="3"
|
{#if registrando}
|
||||||
></textarea>
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
</div>
|
{#if coletandoInfo}
|
||||||
|
Coletando informações...
|
||||||
<button
|
|
||||||
class="btn btn-primary btn-lg"
|
|
||||||
onclick={iniciarRegistroComFoto}
|
|
||||||
disabled={!podeRegistrar}
|
|
||||||
title={!temFuncionarioAssociado
|
|
||||||
? 'Você não possui funcionário associado à sua conta'
|
|
||||||
: estaDispensado
|
|
||||||
? 'Você está dispensado de registrar ponto no momento'
|
|
||||||
: ''}
|
|
||||||
>
|
|
||||||
{#if registrando}
|
|
||||||
<span class="loading loading-spinner loading-sm"></span>
|
|
||||||
{#if coletandoInfo}
|
|
||||||
Coletando informações...
|
|
||||||
{:else}
|
|
||||||
Registrando...
|
|
||||||
{/if}
|
|
||||||
{:else if !temFuncionarioAssociado}
|
|
||||||
<XCircle class="h-5 w-5" />
|
|
||||||
Funcionário Não Associado
|
|
||||||
{:else if estaDispensado}
|
|
||||||
<XCircle class="h-5 w-5" />
|
|
||||||
Registro Indisponível
|
|
||||||
{:else if proximoTipo === 'entrada' || proximoTipo === 'retorno_almoco'}
|
|
||||||
<LogIn class="h-5 w-5" />
|
|
||||||
Registrar Entrada
|
|
||||||
{:else}
|
{:else}
|
||||||
<LogOut class="h-5 w-5" />
|
Registrando...
|
||||||
Registrar Saída
|
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
{:else if !temFuncionarioAssociado}
|
||||||
|
<XCircle class="h-5 w-5" />
|
||||||
|
Funcionário Não Associado
|
||||||
|
{:else if estaDispensado}
|
||||||
|
<XCircle class="h-5 w-5" />
|
||||||
|
Registro Indisponível
|
||||||
|
{:else if proximoTipo === 'entrada' || proximoTipo === 'retorno_almoco'}
|
||||||
|
<LogIn class="h-5 w-5" />
|
||||||
|
Registrar Entrada
|
||||||
|
{:else}
|
||||||
|
<LogOut class="h-5 w-5" />
|
||||||
|
Registrar Saída
|
||||||
|
{/if}
|
||||||
|
</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>
|
||||||
@@ -1287,49 +1330,111 @@
|
|||||||
<!-- 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>
|
||||||
|
<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>
|
</div>
|
||||||
<div class="min-h-[200px] flex items-center justify-center py-4">
|
|
||||||
<WebcamCapture
|
<!-- Conteúdo com rolagem -->
|
||||||
onCapture={handleWebcamCapture}
|
<div class="flex-1 overflow-y-auto px-6 py-4 modal-scroll">
|
||||||
onCancel={handleWebcamCancel}
|
<div class="min-h-[200px] flex items-center justify-center">
|
||||||
onError={handleWebcamError}
|
<WebcamCapture
|
||||||
autoCapture={false}
|
onCapture={handleWebcamCapture}
|
||||||
fotoObrigatoria={true}
|
onCancel={handleWebcamCancel}
|
||||||
/>
|
onError={handleWebcamError}
|
||||||
|
autoCapture={false}
|
||||||
|
fotoObrigatoria={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form
|
</div>
|
||||||
method="dialog"
|
{/if}
|
||||||
class="modal-backdrop"
|
|
||||||
onsubmit={(e) => {
|
<!-- Modal de Aguardando Processamento -->
|
||||||
e.preventDefault();
|
{#if aguardandoProcessamento}
|
||||||
handleWebcamCancel();
|
<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"
|
||||||
|
>
|
||||||
|
<!-- Backdrop -->
|
||||||
|
<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);"
|
||||||
>
|
>
|
||||||
<button type="submit" aria-label="Fechar modal">fechar</button>
|
<div class="flex flex-col items-center gap-4 text-center">
|
||||||
</form>
|
<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>
|
||||||
@@ -1343,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">
|
||||||
@@ -1450,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}
|
||||||
>
|
>
|
||||||
@@ -1460,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}
|
||||||
>
|
>
|
||||||
@@ -1474,7 +1579,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-backdrop" onclick={cancelarRegistro}></div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -1491,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 text-green-600 dark:text-green-400">+{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}
|
||||||
|
|
||||||
@@ -22,18 +22,29 @@
|
|||||||
let previewUrl = $state<string | null>(null);
|
let previewUrl = $state<string | null>(null);
|
||||||
let videoReady = $state(false);
|
let videoReady = $state(false);
|
||||||
|
|
||||||
|
// Flag para evitar múltiplas chamadas de play() simultâneas
|
||||||
|
let playEmAndamento = $state(false);
|
||||||
|
|
||||||
// Efeito para garantir que o vídeo seja exibido quando o stream for atribuído
|
// Efeito para garantir que o vídeo seja exibido quando o stream for atribuído
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (stream && videoElement) {
|
if (stream && videoElement && !playEmAndamento) {
|
||||||
// Sempre atualizar srcObject quando o stream mudar
|
// Sempre atualizar srcObject quando o stream mudar
|
||||||
if (videoElement.srcObject !== stream) {
|
if (videoElement.srcObject !== stream) {
|
||||||
videoElement.srcObject = stream;
|
videoElement.srcObject = stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tentar reproduzir se ainda não estiver pronto
|
// Tentar reproduzir se ainda não estiver pronto e não houver outra chamada em andamento
|
||||||
if (!videoReady && videoElement.readyState < 2) {
|
if (!videoReady && videoElement.readyState < 2) {
|
||||||
|
// Verificar se já não está reproduzindo
|
||||||
|
if (!videoElement.paused && videoElement.readyState >= 2) {
|
||||||
|
videoReady = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
playEmAndamento = true;
|
||||||
videoElement.play()
|
videoElement.play()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
playEmAndamento = false;
|
||||||
// Aguardar um pouco para garantir que o vídeo esteja realmente reproduzindo
|
// Aguardar um pouco para garantir que o vídeo esteja realmente reproduzindo
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (videoElement && videoElement.readyState >= 2 && videoElement.videoWidth > 0) {
|
if (videoElement && videoElement.readyState >= 2 && videoElement.videoWidth > 0) {
|
||||||
@@ -42,7 +53,11 @@
|
|||||||
}, 300);
|
}, 300);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.warn('Erro ao reproduzir vídeo no effect:', err);
|
playEmAndamento = false;
|
||||||
|
// Ignorar AbortError - é esperado quando há uma nova requisição de load
|
||||||
|
if (err.name !== 'AbortError') {
|
||||||
|
console.warn('Erro ao reproduzir vídeo no effect:', err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else if (videoElement.readyState >= 2 && videoElement.videoWidth > 0) {
|
} else if (videoElement.readyState >= 2 && videoElement.videoWidth > 0) {
|
||||||
videoReady = true;
|
videoReady = true;
|
||||||
@@ -219,35 +234,50 @@
|
|||||||
videoElement.addEventListener('playing', onPlaying);
|
videoElement.addEventListener('playing', onPlaying);
|
||||||
videoElement.addEventListener('error', onError);
|
videoElement.addEventListener('error', onError);
|
||||||
|
|
||||||
// Tentar reproduzir
|
// Tentar reproduzir apenas se não estiver já reproduzindo
|
||||||
videoElement.play()
|
if (videoElement.paused) {
|
||||||
.then(() => {
|
playEmAndamento = true;
|
||||||
console.log('Vídeo iniciado, readyState:', videoElement?.readyState);
|
videoElement.play()
|
||||||
// Se já tiver metadata e dimensões, resolver imediatamente
|
.then(() => {
|
||||||
if (videoElement && videoElement.readyState >= 2 && videoElement.videoWidth > 0) {
|
playEmAndamento = false;
|
||||||
setTimeout(() => {
|
console.log('Vídeo iniciado, readyState:', videoElement?.readyState);
|
||||||
onLoadedMetadata();
|
// Se já tiver metadata e dimensões, resolver imediatamente
|
||||||
}, 300);
|
if (videoElement && videoElement.readyState >= 2 && videoElement.videoWidth > 0) {
|
||||||
}
|
setTimeout(() => {
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.warn('Erro ao reproduzir vídeo:', err);
|
|
||||||
// Continuar mesmo assim se já tiver metadata e dimensões
|
|
||||||
if (videoElement && videoElement.readyState >= 2 && videoElement.videoWidth > 0) {
|
|
||||||
setTimeout(() => {
|
|
||||||
onLoadedMetadata();
|
|
||||||
}, 300);
|
|
||||||
} else {
|
|
||||||
// Aguardar um pouco mais antes de dar erro
|
|
||||||
setTimeout(() => {
|
|
||||||
if (videoElement && videoElement.videoWidth > 0) {
|
|
||||||
onLoadedMetadata();
|
onLoadedMetadata();
|
||||||
} else {
|
}, 300);
|
||||||
onError();
|
}
|
||||||
}
|
})
|
||||||
}, 1000);
|
.catch((err) => {
|
||||||
}
|
playEmAndamento = false;
|
||||||
});
|
// Ignorar AbortError - é esperado quando há uma nova requisição de load
|
||||||
|
if (err.name !== 'AbortError') {
|
||||||
|
console.warn('Erro ao reproduzir vídeo:', err);
|
||||||
|
}
|
||||||
|
// Continuar mesmo assim se já tiver metadata e dimensões
|
||||||
|
if (videoElement && videoElement.readyState >= 2 && videoElement.videoWidth > 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
onLoadedMetadata();
|
||||||
|
}, 300);
|
||||||
|
} else {
|
||||||
|
// Aguardar um pouco mais antes de dar erro
|
||||||
|
setTimeout(() => {
|
||||||
|
if (videoElement && videoElement.videoWidth > 0) {
|
||||||
|
onLoadedMetadata();
|
||||||
|
} else {
|
||||||
|
onError();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Já está reproduzindo, apenas verificar se está pronto
|
||||||
|
if (videoElement.readyState >= 2 && videoElement.videoWidth > 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
onLoadedMetadata();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Vídeo pronto, dimensões:', videoElement.videoWidth, 'x', videoElement.videoHeight);
|
console.log('Vídeo pronto, dimensões:', videoElement.videoWidth, 'x', videoElement.videoHeight);
|
||||||
|
|||||||
@@ -171,6 +171,14 @@ export function aplicarTema(temaId: TemaId | string | null | undefined): void {
|
|||||||
// Forçar reflow para garantir que o CSS seja aplicado
|
// Forçar reflow para garantir que o CSS seja aplicado
|
||||||
void htmlElement.offsetHeight;
|
void htmlElement.offsetHeight;
|
||||||
|
|
||||||
|
// Forçar atualização de todas as variáveis CSS
|
||||||
|
// Isso garante que os temas customizados sejam aplicados corretamente
|
||||||
|
if (typeof window !== 'undefined' && window.getComputedStyle) {
|
||||||
|
const computedStyle = window.getComputedStyle(htmlElement);
|
||||||
|
// Forçar recálculo das variáveis CSS
|
||||||
|
computedStyle.getPropertyValue('--p');
|
||||||
|
}
|
||||||
|
|
||||||
// Disparar evento customizado para notificar mudança de tema
|
// Disparar evento customizado para notificar mudança de tema
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.dispatchEvent(new CustomEvent('themechange', { detail: { theme: nomeDaisyUI } }));
|
window.dispatchEvent(new CustomEvent('themechange', { detail: { theme: nomeDaisyUI } }));
|
||||||
|
|||||||
@@ -77,6 +77,34 @@
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
categoria: "Controle de Ponto",
|
||||||
|
descricao: "Gerencie registros de ponto dos funcionários",
|
||||||
|
Icon: Clock,
|
||||||
|
gradient: "from-cyan-500/10 to-cyan-600/20",
|
||||||
|
accentColor: "text-cyan-600",
|
||||||
|
bgIcon: "bg-cyan-500/20",
|
||||||
|
opcoes: [
|
||||||
|
{
|
||||||
|
nome: "Registro de Pontos",
|
||||||
|
descricao: "Visualizar e gerenciar registros de ponto",
|
||||||
|
href: "/recursos-humanos/registro-pontos",
|
||||||
|
Icon: Clock,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nome: "Homologação de Registro",
|
||||||
|
descricao: "Edite registros de ponto e ajuste banco de horas",
|
||||||
|
href: "/recursos-humanos/controle-ponto/homologacao",
|
||||||
|
Icon: CheckCircle2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nome: "Dispensa de Registro",
|
||||||
|
descricao: "Gerencie períodos de dispensa de registro de ponto",
|
||||||
|
href: "/recursos-humanos/controle-ponto/dispensa",
|
||||||
|
Icon: XCircle,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
categoria: "Gestão de Férias e Licenças",
|
categoria: "Gestão de Férias e Licenças",
|
||||||
descricao: "Controle de férias, atestados e licenças",
|
descricao: "Controle de férias, atestados e licenças",
|
||||||
@@ -121,34 +149,6 @@
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
categoria: "Controle de Ponto",
|
|
||||||
descricao: "Gerencie registros de ponto dos funcionários",
|
|
||||||
Icon: Clock,
|
|
||||||
gradient: "from-cyan-500/10 to-cyan-600/20",
|
|
||||||
accentColor: "text-cyan-600",
|
|
||||||
bgIcon: "bg-cyan-500/20",
|
|
||||||
opcoes: [
|
|
||||||
{
|
|
||||||
nome: "Registro de Pontos",
|
|
||||||
descricao: "Visualizar e gerenciar registros de ponto",
|
|
||||||
href: "/recursos-humanos/registro-pontos",
|
|
||||||
Icon: Clock,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nome: "Homologação de Registro",
|
|
||||||
descricao: "Edite registros de ponto e ajuste banco de horas",
|
|
||||||
href: "/recursos-humanos/controle-ponto/homologacao",
|
|
||||||
Icon: CheckCircle2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nome: "Dispensa de Registro",
|
|
||||||
descricao: "Gerencie períodos de dispensa de registro de ponto",
|
|
||||||
href: "/recursos-humanos/controle-ponto/dispensa",
|
|
||||||
Icon: XCircle,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,12 @@
|
|||||||
let homologacaoParaExcluir = $state<Id<'homologacoesPonto'> | null>(null);
|
let homologacaoParaExcluir = $state<Id<'homologacoesPonto'> | null>(null);
|
||||||
let mostrandoModalDetalhes = $state(false);
|
let mostrandoModalDetalhes = $state(false);
|
||||||
let mostrandoModalExcluir = $state(false);
|
let mostrandoModalExcluir = $state(false);
|
||||||
|
|
||||||
|
// Filtros de período
|
||||||
|
const hoje = new Date();
|
||||||
|
const trintaDiasAtras = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
||||||
|
let dataInicioFiltro = $state(trintaDiasAtras.toISOString().split('T')[0]!);
|
||||||
|
let dataFimFiltro = $state(hoje.toISOString().split('T')[0]!);
|
||||||
|
|
||||||
// Monitorar mudanças em funcionarioSelecionado
|
// Monitorar mudanças em funcionarioSelecionado
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -116,8 +122,8 @@
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
funcionarioId: funcionarioSelecionado as Id<'funcionarios'>,
|
funcionarioId: funcionarioSelecionado as Id<'funcionarios'>,
|
||||||
dataInicio: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]!,
|
dataInicio: dataInicioFiltro,
|
||||||
dataFim: new Date().toISOString().split('T')[0]!,
|
dataFim: dataFimFiltro,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -364,18 +370,49 @@
|
|||||||
<div class="card bg-base-100 shadow-xl mb-6">
|
<div class="card bg-base-100 shadow-xl mb-6">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title mb-4">Selecionar Funcionário</h2>
|
<h2 class="card-title mb-4">Selecionar Funcionário</h2>
|
||||||
<select
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
class="select select-bordered w-full"
|
<div class="form-control">
|
||||||
bind:value={funcionarioSelecionado}
|
<label class="label">
|
||||||
disabled={modoEdicao}
|
<span class="label-text font-medium">Funcionário</span>
|
||||||
>
|
</label>
|
||||||
<option value="">Selecione um funcionário</option>
|
<select
|
||||||
{#each funcionarios as funcionario}
|
class="select select-bordered w-full"
|
||||||
<option value={funcionario._id as string}>
|
bind:value={funcionarioSelecionado}
|
||||||
{funcionario.nome} {funcionario.matricula ? `(${funcionario.matricula})` : ''}
|
disabled={modoEdicao}
|
||||||
</option>
|
>
|
||||||
{/each}
|
<option value="">Selecione um funcionário</option>
|
||||||
</select>
|
{#each funcionarios as funcionario}
|
||||||
|
<option value={funcionario._id as string}>
|
||||||
|
{funcionario.nome} {funcionario.matricula ? `(${funcionario.matricula})` : ''}
|
||||||
|
</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">Data Início</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
bind:value={dataInicioFiltro}
|
||||||
|
disabled={modoEdicao}
|
||||||
|
max={dataFimFiltro}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">Data Fim</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
bind:value={dataFimFiltro}
|
||||||
|
disabled={modoEdicao}
|
||||||
|
min={dataInicioFiltro}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -791,6 +791,7 @@ export const registrarPonto = mutation({
|
|||||||
export const listarRegistrosDia = query({
|
export const listarRegistrosDia = query({
|
||||||
args: {
|
args: {
|
||||||
data: v.optional(v.string()), // YYYY-MM-DD, se não fornecido usa hoje
|
data: v.optional(v.string()), // YYYY-MM-DD, se não fornecido usa hoje
|
||||||
|
_refresh: v.optional(v.number()), // Parâmetro usado pelo frontend para forçar refresh
|
||||||
},
|
},
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
const usuario = await getCurrentUserFunction(ctx);
|
const usuario = await getCurrentUserFunction(ctx);
|
||||||
@@ -801,12 +802,22 @@ export const listarRegistrosDia = query({
|
|||||||
const funcionarioId = usuario.funcionarioId; // Garantir que não é undefined
|
const funcionarioId = usuario.funcionarioId; // Garantir que não é undefined
|
||||||
const data = args.data || new Date().toISOString().split('T')[0]!;
|
const data = args.data || new Date().toISOString().split('T')[0]!;
|
||||||
|
|
||||||
|
console.log('[listarRegistrosDia] Buscando registros:', { funcionarioId, data });
|
||||||
|
|
||||||
const registros = await ctx.db
|
const registros = await ctx.db
|
||||||
.query('registrosPonto')
|
.query('registrosPonto')
|
||||||
.withIndex('by_funcionario_data', (q) => q.eq('funcionarioId', funcionarioId).eq('data', data))
|
.withIndex('by_funcionario_data', (q) => q.eq('funcionarioId', funcionarioId).eq('data', data))
|
||||||
.order('asc')
|
.order('asc')
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
console.log('[listarRegistrosDia] Registros encontrados:', registros.length, registros.map(r => ({
|
||||||
|
_id: r._id,
|
||||||
|
tipo: r.tipo,
|
||||||
|
data: r.data,
|
||||||
|
hora: r.hora,
|
||||||
|
minuto: r.minuto
|
||||||
|
})));
|
||||||
|
|
||||||
return registros;
|
return registros;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -862,7 +873,7 @@ export const listarRegistrosPeriodo = query({
|
|||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
const usuario = await getCurrentUserFunction(ctx);
|
const usuario = await getCurrentUserFunction(ctx);
|
||||||
if (!usuario) {
|
if (!usuario) {
|
||||||
// Retornar array vazio quando não autenticado
|
console.warn('[listarRegistrosPeriodo] Usuário não autenticado');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -872,15 +883,27 @@ export const listarRegistrosPeriodo = query({
|
|||||||
|
|
||||||
// Validar formato das datas
|
// Validar formato das datas
|
||||||
if (!args.dataInicio || !args.dataFim) {
|
if (!args.dataInicio || !args.dataFim) {
|
||||||
|
console.warn('[listarRegistrosPeriodo] Datas não fornecidas');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validar formato YYYY-MM-DD
|
// Validar formato YYYY-MM-DD
|
||||||
const dataInicioRegex = /^\d{4}-\d{2}-\d{2}$/;
|
const dataInicioRegex = /^\d{4}-\d{2}-\d{2}$/;
|
||||||
if (!dataInicioRegex.test(args.dataInicio) || !dataInicioRegex.test(args.dataFim)) {
|
if (!dataInicioRegex.test(args.dataInicio) || !dataInicioRegex.test(args.dataFim)) {
|
||||||
|
console.warn('[listarRegistrosPeriodo] Formato de data inválido', {
|
||||||
|
dataInicio: args.dataInicio,
|
||||||
|
dataFim: args.dataFim
|
||||||
|
});
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[listarRegistrosPeriodo] Buscando registros', {
|
||||||
|
dataInicio: args.dataInicio,
|
||||||
|
dataFim: args.dataFim,
|
||||||
|
funcionarioId: args.funcionarioId,
|
||||||
|
usuarioId: usuario._id
|
||||||
|
});
|
||||||
|
|
||||||
let registrosFiltrados;
|
let registrosFiltrados;
|
||||||
|
|
||||||
// Se funcionário foi especificado, usar índice por funcionário e data (mais eficiente)
|
// Se funcionário foi especificado, usar índice por funcionário e data (mais eficiente)
|
||||||
@@ -901,21 +924,41 @@ export const listarRegistrosPeriodo = query({
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Se não há funcionário especificado, buscar todos e filtrar (menos eficiente, mas necessário)
|
// Se não há funcionário especificado, buscar todos e filtrar (menos eficiente, mas necessário)
|
||||||
// Usar comparação de strings diretamente para datas no formato YYYY-MM-DD
|
try {
|
||||||
const registros = await ctx.db
|
// Tentar usar índice por data primeiro
|
||||||
.query('registrosPonto')
|
const registros = await ctx.db
|
||||||
.withIndex('by_data', (q) =>
|
.query('registrosPonto')
|
||||||
q.gte('data', args.dataInicio).lte('data', args.dataFim)
|
.withIndex('by_data', (q) =>
|
||||||
)
|
q.gte('data', args.dataInicio).lte('data', args.dataFim)
|
||||||
.collect();
|
)
|
||||||
|
.collect();
|
||||||
// Garantir que as datas estão no formato correto e filtrar novamente para garantir
|
|
||||||
registrosFiltrados = registros.filter((r) => {
|
console.log('[listarRegistrosPeriodo] Registros do índice by_data:', registros.length);
|
||||||
// Comparação de strings funciona para formato YYYY-MM-DD
|
|
||||||
return r.data >= args.dataInicio && r.data <= args.dataFim;
|
// Garantir que as datas estão no formato correto e filtrar novamente para garantir
|
||||||
});
|
registrosFiltrados = registros.filter((r) => {
|
||||||
|
// Comparação de strings funciona para formato YYYY-MM-DD
|
||||||
|
return r.data >= args.dataInicio && r.data <= args.dataFim;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[listarRegistrosPeriodo] Registros após filtro:', registrosFiltrados.length);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[listarRegistrosPeriodo] Erro ao buscar registros:', error);
|
||||||
|
// Fallback: buscar todos e filtrar manualmente
|
||||||
|
const todosRegistros = await ctx.db
|
||||||
|
.query('registrosPonto')
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
registrosFiltrados = todosRegistros.filter((r) => {
|
||||||
|
return r.data >= args.dataInicio && r.data <= args.dataFim;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[listarRegistrosPeriodo] Fallback - registros encontrados:', registrosFiltrados.length);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[listarRegistrosPeriodo] Registros encontrados antes de buscar funcionários:', registrosFiltrados.length);
|
||||||
|
|
||||||
// Buscar informações dos funcionários
|
// Buscar informações dos funcionários
|
||||||
const funcionariosIds = new Set(registrosFiltrados.map((r) => r.funcionarioId));
|
const funcionariosIds = new Set(registrosFiltrados.map((r) => r.funcionarioId));
|
||||||
const funcionarios = await Promise.all(
|
const funcionarios = await Promise.all(
|
||||||
@@ -940,6 +983,8 @@ export const listarRegistrosPeriodo = query({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[listarRegistrosPeriodo] Total de registros a retornar:', registrosFiltrados.length);
|
||||||
|
|
||||||
return registrosFiltrados.map((registro) => {
|
return registrosFiltrados.map((registro) => {
|
||||||
const funcionario = funcionarios.find((f) => f?._id === registro.funcionarioId);
|
const funcionario = funcionarios.find((f) => f?._id === registro.funcionarioId);
|
||||||
const chave = `${registro.funcionarioId}-${registro.data}`;
|
const chave = `${registro.funcionarioId}-${registro.data}`;
|
||||||
@@ -1225,11 +1270,19 @@ export const obterHistoricoESaldoDia = query({
|
|||||||
args: {
|
args: {
|
||||||
funcionarioId: v.id('funcionarios'),
|
funcionarioId: v.id('funcionarios'),
|
||||||
data: v.string(), // YYYY-MM-DD
|
data: v.string(), // YYYY-MM-DD
|
||||||
|
_refresh: v.optional(v.number()), // Parâmetro usado pelo frontend para forçar refresh
|
||||||
},
|
},
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
const usuario = await getCurrentUserFunction(ctx);
|
const usuario = await getCurrentUserFunction(ctx);
|
||||||
if (!usuario || !usuario.funcionarioId) {
|
if (!usuario || !usuario.funcionarioId) {
|
||||||
throw new Error('Usuário não autenticado');
|
console.warn('[obterHistoricoESaldoDia] Usuário não autenticado ou sem funcionarioId');
|
||||||
|
// Retornar dados vazios em vez de lançar erro
|
||||||
|
return {
|
||||||
|
registros: [],
|
||||||
|
cargaHorariaDiaria: 0,
|
||||||
|
horasTrabalhadas: 0,
|
||||||
|
saldoMinutos: 0,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verificar se é o próprio funcionário ou tem permissão
|
// Verificar se é o próprio funcionário ou tem permissão
|
||||||
@@ -1245,6 +1298,11 @@ export const obterHistoricoESaldoDia = query({
|
|||||||
)
|
)
|
||||||
.order('asc')
|
.order('asc')
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
console.log('[obterHistoricoESaldoDia] Registros encontrados:', registros.length, {
|
||||||
|
funcionarioId: args.funcionarioId,
|
||||||
|
data: args.data
|
||||||
|
});
|
||||||
|
|
||||||
// Buscar configuração de ponto
|
// Buscar configuração de ponto
|
||||||
const config = await ctx.db
|
const config = await ctx.db
|
||||||
|
|||||||
Reference in New Issue
Block a user