feat: enhance modal positioning and styling across components
- Implemented dynamic positioning for modals in ErrorModal, ComprovantePonto, and RegistroPonto components to ensure they are centered based on the viewport and container dimensions. - Updated modal styles to improve visual consistency, including backdrop opacity adjustments and enhanced animations. - Refactored modal handling logic to include scroll and resize event listeners for better responsiveness during user interactions.
This commit is contained in:
@@ -11,6 +11,92 @@
|
||||
|
||||
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 container central da página
|
||||
function calcularPosicaoModal() {
|
||||
// Procurar pelo elemento container-central
|
||||
const containerCentral = document.getElementById('container-central');
|
||||
|
||||
if (containerCentral) {
|
||||
const rect = containerCentral.getBoundingClientRect();
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
// Centralizar baseado na largura do container central
|
||||
const containerCenterX = rect.left + (rect.width / 2);
|
||||
const containerTop = rect.top;
|
||||
|
||||
// Posicionar o modal centralizado verticalmente na viewport, mas respeitando o container
|
||||
const top = Math.max(50, Math.min(containerTop + 100, viewportHeight / 2));
|
||||
|
||||
return {
|
||||
top: top,
|
||||
left: containerCenterX
|
||||
};
|
||||
}
|
||||
|
||||
// Se não encontrar, usar posição padrão (centro da tela)
|
||||
return null;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
// Aguardar um pouco para garantir que o DOM está atualizado
|
||||
setTimeout(() => {
|
||||
modalPosition = calcularPosicaoModal();
|
||||
}, 10);
|
||||
|
||||
// Adicionar listener de scroll para atualizar posição
|
||||
const handleScroll = () => {
|
||||
modalPosition = calcularPosicaoModal();
|
||||
};
|
||||
|
||||
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) {
|
||||
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) {
|
||||
top = viewportHeight - modalHeight - 20;
|
||||
}
|
||||
// Ajustar se o modal sair da viewport acima
|
||||
if (top < 20) {
|
||||
top = 20;
|
||||
}
|
||||
|
||||
return `position: fixed; top: ${top}px; left: ${left}px; transform: translate(-50%, 0); 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
|
||||
const temInstrucoes = $derived.by(() => {
|
||||
if (!details) return false;
|
||||
@@ -31,22 +117,22 @@
|
||||
|
||||
{#if open}
|
||||
<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;"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-error-title"
|
||||
>
|
||||
<!-- Backdrop -->
|
||||
<!-- Backdrop leve -->
|
||||
<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}
|
||||
></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);"
|
||||
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); {getModalStyle()}"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<!-- Header fixo -->
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
import { useQuery } from 'convex-svelte';
|
||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||
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 { formatarDataHoraCompleta, getTipoRegistroLabel } from '$lib/utils/ponto';
|
||||
import logoGovPE from '$lib/assets/logo_governo_PE.png';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
registroId: Id<'registrosPonto'>;
|
||||
@@ -18,6 +19,89 @@
|
||||
const configQuery = useQuery(api.configuracaoPonto.obterConfiguracao, {});
|
||||
|
||||
let gerando = $state(false);
|
||||
let modalPosition = $state<{ top: number; left: number } | null>(null);
|
||||
|
||||
// Função para calcular a posição baseada no container central da página
|
||||
function calcularPosicaoModal() {
|
||||
// Procurar pelo elemento container-central
|
||||
const containerCentral = document.getElementById('container-central');
|
||||
|
||||
if (containerCentral) {
|
||||
const rect = containerCentral.getBoundingClientRect();
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
// Centralizar baseado na largura do container central
|
||||
const containerCenterX = rect.left + (rect.width / 2);
|
||||
const containerTop = rect.top;
|
||||
|
||||
// Posicionar o modal centralizado verticalmente na viewport, mas respeitando o container
|
||||
const top = Math.max(50, Math.min(containerTop + 100, viewportHeight / 2));
|
||||
|
||||
return {
|
||||
top: top,
|
||||
left: containerCenterX
|
||||
};
|
||||
}
|
||||
|
||||
// Se não encontrar, usar posição padrão (centro da tela)
|
||||
return null;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Aguardar um pouco para garantir que o DOM está atualizado
|
||||
setTimeout(() => {
|
||||
modalPosition = calcularPosicaoModal();
|
||||
}, 10);
|
||||
|
||||
// Adicionar listener de scroll para atualizar posição
|
||||
const handleScroll = () => {
|
||||
modalPosition = calcularPosicaoModal();
|
||||
};
|
||||
|
||||
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) {
|
||||
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) {
|
||||
top = viewportHeight - modalHeight - 20;
|
||||
}
|
||||
// Ajustar se o modal sair da viewport acima
|
||||
if (top < 20) {
|
||||
top = 20;
|
||||
}
|
||||
|
||||
return `position: fixed; top: ${top}px; left: ${left}px; transform: translate(-50%, 0); 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() {
|
||||
if (!registroQuery?.data) return;
|
||||
@@ -238,99 +322,178 @@
|
||||
</script>
|
||||
|
||||
<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;"
|
||||
onkeydown={(e) => e.key === 'Escape' && onClose()}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-comprovante-title"
|
||||
>
|
||||
<!-- Backdrop com blur -->
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-comprovante-title"
|
||||
>
|
||||
<!-- Backdrop leve -->
|
||||
<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}
|
||||
></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);"
|
||||
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); {getModalStyle()}"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<!-- Header fixo -->
|
||||
<div class="flex items-center justify-between mb-4 pb-4 border-b border-base-300 flex-shrink-0">
|
||||
<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}>
|
||||
<!-- Header Premium com gradiente -->
|
||||
<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">
|
||||
<div class="flex items-center gap-3">
|
||||
<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" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 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}
|
||||
<div class="flex justify-center items-center py-8">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
<div class="flex justify-center items-center py-12">
|
||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||
</div>
|
||||
{:else if !registroQuery?.data}
|
||||
<div class="alert alert-error">
|
||||
<span>Erro ao carregar registro</span>
|
||||
<div class="alert alert-error shadow-lg">
|
||||
<XCircle class="h-5 w-5" />
|
||||
<span class="font-semibold">Erro ao carregar registro</span>
|
||||
</div>
|
||||
{:else}
|
||||
{@const registro = registroQuery.data}
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-6">
|
||||
<!-- Informações do Funcionário -->
|
||||
<div class="card bg-base-200">
|
||||
<div class="card-body">
|
||||
<h4 class="font-bold">Dados do Funcionário</h4>
|
||||
<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 p-6">
|
||||
<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}
|
||||
<p><strong>Matrícula:</strong> {registro.funcionario.matricula || 'N/A'}</p>
|
||||
<p><strong>Nome:</strong> {registro.funcionario.nome}</p>
|
||||
{#if registro.funcionario.descricaoCargo}
|
||||
<p><strong>Cargo/Função:</strong> {registro.funcionario.descricaoCargo}</p>
|
||||
{/if}
|
||||
<div class="space-y-3">
|
||||
{#if registro.funcionario.matricula}
|
||||
<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">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}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Informações do Registro -->
|
||||
<div class="card bg-base-200">
|
||||
<div class="card-body">
|
||||
<h4 class="font-bold">Dados do Registro</h4>
|
||||
<p>
|
||||
<strong>Tipo:</strong>
|
||||
{configQuery?.data
|
||||
? getTipoRegistroLabel(registro.tipo, {
|
||||
nomeEntrada: configQuery.data.nomeEntrada,
|
||||
nomeSaidaAlmoco: configQuery.data.nomeSaidaAlmoco,
|
||||
nomeRetornoAlmoco: configQuery.data.nomeRetornoAlmoco,
|
||||
nomeSaida: configQuery.data.nomeSaida,
|
||||
})
|
||||
: getTipoRegistroLabel(registro.tipo)}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Data e Hora:</strong>
|
||||
{formatarDataHoraCompleta(registro.data, registro.hora, registro.minuto, registro.segundo)}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Status:</strong>
|
||||
<span class="badge {registro.dentroDoPrazo ? 'badge-success' : 'badge-error'}">
|
||||
{registro.dentroDoPrazo ? 'Dentro do Prazo' : 'Fora do Prazo'}
|
||||
</span>
|
||||
</p>
|
||||
<p><strong>Tolerância:</strong> {registro.toleranciaMinutos} minutos</p>
|
||||
<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 p-6">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="p-2 bg-primary/20 rounded-lg">
|
||||
<Clock class="h-5 w-5 text-primary" strokeWidth={2} />
|
||||
</div>
|
||||
<h4 class="font-bold text-lg text-base-content">Dados do Registro</h4>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Tipo -->
|
||||
<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>
|
||||
<p class="text-lg font-bold text-primary mt-1">
|
||||
{configQuery?.data
|
||||
? getTipoRegistroLabel(registro.tipo, {
|
||||
nomeEntrada: configQuery.data.nomeEntrada,
|
||||
nomeSaidaAlmoco: configQuery.data.nomeSaidaAlmoco,
|
||||
nomeRetornoAlmoco: configQuery.data.nomeRetornoAlmoco,
|
||||
nomeSaida: configQuery.data.nomeSaida,
|
||||
})
|
||||
: getTipoRegistroLabel(registro.tipo)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- Imagem Capturada -->
|
||||
{#if registro.imagemUrl}
|
||||
<div class="card bg-base-200">
|
||||
<div class="card-body">
|
||||
<h4 class="font-bold mb-2">Foto Capturada</h4>
|
||||
<div class="flex justify-center">
|
||||
<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 p-6">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<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
|
||||
src={registro.imagemUrl}
|
||||
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) => {
|
||||
console.error('Erro ao carregar imagem:', e);
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
@@ -345,16 +508,20 @@
|
||||
</div>
|
||||
|
||||
<!-- Footer fixo com botões -->
|
||||
<div class="flex justify-end gap-2 pt-4 mt-4 border-t border-base-300 flex-shrink-0">
|
||||
<button class="btn btn-primary gap-2" onclick={gerarPDF} disabled={gerando}>
|
||||
<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-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}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
Gerando...
|
||||
{:else}
|
||||
<Printer class="h-5 w-5" />
|
||||
Imprimir Comprovante
|
||||
{/if}
|
||||
Imprimir Comprovante
|
||||
</button>
|
||||
<button class="btn btn-outline" onclick={onClose}>Fechar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -851,6 +851,94 @@
|
||||
});
|
||||
|
||||
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 container central da página
|
||||
function calcularPosicaoModal() {
|
||||
// Procurar pelo elemento container-central
|
||||
const containerCentral = document.getElementById('container-central');
|
||||
|
||||
if (containerCentral) {
|
||||
const rect = containerCentral.getBoundingClientRect();
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
// Centralizar baseado na largura do container central
|
||||
const containerCenterX = rect.left + (rect.width / 2);
|
||||
const containerTop = rect.top;
|
||||
|
||||
// Posicionar o modal centralizado verticalmente na viewport, mas respeitando o container
|
||||
const top = Math.max(50, Math.min(containerTop + 100, viewportHeight / 2));
|
||||
|
||||
return {
|
||||
top: top,
|
||||
left: containerCenterX
|
||||
};
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// Aguardar um pouco para garantir que o DOM está atualizado
|
||||
setTimeout(() => {
|
||||
modalPosition = calcularPosicaoModal();
|
||||
}, 10);
|
||||
|
||||
// Adicionar listener de scroll para atualizar posição
|
||||
const handleScroll = () => {
|
||||
modalPosition = calcularPosicaoModal();
|
||||
};
|
||||
|
||||
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 = 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) {
|
||||
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) {
|
||||
top = viewportHeight - modalHeight - 20;
|
||||
}
|
||||
// Ajustar se o modal sair da viewport acima
|
||||
if (top < 20) {
|
||||
top = 20;
|
||||
}
|
||||
|
||||
return `position: fixed; top: ${top}px; left: ${left}px; transform: translate(-50%, 0); 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>
|
||||
|
||||
<div class="space-y-6">
|
||||
@@ -1330,22 +1418,22 @@
|
||||
<!-- Modal Webcam -->
|
||||
{#if mostrandoWebcam}
|
||||
<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;"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-webcam-title"
|
||||
>
|
||||
<!-- Backdrop -->
|
||||
<!-- Backdrop leve -->
|
||||
<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}
|
||||
></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);"
|
||||
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); {getModalStyle()}"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<!-- Header fixo -->
|
||||
@@ -1383,19 +1471,19 @@
|
||||
<!-- Modal de Aguardando Processamento -->
|
||||
{#if aguardandoProcessamento}
|
||||
<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;"
|
||||
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>
|
||||
<!-- Backdrop leve -->
|
||||
<div class="absolute inset-0 bg-black/20 transition-opacity duration-200 pointer-events-auto"></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);"
|
||||
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); {getModalStyle()}"
|
||||
>
|
||||
<div class="flex flex-col items-center gap-4 text-center">
|
||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||
@@ -1409,22 +1497,22 @@
|
||||
<!-- Modal de Confirmação -->
|
||||
{#if mostrandoModalConfirmacao && imagemCapturada && dataHoraAtual}
|
||||
<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;"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-confirmacao-title"
|
||||
>
|
||||
<!-- Backdrop -->
|
||||
<!-- Backdrop leve -->
|
||||
<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}
|
||||
></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);"
|
||||
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); {getModalStyle()}"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<!-- Header fixo -->
|
||||
|
||||
Reference in New Issue
Block a user