Call audio video jitsi #42
@@ -11,6 +11,103 @@
|
|||||||
|
|
||||||
let { open = $bindable(false), title = 'Erro', message, details, onClose }: Props = $props();
|
let { open = $bindable(false), title = 'Erro', message, details, onClose }: Props = $props();
|
||||||
|
|
||||||
|
let modalPosition = $state<{ top: number; left: number } | null>(null);
|
||||||
|
|
||||||
|
// Função para calcular a posição baseada no relógio sincronizado
|
||||||
|
function calcularPosicaoModal() {
|
||||||
|
// Procurar pelo elemento do relógio sincronizado
|
||||||
|
const relogioRef = document.getElementById('relogio-sincronizado-ref');
|
||||||
|
|
||||||
|
if (relogioRef) {
|
||||||
|
const rect = relogioRef.getBoundingClientRect();
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
|
||||||
|
// Posicionar o modal na mesma posição do relógio sincronizado
|
||||||
|
// Centralizado horizontalmente no card do relógio
|
||||||
|
const left = rect.left + (rect.width / 2);
|
||||||
|
// Posicionar abaixo do card do relógio com um pequeno espaçamento
|
||||||
|
const top = rect.bottom + 20;
|
||||||
|
|
||||||
|
return {
|
||||||
|
top: top,
|
||||||
|
left: left
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se não encontrar, usar posição padrão (centro da tela)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (open) {
|
||||||
|
// Usar requestAnimationFrame para garantir que o DOM está completamente renderizado
|
||||||
|
const updatePosition = () => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const pos = calcularPosicaoModal();
|
||||||
|
if (pos) {
|
||||||
|
modalPosition = pos;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Aguardar um pouco mais para garantir que o DOM está atualizado
|
||||||
|
setTimeout(updatePosition, 50);
|
||||||
|
|
||||||
|
// Adicionar listener de scroll para atualizar posição
|
||||||
|
const handleScroll = () => {
|
||||||
|
updatePosition();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('scroll', handleScroll, true);
|
||||||
|
window.addEventListener('resize', handleScroll);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('scroll', handleScroll, true);
|
||||||
|
window.removeEventListener('resize', handleScroll);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Limpar posição quando o modal for fechado
|
||||||
|
modalPosition = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Função para obter estilo do modal baseado na posição calculada
|
||||||
|
function getModalStyle() {
|
||||||
|
if (modalPosition) {
|
||||||
|
// Garantir que o modal não saia da viewport
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
const modalWidth = 700; // Aproximadamente max-w-2xl
|
||||||
|
const modalHeight = Math.min(viewportHeight * 0.9, 600);
|
||||||
|
|
||||||
|
let left = modalPosition.left;
|
||||||
|
let top = modalPosition.top;
|
||||||
|
|
||||||
|
// Ajustar se o modal sair da viewport à direita
|
||||||
|
if (left + (modalWidth / 2) > viewportWidth - 20) {
|
||||||
|
left = viewportWidth - (modalWidth / 2) - 20;
|
||||||
|
}
|
||||||
|
// Ajustar se o modal sair da viewport à esquerda
|
||||||
|
if (left - (modalWidth / 2) < 20) {
|
||||||
|
left = (modalWidth / 2) + 20;
|
||||||
|
}
|
||||||
|
// Ajustar se o modal sair da viewport abaixo
|
||||||
|
if (top + modalHeight > viewportHeight - 20) {
|
||||||
|
top = viewportHeight - modalHeight - 20;
|
||||||
|
}
|
||||||
|
// Ajustar se o modal sair da viewport acima
|
||||||
|
if (top < 20) {
|
||||||
|
top = 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usar transform para centralizar horizontalmente baseado no left calculado
|
||||||
|
return `position: fixed; top: ${top}px; left: ${left}px; transform: translateX(-50%); max-width: ${Math.min(modalWidth, viewportWidth - 40)}px;`;
|
||||||
|
}
|
||||||
|
// Se não houver posição calculada, centralizar na tela
|
||||||
|
return 'position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);';
|
||||||
|
}
|
||||||
|
|
||||||
// Verificar se details contém instruções ou apenas detalhes técnicos
|
// Verificar se details contém instruções ou apenas detalhes técnicos
|
||||||
const temInstrucoes = $derived.by(() => {
|
const temInstrucoes = $derived.by(() => {
|
||||||
if (!details) return false;
|
if (!details) return false;
|
||||||
@@ -31,22 +128,22 @@
|
|||||||
|
|
||||||
{#if open}
|
{#if open}
|
||||||
<div
|
<div
|
||||||
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
class="fixed inset-0 z-50 pointer-events-none"
|
||||||
style="animation: fadeIn 0.2s ease-out;"
|
style="animation: fadeIn 0.2s ease-out;"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="modal-error-title"
|
aria-labelledby="modal-error-title"
|
||||||
>
|
>
|
||||||
<!-- Backdrop -->
|
<!-- Backdrop leve -->
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-black/50 backdrop-blur-sm transition-opacity duration-200"
|
class="absolute inset-0 bg-black/20 transition-opacity duration-200 pointer-events-auto"
|
||||||
onclick={handleClose}
|
onclick={handleClose}
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<!-- Modal Box -->
|
<!-- Modal Box -->
|
||||||
<div
|
<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"
|
class="absolute 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 pointer-events-auto"
|
||||||
style="animation: slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);"
|
style="animation: slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); {getModalStyle()}"
|
||||||
onclick={(e) => e.stopPropagation()}
|
onclick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<!-- Header fixo -->
|
<!-- Header fixo -->
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
import { useQuery } from 'convex-svelte';
|
import { useQuery } from 'convex-svelte';
|
||||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
import jsPDF from 'jspdf';
|
import jsPDF from 'jspdf';
|
||||||
import { Printer, X } from 'lucide-svelte';
|
import { Printer, X, User, Clock, CheckCircle2, XCircle, Calendar, MapPin } from 'lucide-svelte';
|
||||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
import { formatarDataHoraCompleta, getTipoRegistroLabel } from '$lib/utils/ponto';
|
import { formatarDataHoraCompleta, getTipoRegistroLabel } from '$lib/utils/ponto';
|
||||||
import logoGovPE from '$lib/assets/logo_governo_PE.png';
|
import logoGovPE from '$lib/assets/logo_governo_PE.png';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
registroId: Id<'registrosPonto'>;
|
registroId: Id<'registrosPonto'>;
|
||||||
@@ -18,6 +19,97 @@
|
|||||||
const configQuery = useQuery(api.configuracaoPonto.obterConfiguracao, {});
|
const configQuery = useQuery(api.configuracaoPonto.obterConfiguracao, {});
|
||||||
|
|
||||||
let gerando = $state(false);
|
let gerando = $state(false);
|
||||||
|
let modalPosition = $state<{ top: number; left: number } | null>(null);
|
||||||
|
|
||||||
|
// Função para calcular a posição baseada no relógio sincronizado
|
||||||
|
function calcularPosicaoModal() {
|
||||||
|
// Procurar pelo elemento do relógio sincronizado
|
||||||
|
const relogioRef = document.getElementById('relogio-sincronizado-ref');
|
||||||
|
|
||||||
|
if (relogioRef) {
|
||||||
|
const rect = relogioRef.getBoundingClientRect();
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
|
||||||
|
// Posicionar o modal na mesma posição do relógio sincronizado
|
||||||
|
// Centralizado horizontalmente no card do relógio
|
||||||
|
const left = rect.left + (rect.width / 2);
|
||||||
|
// Posicionar abaixo do card do relógio com um pequeno espaçamento
|
||||||
|
const top = rect.bottom + 20;
|
||||||
|
|
||||||
|
return {
|
||||||
|
top: top,
|
||||||
|
left: left
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se não encontrar, usar posição padrão (centro da tela)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
// Usar requestAnimationFrame para garantir que o DOM está completamente renderizado
|
||||||
|
const updatePosition = () => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const pos = calcularPosicaoModal();
|
||||||
|
if (pos) {
|
||||||
|
modalPosition = pos;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Aguardar um pouco mais para garantir que o DOM está atualizado
|
||||||
|
setTimeout(updatePosition, 50);
|
||||||
|
|
||||||
|
// Adicionar listener de scroll para atualizar posição
|
||||||
|
const handleScroll = () => {
|
||||||
|
updatePosition();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('scroll', handleScroll, true);
|
||||||
|
window.addEventListener('resize', handleScroll);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('scroll', handleScroll, true);
|
||||||
|
window.removeEventListener('resize', handleScroll);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Função para obter estilo do modal baseado na posição calculada
|
||||||
|
function getModalStyle() {
|
||||||
|
if (modalPosition) {
|
||||||
|
// Garantir que o modal não saia da viewport
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
const modalWidth = 700; // Aproximadamente max-w-2xl
|
||||||
|
const modalHeight = Math.min(viewportHeight * 0.9, 600);
|
||||||
|
|
||||||
|
let left = modalPosition.left;
|
||||||
|
let top = modalPosition.top;
|
||||||
|
|
||||||
|
// Ajustar se o modal sair da viewport à direita
|
||||||
|
if (left + (modalWidth / 2) > viewportWidth - 20) {
|
||||||
|
left = viewportWidth - (modalWidth / 2) - 20;
|
||||||
|
}
|
||||||
|
// Ajustar se o modal sair da viewport à esquerda
|
||||||
|
if (left - (modalWidth / 2) < 20) {
|
||||||
|
left = (modalWidth / 2) + 20;
|
||||||
|
}
|
||||||
|
// Ajustar se o modal sair da viewport abaixo
|
||||||
|
if (top + modalHeight > viewportHeight - 20) {
|
||||||
|
top = viewportHeight - modalHeight - 20;
|
||||||
|
}
|
||||||
|
// Ajustar se o modal sair da viewport acima
|
||||||
|
if (top < 20) {
|
||||||
|
top = 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usar transform para centralizar horizontalmente baseado no left calculado
|
||||||
|
return `position: fixed; top: ${top}px; left: ${left}px; transform: translateX(-50%); max-width: ${Math.min(modalWidth, viewportWidth - 40)}px;`;
|
||||||
|
}
|
||||||
|
// Se não houver posição calculada, centralizar na tela
|
||||||
|
return 'position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);';
|
||||||
|
}
|
||||||
|
|
||||||
async function gerarPDF() {
|
async function gerarPDF() {
|
||||||
if (!registroQuery?.data) return;
|
if (!registroQuery?.data) return;
|
||||||
@@ -238,99 +330,178 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
class="fixed inset-0 z-50 pointer-events-none"
|
||||||
style="animation: fadeIn 0.2s ease-out;"
|
style="animation: fadeIn 0.2s ease-out;"
|
||||||
onkeydown={(e) => e.key === 'Escape' && onClose()}
|
onkeydown={(e) => e.key === 'Escape' && onClose()}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="modal-comprovante-title"
|
aria-labelledby="modal-comprovante-title"
|
||||||
>
|
>
|
||||||
<!-- Backdrop com blur -->
|
<!-- Backdrop leve -->
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-black/50 backdrop-blur-sm transition-opacity duration-200"
|
class="absolute inset-0 bg-black/20 transition-opacity duration-200 pointer-events-auto"
|
||||||
onclick={onClose}
|
onclick={onClose}
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<!-- Modal Box -->
|
<!-- Modal Box -->
|
||||||
<div
|
<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"
|
class="absolute bg-gradient-to-br from-base-100 via-base-100 to-primary/5 rounded-2xl shadow-2xl border-2 border-primary/20 max-w-2xl w-full max-h-[90vh] overflow-hidden flex flex-col z-10 transform transition-all duration-300 pointer-events-auto"
|
||||||
style="animation: slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);"
|
style="animation: slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); {getModalStyle()}"
|
||||||
onclick={(e) => e.stopPropagation()}
|
onclick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<!-- Header fixo -->
|
<!-- Header Premium com gradiente -->
|
||||||
<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 px-6 py-5 bg-gradient-to-r from-primary/10 via-primary/5 to-transparent border-b-2 border-primary/20 flex-shrink-0">
|
||||||
<h3 id="modal-comprovante-title" class="font-bold text-lg">Comprovante de Registro de Ponto</h3>
|
<div class="flex items-center gap-3">
|
||||||
<button class="btn btn-sm btn-circle btn-ghost hover:bg-base-300" onclick={onClose}>
|
<div class="p-2.5 bg-primary/20 rounded-xl shadow-lg">
|
||||||
|
<Clock class="h-6 w-6 text-primary" strokeWidth={2.5} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 id="modal-comprovante-title" class="font-bold text-xl text-base-content">Comprovante de Registro de Ponto</h3>
|
||||||
|
<p class="text-sm text-base-content/70 mt-0.5">Detalhes do registro realizado</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-circle btn-ghost hover:bg-base-300 transition-all" 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 modal-scroll">
|
<div class="flex-1 overflow-y-auto px-6 py-6 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-12">
|
||||||
<span class="loading loading-spinner loading-lg"></span>
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||||
</div>
|
</div>
|
||||||
{:else if !registroQuery?.data}
|
{:else if !registroQuery?.data}
|
||||||
<div class="alert alert-error">
|
<div class="alert alert-error shadow-lg">
|
||||||
<span>Erro ao carregar registro</span>
|
<XCircle class="h-5 w-5" />
|
||||||
|
<span class="font-semibold">Erro ao carregar registro</span>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{@const registro = registroQuery.data}
|
{@const registro = registroQuery.data}
|
||||||
<div class="space-y-4">
|
<div class="space-y-6">
|
||||||
<!-- Informações do Funcionário -->
|
<!-- Informações do Funcionário -->
|
||||||
<div class="card bg-base-200">
|
<div class="card bg-gradient-to-br from-base-100 to-base-200 shadow-lg border-2 border-primary/10 hover:shadow-xl transition-all">
|
||||||
<div class="card-body">
|
<div class="card-body p-6">
|
||||||
<h4 class="font-bold">Dados do Funcionário</h4>
|
<div class="flex items-center gap-3 mb-4">
|
||||||
|
<div class="p-2 bg-primary/10 rounded-lg">
|
||||||
|
<User class="h-5 w-5 text-primary" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
<h4 class="font-bold text-lg text-base-content">Dados do Funcionário</h4>
|
||||||
|
</div>
|
||||||
{#if registro.funcionario}
|
{#if registro.funcionario}
|
||||||
<p><strong>Matrícula:</strong> {registro.funcionario.matricula || 'N/A'}</p>
|
<div class="space-y-3">
|
||||||
<p><strong>Nome:</strong> {registro.funcionario.nome}</p>
|
{#if registro.funcionario.matricula}
|
||||||
{#if registro.funcionario.descricaoCargo}
|
<div class="flex items-start gap-3 p-3 bg-base-100 rounded-lg border border-base-300">
|
||||||
<p><strong>Cargo/Função:</strong> {registro.funcionario.descricaoCargo}</p>
|
<div class="flex-1">
|
||||||
{/if}
|
<span class="text-xs font-semibold text-base-content/60 uppercase tracking-wide">Matrícula</span>
|
||||||
|
<p class="text-base font-semibold text-base-content mt-1">{registro.funcionario.matricula}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="flex items-start gap-3 p-3 bg-base-100 rounded-lg border border-base-300">
|
||||||
|
<div class="flex-1">
|
||||||
|
<span class="text-xs font-semibold text-base-content/60 uppercase tracking-wide">Nome</span>
|
||||||
|
<p class="text-base font-semibold text-base-content mt-1">{registro.funcionario.nome}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if registro.funcionario.descricaoCargo}
|
||||||
|
<div class="flex items-start gap-3 p-3 bg-base-100 rounded-lg border border-base-300">
|
||||||
|
<div class="flex-1">
|
||||||
|
<span class="text-xs font-semibold text-base-content/60 uppercase tracking-wide">Cargo/Função</span>
|
||||||
|
<p class="text-base font-semibold text-base-content mt-1">{registro.funcionario.descricaoCargo}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Informações do Registro -->
|
<!-- Informações do Registro -->
|
||||||
<div class="card bg-base-200">
|
<div class="card bg-gradient-to-br from-primary/5 to-primary/10 shadow-lg border-2 border-primary/20 hover:shadow-xl transition-all">
|
||||||
<div class="card-body">
|
<div class="card-body p-6">
|
||||||
<h4 class="font-bold">Dados do Registro</h4>
|
<div class="flex items-center gap-3 mb-4">
|
||||||
<p>
|
<div class="p-2 bg-primary/20 rounded-lg">
|
||||||
<strong>Tipo:</strong>
|
<Clock class="h-5 w-5 text-primary" strokeWidth={2} />
|
||||||
{configQuery?.data
|
</div>
|
||||||
? getTipoRegistroLabel(registro.tipo, {
|
<h4 class="font-bold text-lg text-base-content">Dados do Registro</h4>
|
||||||
nomeEntrada: configQuery.data.nomeEntrada,
|
</div>
|
||||||
nomeSaidaAlmoco: configQuery.data.nomeSaidaAlmoco,
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
nomeRetornoAlmoco: configQuery.data.nomeRetornoAlmoco,
|
<!-- Tipo -->
|
||||||
nomeSaida: configQuery.data.nomeSaida,
|
<div class="bg-base-100 rounded-lg p-4 border border-base-300 shadow-sm">
|
||||||
})
|
<span class="text-xs font-semibold text-base-content/60 uppercase tracking-wide">Tipo</span>
|
||||||
: getTipoRegistroLabel(registro.tipo)}
|
<p class="text-lg font-bold text-primary mt-1">
|
||||||
</p>
|
{configQuery?.data
|
||||||
<p>
|
? getTipoRegistroLabel(registro.tipo, {
|
||||||
<strong>Data e Hora:</strong>
|
nomeEntrada: configQuery.data.nomeEntrada,
|
||||||
{formatarDataHoraCompleta(registro.data, registro.hora, registro.minuto, registro.segundo)}
|
nomeSaidaAlmoco: configQuery.data.nomeSaidaAlmoco,
|
||||||
</p>
|
nomeRetornoAlmoco: configQuery.data.nomeRetornoAlmoco,
|
||||||
<p>
|
nomeSaida: configQuery.data.nomeSaida,
|
||||||
<strong>Status:</strong>
|
})
|
||||||
<span class="badge {registro.dentroDoPrazo ? 'badge-success' : 'badge-error'}">
|
: getTipoRegistroLabel(registro.tipo)}
|
||||||
{registro.dentroDoPrazo ? 'Dentro do Prazo' : 'Fora do Prazo'}
|
</p>
|
||||||
</span>
|
</div>
|
||||||
</p>
|
|
||||||
<p><strong>Tolerância:</strong> {registro.toleranciaMinutos} minutos</p>
|
<!-- Data e Hora -->
|
||||||
|
<div class="bg-base-100 rounded-lg p-4 border border-base-300 shadow-sm">
|
||||||
|
<span class="text-xs font-semibold text-base-content/60 uppercase tracking-wide">Data e Hora</span>
|
||||||
|
<p class="text-lg font-bold text-base-content mt-1">
|
||||||
|
{formatarDataHoraCompleta(registro.data, registro.hora, registro.minuto, registro.segundo)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status -->
|
||||||
|
<div class="bg-base-100 rounded-lg p-4 border border-base-300 shadow-sm">
|
||||||
|
<span class="text-xs font-semibold text-base-content/60 uppercase tracking-wide">Status</span>
|
||||||
|
<div class="mt-2">
|
||||||
|
<span class="badge badge-lg gap-2 {registro.dentroDoPrazo ? 'badge-success' : 'badge-error'}">
|
||||||
|
{#if registro.dentroDoPrazo}
|
||||||
|
<CheckCircle2 class="h-4 w-4" />
|
||||||
|
{:else}
|
||||||
|
<XCircle class="h-4 w-4" />
|
||||||
|
{/if}
|
||||||
|
{registro.dentroDoPrazo ? 'Dentro do Prazo' : 'Fora do Prazo'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tolerância -->
|
||||||
|
<div class="bg-base-100 rounded-lg p-4 border border-base-300 shadow-sm">
|
||||||
|
<span class="text-xs font-semibold text-base-content/60 uppercase tracking-wide">Tolerância</span>
|
||||||
|
<p class="text-lg font-bold text-base-content mt-1">{registro.toleranciaMinutos} minutos</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Imagem Capturada -->
|
<!-- Imagem Capturada -->
|
||||||
{#if registro.imagemUrl}
|
{#if registro.imagemUrl}
|
||||||
<div class="card bg-base-200">
|
<div class="card bg-gradient-to-br from-base-100 to-base-200 shadow-lg border-2 border-primary/10 hover:shadow-xl transition-all">
|
||||||
<div class="card-body">
|
<div class="card-body p-6">
|
||||||
<h4 class="font-bold mb-2">Foto Capturada</h4>
|
<div class="flex items-center gap-3 mb-4">
|
||||||
<div class="flex justify-center">
|
<div class="p-2 bg-primary/10 rounded-lg">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-5 w-5 text-primary"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h4 class="font-bold text-lg text-base-content">Foto Capturada</h4>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center bg-base-100 rounded-xl p-4 border-2 border-primary/20">
|
||||||
<img
|
<img
|
||||||
src={registro.imagemUrl}
|
src={registro.imagemUrl}
|
||||||
alt="Foto do registro de ponto"
|
alt="Foto do registro de ponto"
|
||||||
class="max-w-full max-h-[250px] rounded-lg border-2 border-primary object-contain"
|
class="max-w-full max-h-[300px] rounded-lg shadow-md object-contain"
|
||||||
onerror={(e) => {
|
onerror={(e) => {
|
||||||
console.error('Erro ao carregar imagem:', e);
|
console.error('Erro ao carregar imagem:', e);
|
||||||
(e.target as HTMLImageElement).style.display = 'none';
|
(e.target as HTMLImageElement).style.display = 'none';
|
||||||
@@ -345,16 +516,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer fixo com botões -->
|
<!-- Footer fixo com botões -->
|
||||||
<div class="flex justify-end gap-2 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-2 border-primary/20 bg-base-100/50 backdrop-blur-sm flex-shrink-0">
|
||||||
<button class="btn btn-primary gap-2" onclick={gerarPDF} disabled={gerando}>
|
<button class="btn btn-outline gap-2" onclick={onClose}>
|
||||||
|
<X class="h-4 w-4" />
|
||||||
|
Fechar
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary gap-2 shadow-lg hover:shadow-xl transition-all" onclick={gerarPDF} disabled={gerando}>
|
||||||
{#if gerando}
|
{#if gerando}
|
||||||
<span class="loading loading-spinner loading-sm"></span>
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
Gerando...
|
||||||
{:else}
|
{:else}
|
||||||
<Printer class="h-5 w-5" />
|
<Printer class="h-5 w-5" />
|
||||||
|
Imprimir Comprovante
|
||||||
{/if}
|
{/if}
|
||||||
Imprimir Comprovante
|
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-outline" onclick={onClose}>Fechar</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -851,6 +851,105 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const saldoPositivo = $derived(historicoSaldo ? historicoSaldo.saldoMinutos >= 0 : false);
|
const saldoPositivo = $derived(historicoSaldo ? historicoSaldo.saldoMinutos >= 0 : false);
|
||||||
|
|
||||||
|
// Posicionamento dos modais
|
||||||
|
let modalPosition = $state<{ top: number; left: number } | null>(null);
|
||||||
|
|
||||||
|
// Função para calcular a posição baseada no relógio sincronizado
|
||||||
|
function calcularPosicaoModal() {
|
||||||
|
// Procurar pelo elemento do relógio sincronizado
|
||||||
|
const relogioRef = document.getElementById('relogio-sincronizado-ref');
|
||||||
|
|
||||||
|
if (relogioRef) {
|
||||||
|
const rect = relogioRef.getBoundingClientRect();
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
|
||||||
|
// Posicionar o modal na mesma posição do relógio sincronizado
|
||||||
|
// Centralizado horizontalmente no card do relógio
|
||||||
|
const left = rect.left + (rect.width / 2);
|
||||||
|
// Posicionar abaixo do card do relógio com um pequeno espaçamento
|
||||||
|
const top = rect.bottom + 20;
|
||||||
|
|
||||||
|
return {
|
||||||
|
top: top,
|
||||||
|
left: left
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se não encontrar, usar posição padrão (centro da tela)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atualizar posição quando os modais forem abertos ou quando a página rolar
|
||||||
|
$effect(() => {
|
||||||
|
if (mostrandoWebcam || mostrandoModalConfirmacao || aguardandoProcessamento || mostrarModalErro) {
|
||||||
|
// Usar requestAnimationFrame para garantir que o DOM está completamente renderizado
|
||||||
|
const updatePosition = () => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const pos = calcularPosicaoModal();
|
||||||
|
if (pos) {
|
||||||
|
modalPosition = pos;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Aguardar um pouco mais para garantir que o DOM está atualizado
|
||||||
|
setTimeout(updatePosition, 50);
|
||||||
|
|
||||||
|
// Adicionar listener de scroll para atualizar posição
|
||||||
|
const handleScroll = () => {
|
||||||
|
updatePosition();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('scroll', handleScroll, true);
|
||||||
|
window.addEventListener('resize', handleScroll);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('scroll', handleScroll, true);
|
||||||
|
window.removeEventListener('resize', handleScroll);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Limpar posição quando os modais forem fechados
|
||||||
|
modalPosition = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Função para obter estilo do modal baseado na posição calculada
|
||||||
|
function getModalStyle() {
|
||||||
|
if (modalPosition) {
|
||||||
|
// Garantir que o modal não saia da viewport
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
const modalWidth = 800; // Aproximadamente max-w-2xl ou max-w-3xl
|
||||||
|
const modalHeight = Math.min(viewportHeight * 0.9, 600);
|
||||||
|
|
||||||
|
let left = modalPosition.left;
|
||||||
|
let top = modalPosition.top;
|
||||||
|
|
||||||
|
// Ajustar se o modal sair da viewport à direita
|
||||||
|
if (left + (modalWidth / 2) > viewportWidth - 20) {
|
||||||
|
left = viewportWidth - (modalWidth / 2) - 20;
|
||||||
|
}
|
||||||
|
// Ajustar se o modal sair da viewport à esquerda
|
||||||
|
if (left - (modalWidth / 2) < 20) {
|
||||||
|
left = (modalWidth / 2) + 20;
|
||||||
|
}
|
||||||
|
// Ajustar se o modal sair da viewport abaixo
|
||||||
|
if (top + modalHeight > viewportHeight - 20) {
|
||||||
|
top = viewportHeight - modalHeight - 20;
|
||||||
|
}
|
||||||
|
// Ajustar se o modal sair da viewport acima
|
||||||
|
if (top < 20) {
|
||||||
|
top = 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usar transform para centralizar horizontalmente baseado no left calculado
|
||||||
|
return `position: fixed; top: ${top}px; left: ${left}px; transform: translateX(-50%); max-width: ${Math.min(modalWidth, viewportWidth - 40)}px;`;
|
||||||
|
}
|
||||||
|
// Se não houver posição calculada, centralizar na tela
|
||||||
|
return 'position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);';
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
@@ -921,7 +1020,7 @@
|
|||||||
|
|
||||||
<!-- Relógio Sincronizado -->
|
<!-- Relógio Sincronizado -->
|
||||||
<div class="mb-5 flex justify-center">
|
<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">
|
<div id="relogio-sincronizado-ref" 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>
|
</div>
|
||||||
@@ -1330,22 +1429,22 @@
|
|||||||
<!-- Modal Webcam -->
|
<!-- Modal Webcam -->
|
||||||
{#if mostrandoWebcam}
|
{#if mostrandoWebcam}
|
||||||
<div
|
<div
|
||||||
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
class="fixed inset-0 z-50 pointer-events-none"
|
||||||
style="animation: fadeIn 0.2s ease-out;"
|
style="animation: fadeIn 0.2s ease-out;"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="modal-webcam-title"
|
aria-labelledby="modal-webcam-title"
|
||||||
>
|
>
|
||||||
<!-- Backdrop -->
|
<!-- Backdrop leve -->
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-black/50 backdrop-blur-sm transition-opacity duration-200"
|
class="absolute inset-0 bg-black/20 transition-opacity duration-200 pointer-events-auto"
|
||||||
onclick={handleWebcamCancel}
|
onclick={handleWebcamCancel}
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<!-- Modal Box -->
|
<!-- Modal Box -->
|
||||||
<div
|
<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"
|
class="absolute 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 pointer-events-auto"
|
||||||
style="animation: slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);"
|
style="animation: slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); {getModalStyle()}"
|
||||||
onclick={(e) => e.stopPropagation()}
|
onclick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<!-- Header fixo -->
|
<!-- Header fixo -->
|
||||||
@@ -1383,19 +1482,19 @@
|
|||||||
<!-- Modal de Aguardando Processamento -->
|
<!-- Modal de Aguardando Processamento -->
|
||||||
{#if aguardandoProcessamento}
|
{#if aguardandoProcessamento}
|
||||||
<div
|
<div
|
||||||
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
class="fixed inset-0 z-50 pointer-events-none"
|
||||||
style="animation: fadeIn 0.2s ease-out;"
|
style="animation: fadeIn 0.2s ease-out;"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="modal-aguardando-title"
|
aria-labelledby="modal-aguardando-title"
|
||||||
>
|
>
|
||||||
<!-- Backdrop -->
|
<!-- Backdrop leve -->
|
||||||
<div class="absolute inset-0 bg-black/50 backdrop-blur-sm transition-opacity duration-200"></div>
|
<div class="absolute inset-0 bg-black/20 transition-opacity duration-200 pointer-events-auto"></div>
|
||||||
|
|
||||||
<!-- Modal Box -->
|
<!-- Modal Box -->
|
||||||
<div
|
<div
|
||||||
class="relative bg-base-100 rounded-2xl shadow-2xl max-w-md w-full z-10 transform transition-all duration-300 p-8"
|
class="absolute bg-base-100 rounded-2xl shadow-2xl max-w-md w-full z-10 transform transition-all duration-300 p-8 pointer-events-auto"
|
||||||
style="animation: slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);"
|
style="animation: slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); {getModalStyle()}"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col items-center gap-4 text-center">
|
<div class="flex flex-col items-center gap-4 text-center">
|
||||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||||
@@ -1409,22 +1508,22 @@
|
|||||||
<!-- Modal de Confirmação -->
|
<!-- Modal de Confirmação -->
|
||||||
{#if mostrandoModalConfirmacao && imagemCapturada && dataHoraAtual}
|
{#if mostrandoModalConfirmacao && imagemCapturada && dataHoraAtual}
|
||||||
<div
|
<div
|
||||||
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
class="fixed inset-0 z-50 pointer-events-none"
|
||||||
style="animation: fadeIn 0.2s ease-out;"
|
style="animation: fadeIn 0.2s ease-out;"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="modal-confirmacao-title"
|
aria-labelledby="modal-confirmacao-title"
|
||||||
>
|
>
|
||||||
<!-- Backdrop -->
|
<!-- Backdrop leve -->
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-black/50 backdrop-blur-sm transition-opacity duration-200"
|
class="absolute inset-0 bg-black/20 transition-opacity duration-200 pointer-events-auto"
|
||||||
onclick={cancelarRegistro}
|
onclick={cancelarRegistro}
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<!-- Modal Box -->
|
<!-- Modal Box -->
|
||||||
<div
|
<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"
|
class="absolute 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 pointer-events-auto"
|
||||||
style="animation: slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);"
|
style="animation: slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); {getModalStyle()}"
|
||||||
onclick={(e) => e.stopPropagation()}
|
onclick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<!-- Header fixo -->
|
<!-- Header fixo -->
|
||||||
|
|||||||
Reference in New Issue
Block a user