Ajustes final etapa1 #71
@@ -9,6 +9,7 @@
|
|||||||
import NewConversationModal from './NewConversationModal.svelte';
|
import NewConversationModal from './NewConversationModal.svelte';
|
||||||
import { Search, Plus, MessageSquare, Users, UsersRound } from 'lucide-svelte';
|
import { Search, Plus, MessageSquare, Users, UsersRound } from 'lucide-svelte';
|
||||||
import type { Id, Doc } from '@sgse-app/backend/convex/_generated/dataModel';
|
import type { Id, Doc } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
|
import { obterCoresDoTema } from '$lib/utils/temas';
|
||||||
|
|
||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
|
|
||||||
@@ -24,6 +25,57 @@
|
|||||||
let searchQuery = $state('');
|
let searchQuery = $state('');
|
||||||
let activeTab = $state<'usuarios' | 'conversas'>('usuarios');
|
let activeTab = $state<'usuarios' | 'conversas'>('usuarios');
|
||||||
|
|
||||||
|
// Obter cores do tema atual (reativo)
|
||||||
|
let coresTema = $state(obterCoresDoTema());
|
||||||
|
|
||||||
|
// Atualizar cores quando o tema mudar
|
||||||
|
$effect(() => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
const atualizarCores = () => {
|
||||||
|
coresTema = obterCoresDoTema();
|
||||||
|
};
|
||||||
|
|
||||||
|
atualizarCores();
|
||||||
|
|
||||||
|
window.addEventListener('themechange', atualizarCores);
|
||||||
|
|
||||||
|
const observer = new MutationObserver(atualizarCores);
|
||||||
|
const htmlElement = document.documentElement;
|
||||||
|
observer.observe(htmlElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['data-theme']
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('themechange', atualizarCores);
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Função para obter rgba da cor primária
|
||||||
|
function obterPrimariaRgba(alpha: number = 1) {
|
||||||
|
const primary = coresTema.primary;
|
||||||
|
if (primary.startsWith('rgba')) {
|
||||||
|
const match = primary.match(/rgba?\(([^)]+)\)/);
|
||||||
|
if (match) {
|
||||||
|
const values = match[1].split(',');
|
||||||
|
return `rgba(${values[0]}, ${values[1]}, ${values[2]}, ${alpha})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (primary.startsWith('#')) {
|
||||||
|
const hex = primary.replace('#', '');
|
||||||
|
const r = parseInt(hex.substring(0, 2), 16);
|
||||||
|
const g = parseInt(hex.substring(2, 4), 16);
|
||||||
|
const b = parseInt(hex.substring(4, 6), 16);
|
||||||
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||||
|
}
|
||||||
|
if (primary.startsWith('hsl')) {
|
||||||
|
return primary.replace(/\)$/, `, ${alpha})`).replace('hsl', 'hsla');
|
||||||
|
}
|
||||||
|
return `rgba(102, 126, 234, ${alpha})`;
|
||||||
|
}
|
||||||
|
|
||||||
// Debug: monitorar carregamento de dados
|
// Debug: monitorar carregamento de dados
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
console.log('📊 [ChatList] Usuários carregados:', usuarios?.data?.length || 0);
|
console.log('📊 [ChatList] Usuários carregados:', usuarios?.data?.length || 0);
|
||||||
@@ -263,7 +315,7 @@
|
|||||||
<!-- Ícone de mensagem -->
|
<!-- Ícone de mensagem -->
|
||||||
<div
|
<div
|
||||||
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl transition-all duration-300 hover:scale-110"
|
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl transition-all duration-300 hover:scale-110"
|
||||||
style="background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%); border: 1px solid rgba(102, 126, 234, 0.2);"
|
style="background: linear-gradient(135deg, {obterPrimariaRgba(0.1)} 0%, {obterPrimariaRgba(0.1)} 100%); border: 1px solid {obterPrimariaRgba(0.2)};"
|
||||||
>
|
>
|
||||||
<MessageSquare class="text-primary h-5 w-5" strokeWidth={2} />
|
<MessageSquare class="text-primary h-5 w-5" strokeWidth={2} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
import ChatList from './ChatList.svelte';
|
import ChatList from './ChatList.svelte';
|
||||||
import ChatWindow from './ChatWindow.svelte';
|
import ChatWindow from './ChatWindow.svelte';
|
||||||
import { MessageSquare, Minus, Maximize2, X, Bell } from 'lucide-svelte';
|
import { MessageSquare, Minus, Maximize2, X, Bell } from 'lucide-svelte';
|
||||||
|
import { obterCoresDoTema, obterTemaPersistidoNoLocalStorage } from '$lib/utils/temas';
|
||||||
|
|
||||||
const count = useQuery(api.chat.contarNotificacoesNaoLidas, {});
|
const count = useQuery(api.chat.contarNotificacoesNaoLidas, {});
|
||||||
|
|
||||||
@@ -955,6 +956,80 @@
|
|||||||
window.removeEventListener('touchend', handleTouchEnd);
|
window.removeEventListener('touchend', handleTouchEnd);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Obter cores do tema atual (reativo)
|
||||||
|
let coresTema = $state(obterCoresDoTema());
|
||||||
|
|
||||||
|
// Atualizar cores quando o tema mudar
|
||||||
|
$effect(() => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
const atualizarCores = () => {
|
||||||
|
coresTema = obterCoresDoTema();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Atualizar cores inicialmente
|
||||||
|
atualizarCores();
|
||||||
|
|
||||||
|
// Escutar mudanças de tema
|
||||||
|
window.addEventListener('themechange', atualizarCores);
|
||||||
|
|
||||||
|
// Observar mudanças no atributo data-theme do HTML
|
||||||
|
const observer = new MutationObserver(atualizarCores);
|
||||||
|
const htmlElement = document.documentElement;
|
||||||
|
observer.observe(htmlElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['data-theme']
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('themechange', atualizarCores);
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Função para obter gradiente do tema
|
||||||
|
function obterGradienteTema() {
|
||||||
|
const primary = coresTema.primary;
|
||||||
|
// Criar variações da cor primária para o gradiente
|
||||||
|
return `linear-gradient(135deg, ${primary} 0%, ${primary}dd 50%, ${primary}bb 100%)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Função para obter rgba da cor primária
|
||||||
|
function obterPrimariaRgba(alpha: number = 1) {
|
||||||
|
const primary = coresTema.primary.trim();
|
||||||
|
// Se já for rgba, extrair os valores
|
||||||
|
if (primary.startsWith('rgba')) {
|
||||||
|
const match = primary.match(/rgba?\(([^)]+)\)/);
|
||||||
|
if (match) {
|
||||||
|
const values = match[1].split(',').map(v => v.trim());
|
||||||
|
if (values.length >= 3) {
|
||||||
|
return `rgba(${values[0]}, ${values[1]}, ${values[2]}, ${alpha})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Se for hex, converter
|
||||||
|
if (primary.startsWith('#')) {
|
||||||
|
const hex = primary.replace('#', '');
|
||||||
|
if (hex.length === 6) {
|
||||||
|
const r = parseInt(hex.substring(0, 2), 16);
|
||||||
|
const g = parseInt(hex.substring(2, 4), 16);
|
||||||
|
const b = parseInt(hex.substring(4, 6), 16);
|
||||||
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Se for hsl, converter para hsla
|
||||||
|
if (primary.startsWith('hsl')) {
|
||||||
|
const match = primary.match(/hsl\(([^)]+)\)/);
|
||||||
|
if (match) {
|
||||||
|
return `hsla(${match[1]}, ${alpha})`;
|
||||||
|
}
|
||||||
|
// Fallback: tentar adicionar alpha
|
||||||
|
return primary.replace(/\)$/, `, ${alpha})`).replace('hsl', 'hsla');
|
||||||
|
}
|
||||||
|
// Fallback padrão
|
||||||
|
return `rgba(102, 126, 234, ${alpha})`;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Botão flutuante MODERNO E ARRASTÁVEL -->
|
<!-- Botão flutuante MODERNO E ARRASTÁVEL -->
|
||||||
@@ -975,10 +1050,10 @@
|
|||||||
bottom: {bottomPos};
|
bottom: {bottomPos};
|
||||||
right: {rightPos};
|
right: {rightPos};
|
||||||
position: fixed !important;
|
position: fixed !important;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
|
background: {obterGradienteTema()};
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 20px 60px -10px rgba(102, 126, 234, 0.5),
|
0 20px 60px -10px {obterPrimariaRgba(0.5)},
|
||||||
0 10px 30px -5px rgba(118, 75, 162, 0.4),
|
0 10px 30px -5px {obterPrimariaRgba(0.4)},
|
||||||
0 0 0 1px rgba(255, 255, 255, 0.1) inset;
|
0 0 0 1px rgba(255, 255, 255, 0.1) inset;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
cursor: {isDragging ? 'grabbing' : 'grab'};
|
cursor: {isDragging ? 'grabbing' : 'grab'};
|
||||||
@@ -1058,17 +1133,17 @@
|
|||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Badge ULTRA PREMIUM com gradiente e brilho -->
|
<!-- Badge ULTRA PREMIUM com gradiente e brilho usando cores do tema -->
|
||||||
{#if count?.data && count.data > 0}
|
{#if count?.data && count.data > 0}
|
||||||
<span
|
<span
|
||||||
class="absolute -top-1 -right-1 z-20 flex h-8 w-8 items-center justify-center rounded-full text-xs font-black text-white"
|
class="absolute -top-1 -right-1 z-20 flex h-8 w-8 items-center justify-center rounded-full text-xs font-black text-white"
|
||||||
style="
|
style="
|
||||||
background: linear-gradient(135deg, #ff416c, #ff4b2b);
|
background: {coresTema.error ? `linear-gradient(135deg, ${coresTema.error}, ${coresTema.error}dd)` : 'linear-gradient(135deg, #ff416c, #ff4b2b)'};
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 8px 24px -4px rgba(255, 65, 108, 0.6),
|
0 8px 24px -4px {coresTema.error ? obterPrimariaRgba(0.6).replace(coresTema.primary, coresTema.error) : 'rgba(255, 65, 108, 0.6)'},
|
||||||
0 4px 12px -2px rgba(255, 75, 43, 0.4),
|
0 4px 12px -2px {coresTema.error ? obterPrimariaRgba(0.4).replace(coresTema.primary, coresTema.error) : 'rgba(255, 75, 43, 0.4)'},
|
||||||
0 0 0 3px rgba(255, 255, 255, 0.3),
|
0 0 0 3px rgba(255, 255, 255, 0.3),
|
||||||
0 0 0 5px rgba(255, 65, 108, 0.2);
|
0 0 0 5px {coresTema.error ? obterPrimariaRgba(0.2).replace(coresTema.primary, coresTema.error) : 'rgba(255, 65, 108, 0.2)'};
|
||||||
animation: badge-bounce 2s ease-in-out infinite;
|
animation: badge-bounce 2s ease-in-out infinite;
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
@@ -1121,8 +1196,8 @@
|
|||||||
<div
|
<div
|
||||||
class="relative flex items-center justify-between overflow-hidden px-6 py-5 text-white"
|
class="relative flex items-center justify-between overflow-hidden px-6 py-5 text-white"
|
||||||
style="
|
style="
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
|
background: {obterGradienteTema()};
|
||||||
box-shadow: 0 8px 32px -4px rgba(102, 126, 234, 0.3);
|
box-shadow: 0 8px 32px -4px {obterPrimariaRgba(0.3)};
|
||||||
cursor: {isDragging ? 'grabbing' : 'grab'};
|
cursor: {isDragging ? 'grabbing' : 'grab'};
|
||||||
"
|
"
|
||||||
onmousedown={handleMouseDown}
|
onmousedown={handleMouseDown}
|
||||||
@@ -1239,77 +1314,77 @@
|
|||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
aria-label="Redimensionar janela pela borda superior"
|
aria-label="Redimensionar janela pela borda superior"
|
||||||
class="hover:bg-primary/20 absolute top-0 right-0 left-0 z-50 h-2 cursor-ns-resize transition-colors"
|
class="absolute top-0 right-0 left-0 z-50 h-2 cursor-ns-resize transition-colors"
|
||||||
|
style="--hover-bg: {obterPrimariaRgba(0.2)}; border-radius: 24px 24px 0 0;"
|
||||||
onmousedown={(e) => handleResizeStart(e, 'n')}
|
onmousedown={(e) => handleResizeStart(e, 'n')}
|
||||||
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'n')}
|
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'n')}
|
||||||
style="border-radius: 24px 24px 0 0;"
|
|
||||||
></div>
|
></div>
|
||||||
<!-- Bottom -->
|
<!-- Bottom -->
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
aria-label="Redimensionar janela pela borda inferior"
|
aria-label="Redimensionar janela pela borda inferior"
|
||||||
class="hover:bg-primary/20 absolute right-0 bottom-0 left-0 z-50 h-2 cursor-ns-resize transition-colors"
|
class="absolute right-0 bottom-0 left-0 z-50 h-2 cursor-ns-resize transition-colors"
|
||||||
|
style="--hover-bg: {obterPrimariaRgba(0.2)}; border-radius: 0 0 24px 24px;"
|
||||||
onmousedown={(e) => handleResizeStart(e, 's')}
|
onmousedown={(e) => handleResizeStart(e, 's')}
|
||||||
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 's')}
|
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 's')}
|
||||||
style="border-radius: 0 0 24px 24px;"
|
|
||||||
></div>
|
></div>
|
||||||
<!-- Left -->
|
<!-- Left -->
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
aria-label="Redimensionar janela pela borda esquerda"
|
aria-label="Redimensionar janela pela borda esquerda"
|
||||||
class="hover:bg-primary/20 absolute top-0 bottom-0 left-0 z-50 w-2 cursor-ew-resize transition-colors"
|
class="absolute top-0 bottom-0 left-0 z-50 w-2 cursor-ew-resize transition-colors"
|
||||||
|
style="--hover-bg: {obterPrimariaRgba(0.2)}; border-radius: 24px 0 0 24px;"
|
||||||
onmousedown={(e) => handleResizeStart(e, 'w')}
|
onmousedown={(e) => handleResizeStart(e, 'w')}
|
||||||
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'w')}
|
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'w')}
|
||||||
style="border-radius: 24px 0 0 24px;"
|
|
||||||
></div>
|
></div>
|
||||||
<!-- Right -->
|
<!-- Right -->
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
aria-label="Redimensionar janela pela borda direita"
|
aria-label="Redimensionar janela pela borda direita"
|
||||||
class="hover:bg-primary/20 absolute top-0 right-0 bottom-0 z-50 w-2 cursor-ew-resize transition-colors"
|
class="absolute top-0 right-0 bottom-0 z-50 w-2 cursor-ew-resize transition-colors"
|
||||||
|
style="--hover-bg: {obterPrimariaRgba(0.2)}; border-radius: 0 24px 24px 0;"
|
||||||
onmousedown={(e) => handleResizeStart(e, 'e')}
|
onmousedown={(e) => handleResizeStart(e, 'e')}
|
||||||
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'e')}
|
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'e')}
|
||||||
style="border-radius: 0 24px 24px 0;"
|
|
||||||
></div>
|
></div>
|
||||||
<!-- Corners -->
|
<!-- Corners -->
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
aria-label="Redimensionar janela pelo canto superior esquerdo"
|
aria-label="Redimensionar janela pelo canto superior esquerdo"
|
||||||
class="hover:bg-primary/20 absolute top-0 left-0 z-50 h-4 w-4 cursor-nwse-resize transition-colors"
|
class="absolute top-0 left-0 z-50 h-4 w-4 cursor-nwse-resize transition-colors"
|
||||||
|
style="--hover-bg: {obterPrimariaRgba(0.2)}; border-radius: 24px 0 0 0;"
|
||||||
onmousedown={(e) => handleResizeStart(e, 'nw')}
|
onmousedown={(e) => handleResizeStart(e, 'nw')}
|
||||||
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'nw')}
|
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'nw')}
|
||||||
style="border-radius: 24px 0 0 0;"
|
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
aria-label="Redimensionar janela pelo canto superior direito"
|
aria-label="Redimensionar janela pelo canto superior direito"
|
||||||
class="hover:bg-primary/20 absolute top-0 right-0 z-50 h-4 w-4 cursor-nesw-resize transition-colors"
|
class="absolute top-0 right-0 z-50 h-4 w-4 cursor-nesw-resize transition-colors"
|
||||||
|
style="--hover-bg: {obterPrimariaRgba(0.2)}; border-radius: 0 24px 0 0;"
|
||||||
onmousedown={(e) => handleResizeStart(e, 'ne')}
|
onmousedown={(e) => handleResizeStart(e, 'ne')}
|
||||||
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'ne')}
|
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'ne')}
|
||||||
style="border-radius: 0 24px 0 0;"
|
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
aria-label="Redimensionar janela pelo canto inferior esquerdo"
|
aria-label="Redimensionar janela pelo canto inferior esquerdo"
|
||||||
class="hover:bg-primary/20 absolute bottom-0 left-0 z-50 h-4 w-4 cursor-nesw-resize transition-colors"
|
class="absolute bottom-0 left-0 z-50 h-4 w-4 cursor-nesw-resize transition-colors"
|
||||||
|
style="--hover-bg: {obterPrimariaRgba(0.2)}; border-radius: 0 0 0 24px;"
|
||||||
onmousedown={(e) => handleResizeStart(e, 'sw')}
|
onmousedown={(e) => handleResizeStart(e, 'sw')}
|
||||||
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'sw')}
|
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'sw')}
|
||||||
style="border-radius: 0 0 0 24px;"
|
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
aria-label="Redimensionar janela pelo canto inferior direito"
|
aria-label="Redimensionar janela pelo canto inferior direito"
|
||||||
class="hover:bg-primary/20 absolute right-0 bottom-0 z-50 h-4 w-4 cursor-nwse-resize transition-colors"
|
class="absolute right-0 bottom-0 z-50 h-4 w-4 cursor-nwse-resize transition-colors"
|
||||||
|
style="--hover-bg: {obterPrimariaRgba(0.2)}; border-radius: 0 0 24px 0;"
|
||||||
onmousedown={(e) => handleResizeStart(e, 'se')}
|
onmousedown={(e) => handleResizeStart(e, 'se')}
|
||||||
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'se')}
|
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'se')}
|
||||||
style="border-radius: 0 0 24px 0;"
|
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1324,8 +1399,8 @@
|
|||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
aria-label="Abrir conversa: Nova mensagem de {notificationMsg.remetente}"
|
aria-label="Abrir conversa: Nova mensagem de {notificationMsg.remetente}"
|
||||||
class="bg-base-100 border-primary/20 fixed top-4 right-4 z-1000 max-w-sm cursor-pointer rounded-lg border p-4 shadow-2xl"
|
class="bg-base-100 fixed top-4 right-4 z-1000 max-w-sm cursor-pointer rounded-lg border p-4 shadow-2xl"
|
||||||
style="box-shadow: 0 10px 40px -10px rgba(0,0,0,0.3); animation: slideInRight 0.3s ease-out;"
|
style="border-color: {obterPrimariaRgba(0.2)}; box-shadow: 0 10px 40px -10px rgba(0,0,0,0.3); animation: slideInRight 0.3s ease-out;"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
const conversaIdToOpen = notificationMsg?.conversaId;
|
const conversaIdToOpen = notificationMsg?.conversaId;
|
||||||
showGlobalNotificationPopup = false;
|
showGlobalNotificationPopup = false;
|
||||||
@@ -1356,8 +1431,8 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<div class="bg-primary/20 flex h-10 w-10 shrink-0 items-center justify-center rounded-full">
|
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full" style="background-color: {obterPrimariaRgba(0.2)}">
|
||||||
<Bell class="text-primary h-5 w-5" strokeWidth={2} />
|
<Bell class="h-5 w-5" style="color: {coresTema.primary}" strokeWidth={2} />
|
||||||
</div>
|
</div>
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<p class="text-base-content mb-1 text-sm font-semibold">
|
<p class="text-base-content mb-1 text-sm font-semibold">
|
||||||
@@ -1366,7 +1441,7 @@
|
|||||||
<p class="text-base-content/70 line-clamp-2 text-xs">
|
<p class="text-base-content/70 line-clamp-2 text-xs">
|
||||||
{notificationMsg.conteudo}
|
{notificationMsg.conteudo}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-primary mt-1 text-xs">Clique para abrir</p>
|
<p class="mt-1 text-xs" style="color: {coresTema.primary}">Clique para abrir</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1414,19 +1489,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ondas de pulso para o botão flutuante */
|
/* Ondas de pulso para o botão flutuante - cores dinâmicas */
|
||||||
@keyframes pulse-ring {
|
@keyframes pulse-ring {
|
||||||
0% {
|
0% {
|
||||||
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0.5);
|
box-shadow: 0 0 0 0 var(--pulse-color, rgba(102, 126, 234, 0.5));
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
box-shadow: 0 0 0 15px rgba(102, 126, 234, 0);
|
box-shadow: 0 0 0 15px transparent;
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0);
|
box-shadow: 0 0 0 0 var(--pulse-color, rgba(102, 126, 234, 0.5));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Estilos para handles de redimensionamento com hover dinâmico */
|
||||||
|
[style*="--hover-bg"]:hover {
|
||||||
|
background-color: var(--hover-bg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Rotação para anel de brilho - suavizada */
|
/* Rotação para anel de brilho - suavizada */
|
||||||
@keyframes rotate {
|
@keyframes rotate {
|
||||||
from {
|
from {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
//import { getAvatarUrl } from '$lib/utils/avatarGenerator';
|
//import { getAvatarUrl } from '$lib/utils/avatarGenerator';
|
||||||
import { voltarParaLista } from '$lib/stores/chatStore';
|
import { voltarParaLista } from '$lib/stores/chatStore';
|
||||||
import { useConvexClient, useQuery } from 'convex-svelte';
|
import { useConvexClient, useQuery } from 'convex-svelte';
|
||||||
|
import { obterCoresDoTema } from '$lib/utils/temas';
|
||||||
|
|
||||||
//import { Bell, X, ArrowLeft, LogOut, MoreVertical, Users, Clock, XCircle } from 'lucide-svelte';
|
//import { Bell, X, ArrowLeft, LogOut, MoreVertical, Users, Clock, XCircle } from 'lucide-svelte';
|
||||||
|
|
||||||
@@ -260,23 +261,84 @@
|
|||||||
let souAnfitriao = $derived(
|
let souAnfitriao = $derived(
|
||||||
chamadaAtual && meuPerfil?.data ? chamadaAtual.criadoPor === meuPerfil.data._id : false
|
chamadaAtual && meuPerfil?.data ? chamadaAtual.criadoPor === meuPerfil.data._id : false
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Obter cores do tema atual (reativo)
|
||||||
|
let coresTema = $state(obterCoresDoTema());
|
||||||
|
|
||||||
|
// Atualizar cores quando o tema mudar
|
||||||
|
$effect(() => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
const atualizarCores = () => {
|
||||||
|
coresTema = obterCoresDoTema();
|
||||||
|
};
|
||||||
|
|
||||||
|
atualizarCores();
|
||||||
|
|
||||||
|
window.addEventListener('themechange', atualizarCores);
|
||||||
|
|
||||||
|
const observer = new MutationObserver(atualizarCores);
|
||||||
|
const htmlElement = document.documentElement;
|
||||||
|
observer.observe(htmlElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['data-theme']
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('themechange', atualizarCores);
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Função para obter rgba da cor primária
|
||||||
|
function obterPrimariaRgba(alpha: number = 1) {
|
||||||
|
const primary = coresTema.primary.trim();
|
||||||
|
if (primary.startsWith('rgba')) {
|
||||||
|
const match = primary.match(/rgba?\(([^)]+)\)/);
|
||||||
|
if (match) {
|
||||||
|
const values = match[1].split(',').map(v => v.trim());
|
||||||
|
if (values.length >= 3) {
|
||||||
|
return `rgba(${values[0]}, ${values[1]}, ${values[2]}, ${alpha})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (primary.startsWith('#')) {
|
||||||
|
const hex = primary.replace('#', '');
|
||||||
|
if (hex.length === 6) {
|
||||||
|
const r = parseInt(hex.substring(0, 2), 16);
|
||||||
|
const g = parseInt(hex.substring(2, 4), 16);
|
||||||
|
const b = parseInt(hex.substring(4, 6), 16);
|
||||||
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (primary.startsWith('hsl')) {
|
||||||
|
const match = primary.match(/hsl\(([^)]+)\)/);
|
||||||
|
if (match) {
|
||||||
|
return `hsla(${match[1]}, ${alpha})`;
|
||||||
|
}
|
||||||
|
return primary.replace(/\)$/, `, ${alpha})`).replace('hsl', 'hsla');
|
||||||
|
}
|
||||||
|
return `rgba(102, 126, 234, ${alpha})`;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-full flex-col" onclick={() => (showAdminMenu = false)}>
|
<div class="flex h-full flex-col" onclick={() => (showAdminMenu = false)}>
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div
|
<div
|
||||||
class="border-base-300 bg-base-200 flex items-center gap-3 border-b px-4 py-3"
|
class="border-base-300 flex items-center gap-3 border-b px-4 py-3"
|
||||||
|
style="background-color: {coresTema.base200}; border-color: {coresTema.base300};"
|
||||||
onclick={(e) => e.stopPropagation()}
|
onclick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<!-- Botão Voltar -->
|
<!-- Botão Voltar -->
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-circle hover:bg-primary/20 transition-all duration-200 hover:scale-110"
|
class="btn btn-sm btn-circle transition-all duration-200 hover:scale-110"
|
||||||
|
style="--hover-bg: {obterPrimariaRgba(0.2)}"
|
||||||
onclick={voltarParaLista}
|
onclick={voltarParaLista}
|
||||||
aria-label="Voltar"
|
aria-label="Voltar"
|
||||||
title="Voltar para lista de conversas"
|
title="Voltar para lista de conversas"
|
||||||
>
|
>
|
||||||
<ArrowLeft class="text-primary h-6 w-6" strokeWidth={2.5} />
|
<ArrowLeft class="h-6 w-6" style="color: {coresTema.primary}" strokeWidth={2.5} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Avatar e Info -->
|
<!-- Avatar e Info -->
|
||||||
@@ -289,7 +351,7 @@
|
|||||||
userId={conversa()?.outroUsuario?._id}
|
userId={conversa()?.outroUsuario?._id}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="bg-primary/20 flex h-10 w-10 items-center justify-center rounded-full text-xl">
|
<div class="flex h-10 w-10 items-center justify-center rounded-full text-xl" style="background-color: {obterPrimariaRgba(0.2)}">
|
||||||
{getAvatarConversa()}
|
{getAvatarConversa()}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -380,7 +442,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{#if conversa()?.tipo === 'sala_reuniao' && isAdmin?.data}
|
{#if conversa()?.tipo === 'sala_reuniao' && isAdmin?.data}
|
||||||
<span
|
<span
|
||||||
class="text-primary ml-1 text-[10px] font-semibold whitespace-nowrap"
|
class="ml-1 text-[10px] font-semibold whitespace-nowrap"
|
||||||
|
style="color: {coresTema.primary}"
|
||||||
title="Você é administrador desta sala">• Admin</span
|
title="Você é administrador desta sala">• Admin</span
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -727,10 +790,8 @@
|
|||||||
{#each searchResults as resultado, index (resultado._id)}
|
{#each searchResults as resultado, index (resultado._id)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="hover:bg-base-300 flex w-full items-start gap-3 px-4 py-3 text-left transition-colors {index ===
|
class="hover:bg-base-300 flex w-full items-start gap-3 px-4 py-3 text-left transition-colors"
|
||||||
selectedSearchResult
|
style={index === selectedSearchResult ? `background-color: ${obterPrimariaRgba(0.1)}` : ''}
|
||||||
? 'bg-primary/10'
|
|
||||||
: ''}"
|
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent('scrollToMessage', {
|
new CustomEvent('scrollToMessage', {
|
||||||
@@ -745,7 +806,8 @@
|
|||||||
aria-label="Mensagem de {resultado.remetente?.nome || 'Usuário'}"
|
aria-label="Mensagem de {resultado.remetente?.nome || 'Usuário'}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="bg-primary/20 flex h-8 w-8 shrink-0 items-center justify-center overflow-hidden rounded-full"
|
class="flex h-8 w-8 shrink-0 items-center justify-center overflow-hidden rounded-full"
|
||||||
|
style="background-color: {obterPrimariaRgba(0.2)}"
|
||||||
>
|
>
|
||||||
{#if resultado.remetente?.fotoPerfilUrl}
|
{#if resultado.remetente?.fotoPerfilUrl}
|
||||||
<img
|
<img
|
||||||
@@ -840,7 +902,7 @@
|
|||||||
<div class="modal-box max-w-md" onclick={(e) => e.stopPropagation()}>
|
<div class="modal-box max-w-md" onclick={(e) => e.stopPropagation()}>
|
||||||
<div class="border-base-300 flex items-center justify-between border-b px-6 py-4">
|
<div class="border-base-300 flex items-center justify-between border-b px-6 py-4">
|
||||||
<h2 class="flex items-center gap-2 text-xl font-semibold">
|
<h2 class="flex items-center gap-2 text-xl font-semibold">
|
||||||
<Bell class="text-primary h-5 w-5" />
|
<Bell class="h-5 w-5" style="color: {coresTema.primary}" />
|
||||||
Enviar Notificação
|
Enviar Notificação
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
@@ -931,3 +993,10 @@
|
|||||||
details={errorInstructions || errorDetails}
|
details={errorInstructions || errorDetails}
|
||||||
onClose={fecharErrorModal}
|
onClose={fecharErrorModal}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Estilos para hover dinâmico com cores do tema */
|
||||||
|
[style*="--hover-bg"]:hover {
|
||||||
|
background-color: var(--hover-bg) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
type EncryptedMessage
|
type EncryptedMessage
|
||||||
} from '$lib/utils/e2eEncryption';
|
} from '$lib/utils/e2eEncryption';
|
||||||
import { obterChaveCriptografia, armazenarChaveCriptografia } from '$lib/stores/chatStore';
|
import { obterChaveCriptografia, armazenarChaveCriptografia } from '$lib/stores/chatStore';
|
||||||
|
import { obterCoresDoTema } from '$lib/utils/temas';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
conversaId: Id<'conversas'>;
|
conversaId: Id<'conversas'>;
|
||||||
@@ -89,6 +90,63 @@
|
|||||||
let selectedMentionIndex = $state(0); // Índice do participante selecionado no dropdown
|
let selectedMentionIndex = $state(0); // Índice do participante selecionado no dropdown
|
||||||
let mensagemMuitoLonga = $state(false);
|
let mensagemMuitoLonga = $state(false);
|
||||||
|
|
||||||
|
// Obter cores do tema atual (reativo)
|
||||||
|
let coresTema = $state(obterCoresDoTema());
|
||||||
|
|
||||||
|
// Atualizar cores quando o tema mudar
|
||||||
|
$effect(() => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
const atualizarCores = () => {
|
||||||
|
coresTema = obterCoresDoTema();
|
||||||
|
};
|
||||||
|
|
||||||
|
atualizarCores();
|
||||||
|
|
||||||
|
window.addEventListener('themechange', atualizarCores);
|
||||||
|
|
||||||
|
const observer = new MutationObserver(atualizarCores);
|
||||||
|
const htmlElement = document.documentElement;
|
||||||
|
observer.observe(htmlElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['data-theme']
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('themechange', atualizarCores);
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Função para obter rgba da cor primária
|
||||||
|
function obterPrimariaRgba(alpha: number = 1) {
|
||||||
|
const primary = coresTema.primary;
|
||||||
|
if (primary.startsWith('rgba')) {
|
||||||
|
const match = primary.match(/rgba?\(([^)]+)\)/);
|
||||||
|
if (match) {
|
||||||
|
const values = match[1].split(',');
|
||||||
|
return `rgba(${values[0]}, ${values[1]}, ${values[2]}, ${alpha})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (primary.startsWith('#')) {
|
||||||
|
const hex = primary.replace('#', '');
|
||||||
|
const r = parseInt(hex.substring(0, 2), 16);
|
||||||
|
const g = parseInt(hex.substring(2, 4), 16);
|
||||||
|
const b = parseInt(hex.substring(4, 6), 16);
|
||||||
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||||
|
}
|
||||||
|
if (primary.startsWith('hsl')) {
|
||||||
|
return primary.replace(/\)$/, `, ${alpha})`).replace('hsl', 'hsla');
|
||||||
|
}
|
||||||
|
return `rgba(102, 126, 234, ${alpha})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Função para obter gradiente do tema
|
||||||
|
function obterGradienteTema() {
|
||||||
|
const primary = coresTema.primary;
|
||||||
|
return `linear-gradient(135deg, ${primary} 0%, ${primary}dd 100%)`;
|
||||||
|
}
|
||||||
|
|
||||||
// Emojis mais usados
|
// Emojis mais usados
|
||||||
const emojis = [
|
const emojis = [
|
||||||
'😀',
|
'😀',
|
||||||
@@ -670,7 +728,7 @@
|
|||||||
<div class="relative shrink-0">
|
<div class="relative shrink-0">
|
||||||
<label
|
<label
|
||||||
class="group relative flex h-10 w-10 shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-xl transition-all duration-300"
|
class="group relative flex h-10 w-10 shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-xl transition-all duration-300"
|
||||||
style="background: rgba(102, 126, 234, 0.1); border: 1px solid rgba(102, 126, 234, 0.2);"
|
style="background: {obterPrimariaRgba(0.1)}; border: 1px solid {obterPrimariaRgba(0.2)};"
|
||||||
title="Anexar arquivo"
|
title="Anexar arquivo"
|
||||||
aria-label="Anexar arquivo"
|
aria-label="Anexar arquivo"
|
||||||
>
|
>
|
||||||
@@ -838,7 +896,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="group relative flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden rounded-xl transition-all duration-300 disabled:cursor-not-allowed disabled:opacity-50"
|
class="group relative flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden rounded-xl transition-all duration-300 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); box-shadow: 0 8px 24px -4px rgba(102, 126, 234, 0.4);"
|
style="background: {obterGradienteTema()}; box-shadow: 0 8px 24px -4px {obterPrimariaRgba(0.4)};"
|
||||||
onclick={handleEnviar}
|
onclick={handleEnviar}
|
||||||
disabled={!mensagem.trim() || enviando || uploadingFile}
|
disabled={!mensagem.trim() || enviando || uploadingFile}
|
||||||
aria-label="Enviar mensagem"
|
aria-label="Enviar mensagem"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { ptBR } from 'date-fns/locale';
|
import { ptBR } from 'date-fns/locale';
|
||||||
import { Clock, Trash2, X } from 'lucide-svelte';
|
import { Clock, Trash2, X } from 'lucide-svelte';
|
||||||
|
import { obterCoresDoTema } from '$lib/utils/temas';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
conversaId: Id<'conversas'>;
|
conversaId: Id<'conversas'>;
|
||||||
@@ -23,6 +24,63 @@
|
|||||||
let hora = $state('');
|
let hora = $state('');
|
||||||
let loading = $state(false);
|
let loading = $state(false);
|
||||||
|
|
||||||
|
// Obter cores do tema atual (reativo)
|
||||||
|
let coresTema = $state(obterCoresDoTema());
|
||||||
|
|
||||||
|
// Atualizar cores quando o tema mudar
|
||||||
|
$effect(() => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
const atualizarCores = () => {
|
||||||
|
coresTema = obterCoresDoTema();
|
||||||
|
};
|
||||||
|
|
||||||
|
atualizarCores();
|
||||||
|
|
||||||
|
window.addEventListener('themechange', atualizarCores);
|
||||||
|
|
||||||
|
const observer = new MutationObserver(atualizarCores);
|
||||||
|
const htmlElement = document.documentElement;
|
||||||
|
observer.observe(htmlElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['data-theme']
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('themechange', atualizarCores);
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Função para obter rgba da cor primária
|
||||||
|
function obterPrimariaRgba(alpha: number = 1) {
|
||||||
|
const primary = coresTema.primary;
|
||||||
|
if (primary.startsWith('rgba')) {
|
||||||
|
const match = primary.match(/rgba?\(([^)]+)\)/);
|
||||||
|
if (match) {
|
||||||
|
const values = match[1].split(',');
|
||||||
|
return `rgba(${values[0]}, ${values[1]}, ${values[2]}, ${alpha})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (primary.startsWith('#')) {
|
||||||
|
const hex = primary.replace('#', '');
|
||||||
|
const r = parseInt(hex.substring(0, 2), 16);
|
||||||
|
const g = parseInt(hex.substring(2, 4), 16);
|
||||||
|
const b = parseInt(hex.substring(4, 6), 16);
|
||||||
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||||
|
}
|
||||||
|
if (primary.startsWith('hsl')) {
|
||||||
|
return primary.replace(/\)$/, `, ${alpha})`).replace('hsl', 'hsla');
|
||||||
|
}
|
||||||
|
return `rgba(102, 126, 234, ${alpha})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Função para obter gradiente do tema
|
||||||
|
function obterGradienteTema() {
|
||||||
|
const primary = coresTema.primary;
|
||||||
|
return `linear-gradient(135deg, ${primary} 0%, ${primary}dd 100%)`;
|
||||||
|
}
|
||||||
|
|
||||||
// Rastrear mudanças nas mensagens agendadas
|
// Rastrear mudanças nas mensagens agendadas
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
console.log('📅 [ScheduleModal] Mensagens agendadas atualizadas:', mensagensAgendadas?.data);
|
console.log('📅 [ScheduleModal] Mensagens agendadas atualizadas:', mensagensAgendadas?.data);
|
||||||
@@ -186,7 +244,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="group relative overflow-hidden rounded-xl px-6 py-3 font-bold text-white transition-all duration-300 disabled:cursor-not-allowed disabled:opacity-50"
|
class="group relative overflow-hidden rounded-xl px-6 py-3 font-bold text-white transition-all duration-300 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); box-shadow: 0 8px 24px -4px rgba(102, 126, 234, 0.4);"
|
style="background: {obterGradienteTema()}; box-shadow: 0 8px 24px -4px {obterPrimariaRgba(0.4)};"
|
||||||
onclick={handleAgendar}
|
onclick={handleAgendar}
|
||||||
disabled={loading || !mensagem.trim() || !data || !hora}
|
disabled={loading || !mensagem.trim() || !data || !hora}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -35,6 +35,9 @@
|
|||||||
|
|
||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
|
|
||||||
|
// Estado de sincronização do relógio
|
||||||
|
let sincronizacaoConcluida = $state(false);
|
||||||
|
|
||||||
// Chave de refresh para forçar atualização das queries após registro
|
// Chave de refresh para forçar atualização das queries após registro
|
||||||
let refreshKey = $state(0);
|
let refreshKey = $state(0);
|
||||||
|
|
||||||
@@ -60,17 +63,12 @@
|
|||||||
funcionarioId && dataHoje ? { funcionarioId, data: dataHoje } : 'skip'
|
funcionarioId && dataHoje ? { funcionarioId, data: dataHoje } : 'skip'
|
||||||
);
|
);
|
||||||
|
|
||||||
const registrosHojeQuery = $derived.by(() =>
|
// Queries de ponto - usando useQuery com parâmetros derivados reativos
|
||||||
useQuery(api.pontos.listarRegistrosDia, registrosHojeParams)
|
const registrosHojeQuery = useQuery(api.pontos.listarRegistrosDia, registrosHojeParams);
|
||||||
);
|
|
||||||
|
|
||||||
const historicoSaldoQuery = $derived.by(() =>
|
const historicoSaldoQuery = useQuery(api.pontos.obterHistoricoESaldoDia, historicoSaldoParams);
|
||||||
useQuery(api.pontos.obterHistoricoESaldoDia, historicoSaldoParams)
|
|
||||||
);
|
|
||||||
|
|
||||||
const dispensaQuery = $derived.by(() =>
|
const dispensaQuery = useQuery(api.pontos.verificarDispensaAtiva, dispensaParams);
|
||||||
useQuery(api.pontos.verificarDispensaAtiva, dispensaParams)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Query para obter status atual do funcionário (férias/licença)
|
// Query para obter status atual do funcionário (férias/licença)
|
||||||
const funcionarioStatusQuery = useQuery(
|
const funcionarioStatusQuery = useQuery(
|
||||||
@@ -355,6 +353,9 @@
|
|||||||
justificativa = ''; // Limpar justificativa após registro
|
justificativa = ''; // Limpar justificativa após registro
|
||||||
mostrandoModalConfirmacao = false;
|
mostrandoModalConfirmacao = false;
|
||||||
|
|
||||||
|
// Aguardar um pouco para garantir que o backend processou o registro
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||||
|
|
||||||
// Forçar atualização das queries para mostrar o novo registro
|
// Forçar atualização das queries para mostrar o novo registro
|
||||||
refreshKey++;
|
refreshKey++;
|
||||||
|
|
||||||
@@ -362,11 +363,13 @@
|
|||||||
console.log('[RegistroPonto] Registro bem-sucedido, refreshKey incrementado:', refreshKey);
|
console.log('[RegistroPonto] Registro bem-sucedido, refreshKey incrementado:', refreshKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aguardar um pouco para garantir que o backend processou o registro
|
// Aguardar mais um pouco e forçar outra atualização para garantir sincronização completa
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
setTimeout(() => {
|
||||||
|
|
||||||
// Forçar mais uma atualização após o delay para garantir sincronização
|
|
||||||
refreshKey++;
|
refreshKey++;
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.log('[RegistroPonto] Segunda atualização, refreshKey incrementado:', refreshKey);
|
||||||
|
}
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
// Mostrar comprovante após 1 segundo
|
// Mostrar comprovante após 1 segundo
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -500,25 +503,27 @@
|
|||||||
timestampBase = Date.now();
|
timestampBase = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aplicar GMT offset ao timestamp
|
// Aplicar GMT offset ao timestamp (o horário já vem corrigido do servidor)
|
||||||
// Quando GMT é 0, usar timestamp UTC puro e deixar toLocaleTimeString() fazer a conversão automática
|
// Apenas aplicar o offset configurado, sem ajustes adicionais de timezone
|
||||||
// Quando GMT ≠ 0, aplicar offset configurado ao timestamp
|
|
||||||
let timestamp: number;
|
let timestamp: number;
|
||||||
if (gmtOffset !== 0) {
|
if (gmtOffset !== 0) {
|
||||||
// Aplicar offset configurado
|
// Aplicar offset configurado
|
||||||
timestamp = timestampBase + gmtOffset * 60 * 60 * 1000;
|
timestamp = timestampBase + gmtOffset * 60 * 60 * 1000;
|
||||||
} else {
|
} else {
|
||||||
// Quando GMT = 0, manter timestamp UTC puro
|
// Quando GMT = 0, usar timestamp base diretamente (já vem corrigido)
|
||||||
// O toLocaleTimeString() converterá automaticamente para o timezone local do navegador
|
|
||||||
timestamp = timestampBase;
|
timestamp = timestampBase;
|
||||||
}
|
}
|
||||||
|
// Usar métodos UTC diretamente para evitar conversão automática do navegador
|
||||||
|
// O timestamp já está ajustado, então formatamos como UTC para manter o valor correto
|
||||||
const dataObj = new Date(timestamp);
|
const dataObj = new Date(timestamp);
|
||||||
const data = dataObj.toLocaleDateString('pt-BR');
|
const dia = String(dataObj.getUTCDate()).padStart(2, '0');
|
||||||
const hora = dataObj.toLocaleTimeString('pt-BR', {
|
const mes = String(dataObj.getUTCMonth() + 1).padStart(2, '0');
|
||||||
hour: '2-digit',
|
const ano = dataObj.getUTCFullYear();
|
||||||
minute: '2-digit',
|
const data = `${dia}/${mes}/${ano}`;
|
||||||
second: '2-digit'
|
const horaStr = String(dataObj.getUTCHours()).padStart(2, '0');
|
||||||
});
|
const minutoStr = String(dataObj.getUTCMinutes()).padStart(2, '0');
|
||||||
|
const segundoStr = String(dataObj.getUTCSeconds()).padStart(2, '0');
|
||||||
|
const hora = `${horaStr}:${minutoStr}:${segundoStr}`;
|
||||||
dataHoraAtual = { data, hora };
|
dataHoraAtual = { data, hora };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Erro ao obter tempo do servidor, usando tempo local:', error);
|
console.warn('Erro ao obter tempo do servidor, usando tempo local:', error);
|
||||||
@@ -866,7 +871,8 @@
|
|||||||
!estaDispensado &&
|
!estaDispensado &&
|
||||||
!emFerias &&
|
!emFerias &&
|
||||||
!emLicenca &&
|
!emLicenca &&
|
||||||
temFuncionarioAssociado
|
temFuncionarioAssociado &&
|
||||||
|
sincronizacaoConcluida // Só permitir registro após sincronização concluída
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1131,16 +1137,32 @@
|
|||||||
id="relogio-sincronizado-ref"
|
id="relogio-sincronizado-ref"
|
||||||
class="card from-primary/10 to-primary/5 border-primary/20 w-full max-w-sm rounded-2xl border-2 bg-linear-to-br p-5 shadow-lg"
|
class="card from-primary/10 to-primary/5 border-primary/20 w-full max-w-sm rounded-2xl border-2 bg-linear-to-br p-5 shadow-lg"
|
||||||
>
|
>
|
||||||
<RelogioSincronizado />
|
<RelogioSincronizado bind:sincronizacaoConcluida />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Mensagem de Aguarde Sincronização -->
|
||||||
|
{#if !sincronizacaoConcluida}
|
||||||
|
<div class="alert alert-info mb-5 rounded-xl shadow-lg">
|
||||||
|
<span class="loading loading-spinner loading-sm text-info"></span>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-bold">Aguarde a sincronização</h3>
|
||||||
|
<div class="text-sm">
|
||||||
|
O sistema está sincronizando o horário com o servidor. O botão de registro será habilitado
|
||||||
|
após a conclusão da sincronização.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Botão de Registro -->
|
<!-- Botão de Registro -->
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary mb-5 w-full gap-2 rounded-xl font-semibold shadow-lg transition-all duration-300 hover:shadow-xl"
|
class="btn btn-primary mb-5 w-full gap-2 rounded-xl font-semibold shadow-lg transition-all duration-300 hover:shadow-xl"
|
||||||
onclick={iniciarRegistroComFoto}
|
onclick={iniciarRegistroComFoto}
|
||||||
disabled={!podeRegistrar}
|
disabled={!podeRegistrar}
|
||||||
title={!temFuncionarioAssociado
|
title={!sincronizacaoConcluida
|
||||||
|
? 'Aguarde a sincronização do horário com o servidor'
|
||||||
|
: !temFuncionarioAssociado
|
||||||
? 'Você não possui funcionário associado à sua conta'
|
? 'Você não possui funcionário associado à sua conta'
|
||||||
: estaDispensado
|
: estaDispensado
|
||||||
? 'Você está dispensado de registrar ponto no momento'
|
? 'Você está dispensado de registrar ponto no momento'
|
||||||
@@ -1157,6 +1179,9 @@
|
|||||||
{:else}
|
{:else}
|
||||||
Registrando...
|
Registrando...
|
||||||
{/if}
|
{/if}
|
||||||
|
{:else if !sincronizacaoConcluida}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
Aguardando Sincronização
|
||||||
{:else if !temFuncionarioAssociado}
|
{:else if !temFuncionarioAssociado}
|
||||||
<XCircle class="h-5 w-5" />
|
<XCircle class="h-5 w-5" />
|
||||||
Funcionário Não Associado
|
Funcionário Não Associado
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
|
|
||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
|
|
||||||
|
// Expor estados para o componente pai usando $props() do Svelte 5
|
||||||
|
let { sincronizacaoConcluida = $bindable(false) }: { sincronizacaoConcluida: boolean } = $props();
|
||||||
|
|
||||||
let tempoAtual = $state<Date>(new Date());
|
let tempoAtual = $state<Date>(new Date());
|
||||||
let sincronizado = $state(false);
|
let sincronizado = $state(false);
|
||||||
let sincronizando = $state(false);
|
let sincronizando = $state(false);
|
||||||
@@ -16,6 +19,7 @@
|
|||||||
let intervalId: ReturnType<typeof setInterval> | null = null;
|
let intervalId: ReturnType<typeof setInterval> | null = null;
|
||||||
let intervaloSincronizacao: ReturnType<typeof setInterval> | null = null;
|
let intervaloSincronizacao: ReturnType<typeof setInterval> | null = null;
|
||||||
let sincronizacaoEmAndamento = $state(false); // Flag para evitar múltiplas sincronizações simultâneas
|
let sincronizacaoEmAndamento = $state(false); // Flag para evitar múltiplas sincronizações simultâneas
|
||||||
|
let sincronizacaoInicialConcluida = $state(false); // Flag para indicar que a primeira sincronização foi concluída
|
||||||
|
|
||||||
async function atualizarTempo() {
|
async function atualizarTempo() {
|
||||||
// Evitar múltiplas sincronizações simultâneas
|
// Evitar múltiplas sincronizações simultâneas
|
||||||
@@ -92,6 +96,11 @@
|
|||||||
} finally {
|
} finally {
|
||||||
sincronizando = false;
|
sincronizando = false;
|
||||||
sincronizacaoEmAndamento = false;
|
sincronizacaoEmAndamento = false;
|
||||||
|
// Marcar sincronização inicial como concluída após a primeira tentativa
|
||||||
|
if (!sincronizacaoInicialConcluida) {
|
||||||
|
sincronizacaoInicialConcluida = true;
|
||||||
|
sincronizacaoConcluida = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +115,7 @@
|
|||||||
tempoAtual = new Date(obterTempoPC());
|
tempoAtual = new Date(obterTempoPC());
|
||||||
sincronizado = false;
|
sincronizado = false;
|
||||||
erro = 'Usando servidor interno';
|
erro = 'Usando servidor interno';
|
||||||
|
sincronizacaoConcluida = false; // Garantir que começa como false
|
||||||
// Atualizar display a cada segundo
|
// Atualizar display a cada segundo
|
||||||
intervalId = setInterval(atualizarRelogio, 1000);
|
intervalId = setInterval(atualizarRelogio, 1000);
|
||||||
// Sincronizar em background (não bloquear) após um pequeno delay para garantir que a UI está renderizada
|
// Sincronizar em background (não bloquear) após um pequeno delay para garantir que a UI está renderizada
|
||||||
|
|||||||
@@ -216,14 +216,14 @@ function gerarTabelaRegistrosPDF(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Função auxiliar para obter ícone do tipo de dia
|
// Função auxiliar para obter símbolo do tipo de dia
|
||||||
const obterIconeTipoDia = (dia: DiaFichaPonto): string => {
|
const obterSimboloTipoDia = (dia: DiaFichaPonto): string => {
|
||||||
if (dia.atestado) return '🏥';
|
if (dia.atestado) return 'AT';
|
||||||
if (dia.ausencia) return '🚫';
|
if (dia.ausencia) return 'AUS';
|
||||||
if (dia.licenca) return '📋';
|
if (dia.licenca) return 'LIC';
|
||||||
if (dia.tipoDia === 'abonado') return '✅';
|
if (dia.tipoDia === 'abonado') return 'ABO';
|
||||||
if (dia.tipoDia === 'nao_computado') return '⏸';
|
if (dia.tipoDia === 'nao_computado') return 'NC';
|
||||||
if (dia.inconsistencias.length > 0) return '⚠';
|
if (dia.inconsistencias.length > 0) return 'INC';
|
||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -258,8 +258,10 @@ function gerarTabelaRegistrosPDF(
|
|||||||
|
|
||||||
// Coluna Data (apenas na primeira linha)
|
// Coluna Data (apenas na primeira linha)
|
||||||
if (i === 0) {
|
if (i === 0) {
|
||||||
|
const simbolo = obterSimboloTipoDia(dia);
|
||||||
|
const dataComSimbolo = simbolo ? `${dataFormatada} [${simbolo}]` : dataFormatada;
|
||||||
linha.push({
|
linha.push({
|
||||||
content: `${dataFormatada} ${obterIconeTipoDia(dia)}`,
|
content: dataComSimbolo,
|
||||||
styles: {
|
styles: {
|
||||||
fillColor: obterCorFundoTipoDia(dia.tipoDia),
|
fillColor: obterCorFundoTipoDia(dia.tipoDia),
|
||||||
fontStyle: 'bold'
|
fontStyle: 'bold'
|
||||||
@@ -539,20 +541,48 @@ function gerarSecaoAjustesPDF(doc: jsPDF, yPosition: number, dias: DiaFichaPonto
|
|||||||
doc.setTextColor(0, 0, 0);
|
doc.setTextColor(0, 0, 0);
|
||||||
yPosition += 10;
|
yPosition += 10;
|
||||||
|
|
||||||
|
// Função auxiliar para formatar período do ajuste
|
||||||
|
const formatarPeriodoAjuste = (ajuste: (typeof todosAjustes)[number]): string => {
|
||||||
|
if (
|
||||||
|
ajuste.dataInicio &&
|
||||||
|
ajuste.horaInicio !== undefined &&
|
||||||
|
ajuste.minutoInicio !== undefined &&
|
||||||
|
ajuste.dataFim &&
|
||||||
|
ajuste.horaFim !== undefined &&
|
||||||
|
ajuste.minutoFim !== undefined
|
||||||
|
) {
|
||||||
|
const inicioStr = `${formatarDataDDMMAAAA(ajuste.dataInicio)} ${formatarHoraPonto(
|
||||||
|
ajuste.horaInicio,
|
||||||
|
ajuste.minutoInicio
|
||||||
|
)}`;
|
||||||
|
const fimStr = `${formatarDataDDMMAAAA(ajuste.dataFim)} ${formatarHoraPonto(
|
||||||
|
ajuste.horaFim,
|
||||||
|
ajuste.minutoFim
|
||||||
|
)}`;
|
||||||
|
return `${inicioStr} a ${fimStr}`;
|
||||||
|
}
|
||||||
|
// Fallback para ajustes antigos sem período
|
||||||
|
return formatarDataDDMMAAAA(ajuste.data);
|
||||||
|
};
|
||||||
|
|
||||||
const ajustesData = todosAjustes.map((ajuste) => [
|
const ajustesData = todosAjustes.map((ajuste) => [
|
||||||
formatarDataDDMMAAAA(ajuste.data),
|
formatarDataDDMMAAAA(ajuste.data),
|
||||||
ajuste.tipo === 'abonar' ? 'Abonar' : ajuste.tipo === 'descontar' ? 'Descontar' : 'Compensar',
|
ajuste.tipo === 'abonar' ? 'Abonar' : ajuste.tipo === 'descontar' ? 'Descontar' : 'Compensar',
|
||||||
formatarMinutos(ajuste.valorMinutos),
|
formatarMinutos(ajuste.valorMinutos),
|
||||||
|
formatarPeriodoAjuste(ajuste),
|
||||||
ajuste.motivoDescricao || '-'
|
ajuste.motivoDescricao || '-'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
autoTable(doc, {
|
autoTable(doc, {
|
||||||
startY: yPosition,
|
startY: yPosition,
|
||||||
head: [['Data', 'Tipo', 'Valor', 'Motivo']],
|
head: [['Data Aplicação', 'Tipo', 'Valor', 'Período', 'Motivo']],
|
||||||
body: ajustesData,
|
body: ajustesData,
|
||||||
theme: 'grid',
|
theme: 'grid',
|
||||||
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
|
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
|
||||||
styles: { fontSize: 9 }
|
styles: { fontSize: 9 },
|
||||||
|
columnStyles: {
|
||||||
|
3: { cellWidth: 'auto', minCellWidth: 60 } // Coluna de período com largura maior
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
type JsPDFWithAutoTable = jsPDF & {
|
type JsPDFWithAutoTable = jsPDF & {
|
||||||
|
|||||||
@@ -598,7 +598,13 @@ export async function processarDadosFichaPonto(
|
|||||||
tipo: a.tipo,
|
tipo: a.tipo,
|
||||||
valorMinutos: a.valorMinutos,
|
valorMinutos: a.valorMinutos,
|
||||||
motivoDescricao: a.motivoDescricao,
|
motivoDescricao: a.motivoDescricao,
|
||||||
gestorId: a.gestorId
|
gestorId: a.gestorId,
|
||||||
|
dataInicio: a.dataInicio,
|
||||||
|
horaInicio: a.horaInicio,
|
||||||
|
minutoInicio: a.minutoInicio,
|
||||||
|
dataFim: a.dataFim,
|
||||||
|
horaFim: a.horaFim,
|
||||||
|
minutoFim: a.minutoFim
|
||||||
})),
|
})),
|
||||||
inconsistencias: inconsistenciasDia.map((i) => ({
|
inconsistencias: inconsistenciasDia.map((i) => ({
|
||||||
_id: i._id,
|
_id: i._id,
|
||||||
|
|||||||
@@ -60,6 +60,13 @@ export interface DiaFichaPonto {
|
|||||||
valorMinutos: number;
|
valorMinutos: number;
|
||||||
motivoDescricao?: string;
|
motivoDescricao?: string;
|
||||||
gestorId?: Id<'usuarios'>;
|
gestorId?: Id<'usuarios'>;
|
||||||
|
// Período do ajuste
|
||||||
|
dataInicio?: string; // YYYY-MM-DD
|
||||||
|
horaInicio?: number; // 0-23
|
||||||
|
minutoInicio?: number; // 0-59
|
||||||
|
dataFim?: string; // YYYY-MM-DD
|
||||||
|
horaFim?: number; // 0-23
|
||||||
|
minutoFim?: number; // 0-59
|
||||||
}>;
|
}>;
|
||||||
inconsistencias: Array<{
|
inconsistencias: Array<{
|
||||||
_id: Id<'inconsistenciasBancoHoras'>;
|
_id: Id<'inconsistenciasBancoHoras'>;
|
||||||
|
|||||||
@@ -1,20 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
|
||||||
import { useQuery } from 'convex-svelte';
|
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { afterNavigate, goto, replaceState } from '$app/navigation';
|
import { afterNavigate, goto, replaceState } from '$app/navigation';
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
import { UserPlus, Mail, Clock, Award, TrendingUp, Zap, Users, Database } from 'lucide-svelte';
|
import { UserPlus, Mail, Users, Clock, Calendar, BadgeCheck, Package } from 'lucide-svelte';
|
||||||
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
|
import { useQuery } from 'convex-svelte';
|
||||||
import ProtectedRoute from '$lib/components/ProtectedRoute.svelte';
|
import ProtectedRoute from '$lib/components/ProtectedRoute.svelte';
|
||||||
|
|
||||||
// Queries para dados do dashboard
|
// Queries para dados do dashboard
|
||||||
const statsQuery = useQuery(api.dashboard.getStats, {});
|
const statsQuery = useQuery(api.dashboard.getStats, {});
|
||||||
const activityQuery = useQuery(api.dashboard.getRecentActivity, {});
|
|
||||||
|
|
||||||
// Queries para monitoramento em tempo real
|
|
||||||
const statusSistemaQuery = useQuery(api.monitoramento.getStatusSistema, {});
|
|
||||||
const atividadeBDQuery = useQuery(api.monitoramento.getAtividadeBancoDados, {});
|
|
||||||
const distribuicaoQuery = useQuery(api.monitoramento.getDistribuicaoRequisicoes, {});
|
|
||||||
|
|
||||||
// Estado para animações
|
// Estado para animações
|
||||||
let currentTime = $state(new Date());
|
let currentTime = $state(new Date());
|
||||||
@@ -36,6 +30,7 @@
|
|||||||
// Se for erro de autenticação, abrir modal de login automaticamente
|
// Se for erro de autenticação, abrir modal de login automaticamente
|
||||||
if (error === 'auth_required') {
|
if (error === 'auth_required') {
|
||||||
const redirectTo = route || to.url.pathname;
|
const redirectTo = route || to.url.pathname;
|
||||||
|
// eslint-disable-next-line svelte/no-navigation-without-resolve
|
||||||
goto(`${resolve('/login')}?redirect=${encodeURIComponent(redirectTo)}`, {
|
goto(`${resolve('/login')}?redirect=${encodeURIComponent(redirectTo)}`, {
|
||||||
replaceState: true,
|
replaceState: true,
|
||||||
noScroll: true
|
noScroll: true
|
||||||
@@ -70,6 +65,7 @@
|
|||||||
|
|
||||||
if (error === 'auth_required') {
|
if (error === 'auth_required') {
|
||||||
const redirectTo = route || window.location.pathname;
|
const redirectTo = route || window.location.pathname;
|
||||||
|
// eslint-disable-next-line svelte/no-navigation-without-resolve
|
||||||
goto(`${resolve('/login')}?redirect=${encodeURIComponent(redirectTo)}`, {
|
goto(`${resolve('/login')}?redirect=${encodeURIComponent(redirectTo)}`, {
|
||||||
replaceState: true,
|
replaceState: true,
|
||||||
noScroll: true
|
noScroll: true
|
||||||
@@ -119,17 +115,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Função para formatar números
|
|
||||||
function formatNumber(num: number): string {
|
|
||||||
return new Intl.NumberFormat('pt-BR').format(num);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Função para calcular porcentagem
|
|
||||||
function calcPercentage(value: number, total: number): number {
|
|
||||||
if (total === 0) return 0;
|
|
||||||
return Math.round((value / total) * 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obter saudação baseada na hora
|
// Obter saudação baseada na hora
|
||||||
function getSaudacao(): string {
|
function getSaudacao(): string {
|
||||||
const hora = currentTime.getHours();
|
const hora = currentTime.getHours();
|
||||||
@@ -137,6 +122,43 @@
|
|||||||
if (hora < 18) return 'Boa tarde';
|
if (hora < 18) return 'Boa tarde';
|
||||||
return 'Boa noite';
|
return 'Boa noite';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Função para animar contador numérico
|
||||||
|
function animateCounter(element: HTMLElement, target: number, duration: number = 2000): void {
|
||||||
|
const start = 0;
|
||||||
|
const increment = target / (duration / 16);
|
||||||
|
let current = start;
|
||||||
|
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
current += increment;
|
||||||
|
if (current >= target) {
|
||||||
|
element.textContent = target.toLocaleString('pt-BR');
|
||||||
|
clearInterval(timer);
|
||||||
|
} else {
|
||||||
|
element.textContent = Math.floor(current).toLocaleString('pt-BR');
|
||||||
|
}
|
||||||
|
}, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action para animar contador
|
||||||
|
function animateCounterAction(node: HTMLElement, value: number) {
|
||||||
|
if (value > 0 && value !== undefined) {
|
||||||
|
// Pequeno delay para garantir que o elemento está renderizado
|
||||||
|
setTimeout(() => {
|
||||||
|
animateCounter(node, value);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
update(newValue: number) {
|
||||||
|
if (newValue > 0 && newValue !== undefined && newValue !== value) {
|
||||||
|
node.textContent = '0';
|
||||||
|
setTimeout(() => {
|
||||||
|
animateCounter(node, newValue);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
@@ -176,17 +198,23 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Cabeçalho com Boas-vindas -->
|
<!-- Hero Section com Boas-vindas -->
|
||||||
<div class="from-primary/20 to-secondary/20 mb-6 rounded-2xl bg-linear-to-r p-8 shadow-lg">
|
<div
|
||||||
|
class="fade-in from-primary/20 to-secondary/20 mb-8 rounded-2xl bg-linear-to-r p-8 shadow-xl"
|
||||||
|
>
|
||||||
<div class="flex flex-col items-start justify-between gap-4 md:flex-row md:items-center">
|
<div class="flex flex-col items-start justify-between gap-4 md:flex-row md:items-center">
|
||||||
<div>
|
<div class="flex-1">
|
||||||
<h1 class="text-primary mb-2 text-4xl font-bold">
|
<h1 class="text-primary mb-3 text-5xl font-bold">
|
||||||
{getSaudacao()}! 👋
|
{getSaudacao()}! 👋
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-base-content/80 text-xl">
|
<p class="text-base-content/80 mb-3 text-2xl font-semibold">Bem-vindo ao SGSE</p>
|
||||||
Bem-vindo ao SGSE - Sistema de Gerenciamento de Secretaria
|
<p
|
||||||
|
class="from-primary to-secondary mb-4 bg-gradient-to-r bg-clip-text text-3xl font-bold text-transparent"
|
||||||
|
>
|
||||||
|
Simplificando a Gestão Pública
|
||||||
</p>
|
</p>
|
||||||
<p class="text-base-content/60 mt-2 text-sm">
|
<p class="text-base-content/70 text-lg">Sistema de Gerenciamento de Secretaria</p>
|
||||||
|
<p class="text-base-content/60 mt-3 text-sm">
|
||||||
{currentTime.toLocaleDateString('pt-BR', {
|
{currentTime.toLocaleDateString('pt-BR', {
|
||||||
weekday: 'long',
|
weekday: 'long',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
@@ -197,518 +225,259 @@
|
|||||||
{currentTime.toLocaleTimeString('pt-BR')}
|
{currentTime.toLocaleTimeString('pt-BR')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex flex-col gap-3">
|
||||||
<div class="badge badge-primary badge-lg">Sistema Online</div>
|
<div class="badge badge-primary badge-lg animate-pulse">Sistema Online</div>
|
||||||
<div class="badge badge-success badge-lg">Atualizado</div>
|
<div class="badge badge-success badge-lg">Atualizado</div>
|
||||||
|
<div class="badge badge-info badge-lg">Disponível 24h</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Cards de Estatísticas Principais -->
|
<!-- Seção de Estatísticas -->
|
||||||
{#if statsQuery.isLoading}
|
{#if statsQuery.isLoading}
|
||||||
<div class="flex items-center justify-center py-12">
|
<div class="flex items-center justify-center py-12">
|
||||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||||
</div>
|
</div>
|
||||||
{:else if statsQuery.data}
|
{:else if statsQuery.data}
|
||||||
<div class="mb-6 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
|
<div class="mb-8 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||||
<!-- Total de Funcionários -->
|
<!-- Card Usuários Cadastrados -->
|
||||||
<div
|
<div
|
||||||
class="card transform bg-linear-to-br from-blue-500/10 to-blue-600/20 shadow-xl transition-all duration-300 hover:-translate-y-1 hover:shadow-2xl"
|
class="stats-card card transform bg-linear-to-br from-blue-500/20 to-blue-600/30 shadow-xl transition-all duration-300 hover:scale-105 hover:shadow-2xl"
|
||||||
>
|
>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div class="flex-1">
|
||||||
<p class="text-base-content/70 text-sm font-semibold">Total de Funcionários</p>
|
<p class="text-base-content/70 mb-2 text-sm font-semibold uppercase">
|
||||||
<h2 class="text-primary mt-2 text-4xl font-bold">
|
Usuários Cadastrados
|
||||||
{formatNumber(statsQuery.data.totalFuncionarios)}
|
|
||||||
</h2>
|
|
||||||
<p class="text-base-content/60 mt-1 text-xs">
|
|
||||||
{statsQuery.data.funcionariosAtivos} ativos
|
|
||||||
</p>
|
</p>
|
||||||
|
<h2 class="text-primary text-4xl font-bold">
|
||||||
|
{#if statsQuery.data}
|
||||||
|
<span use:animateCounterAction={statsQuery.data.totalUsuarios}>0</span>
|
||||||
|
{:else}
|
||||||
|
0
|
||||||
|
{/if}
|
||||||
|
</h2>
|
||||||
|
<p class="text-base-content/60 mt-2 text-xs">no sistema</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="bg-primary/20 rounded-full p-4">
|
||||||
class="radial-progress text-primary"
|
<Users class="text-primary h-8 w-8" strokeWidth={2} />
|
||||||
style="--value:{calcPercentage(
|
|
||||||
statsQuery.data.funcionariosAtivos,
|
|
||||||
statsQuery.data.totalFuncionarios
|
|
||||||
)}; --size:4rem;"
|
|
||||||
>
|
|
||||||
<span class="text-xs font-bold"
|
|
||||||
>{calcPercentage(
|
|
||||||
statsQuery.data.funcionariosAtivos,
|
|
||||||
statsQuery.data.totalFuncionarios
|
|
||||||
)}%</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Solicitações Pendentes -->
|
<!-- Card Funcionários Ativos -->
|
||||||
<div
|
<div
|
||||||
class="card transform bg-linear-to-br from-yellow-500/10 to-yellow-600/20 shadow-xl transition-all duration-300 hover:-translate-y-1 hover:shadow-2xl"
|
class="stats-card card transform bg-linear-to-br from-green-500/20 to-green-600/30 shadow-xl transition-all duration-300 hover:scale-105 hover:shadow-2xl"
|
||||||
>
|
>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div class="flex-1">
|
||||||
<p class="text-base-content/70 text-sm font-semibold">Solicitações Pendentes</p>
|
<p class="text-base-content/70 mb-2 text-sm font-semibold uppercase">
|
||||||
<h2 class="text-warning mt-2 text-4xl font-bold">4</h2>
|
Funcionários Ativos
|
||||||
<p class="text-base-content/60 mt-1 text-xs">de 5 total</p>
|
</p>
|
||||||
|
<h2 class="text-success text-4xl font-bold">
|
||||||
|
{#if statsQuery.data}
|
||||||
|
<span use:animateCounterAction={statsQuery.data.funcionariosAtivos}>0</span>
|
||||||
|
{:else}
|
||||||
|
0
|
||||||
|
{/if}
|
||||||
|
</h2>
|
||||||
|
<p class="text-base-content/60 mt-2 text-xs">em atividade</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-warning/20 rounded-full p-4">
|
<div class="bg-success/20 rounded-full p-4">
|
||||||
|
<Users class="text-success h-8 w-8" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Card Cadastros Realizados -->
|
||||||
|
<div
|
||||||
|
class="stats-card card transform bg-linear-to-br from-purple-500/20 to-purple-600/30 shadow-xl transition-all duration-300 hover:scale-105 hover:shadow-2xl"
|
||||||
|
>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-base-content/70 mb-2 text-sm font-semibold uppercase">
|
||||||
|
Cadastros Realizados
|
||||||
|
</p>
|
||||||
|
<h2 class="text-secondary text-4xl font-bold">
|
||||||
|
{#if statsQuery.data}
|
||||||
|
<span use:animateCounterAction={statsQuery.data.totalCadastros}>0</span>
|
||||||
|
{:else}
|
||||||
|
0
|
||||||
|
{/if}
|
||||||
|
</h2>
|
||||||
|
<p class="text-base-content/60 mt-2 text-xs">total de registros</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-secondary/20 rounded-full p-4">
|
||||||
|
<BadgeCheck class="text-secondary h-8 w-8" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Card Disponibilidade 24h -->
|
||||||
|
<div
|
||||||
|
class="stats-card card transform bg-linear-to-br from-orange-500/20 to-orange-600/30 shadow-xl transition-all duration-300 hover:scale-105 hover:shadow-2xl"
|
||||||
|
>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-base-content/70 mb-2 text-sm font-semibold uppercase">
|
||||||
|
Disponibilidade
|
||||||
|
</p>
|
||||||
|
<h2 class="text-warning text-4xl font-bold">24h</h2>
|
||||||
|
<p class="text-base-content/60 mt-2 text-xs">funcionando continuamente</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-warning/20 animate-pulse rounded-full p-4">
|
||||||
<Clock class="text-warning h-8 w-8" strokeWidth={2} />
|
<Clock class="text-warning h-8 w-8" strokeWidth={2} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Símbolos Cadastrados -->
|
|
||||||
<div
|
|
||||||
class="card transform bg-linear-to-br from-green-500/10 to-green-600/20 shadow-xl transition-all duration-300 hover:-translate-y-1 hover:shadow-2xl"
|
|
||||||
>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p class="text-base-content/70 text-sm font-semibold">Símbolos Cadastrados</p>
|
|
||||||
<h2 class="text-success mt-2 text-4xl font-bold">
|
|
||||||
{formatNumber(statsQuery.data.totalSimbolos)}
|
|
||||||
</h2>
|
|
||||||
<p class="text-base-content/60 mt-1 text-xs">
|
|
||||||
{statsQuery.data.cargoComissionado} CC / {statsQuery.data.funcaoGratificada} FG
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="bg-success/20 rounded-full p-4">
|
|
||||||
<Award class="text-success h-8 w-8" strokeWidth={2} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Atividade 24h -->
|
|
||||||
{#if activityQuery.data}
|
|
||||||
<div
|
|
||||||
class="card transform bg-linear-to-br from-purple-500/10 to-purple-600/20 shadow-xl transition-all duration-300 hover:-translate-y-1 hover:shadow-2xl"
|
|
||||||
>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p class="text-base-content/70 text-sm font-semibold">Atividade (24h)</p>
|
|
||||||
<p class="text-base-content/60 mt-1 text-xs">
|
|
||||||
{activityQuery.data.funcionariosCadastrados24h} cadastros
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="bg-secondary/20 rounded-full p-4">
|
|
||||||
<TrendingUp class="text-secondary h-8 w-8" strokeWidth={2} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Monitoramento em Tempo Real -->
|
<!-- Seção Sobre o SGSE -->
|
||||||
{#if statusSistemaQuery?.data}
|
|
||||||
{@const status = statusSistemaQuery.data}
|
|
||||||
{@const atividade = atividadeBDQuery?.data || {
|
|
||||||
historico: Array.from({ length: 30 }, () => ({ entradas: 0, saidas: 0 }))
|
|
||||||
}}
|
|
||||||
{@const distribuicao = distribuicaoQuery?.data || {
|
|
||||||
queries: 0,
|
|
||||||
mutations: 0,
|
|
||||||
leituras: 0,
|
|
||||||
escritas: 0
|
|
||||||
}}
|
|
||||||
{@const maxAtividade =
|
|
||||||
atividade.historico && atividade.historico.length > 0
|
|
||||||
? Math.max(
|
|
||||||
1,
|
|
||||||
...atividade.historico.map((p) => Math.max(p.entradas || 0, p.saidas || 0))
|
|
||||||
)
|
|
||||||
: 1}
|
|
||||||
|
|
||||||
<div class="mb-6">
|
|
||||||
<div class="mb-4 flex items-center gap-3">
|
|
||||||
<div class="bg-error/10 animate-pulse rounded-lg p-2">
|
|
||||||
<Zap class="text-error h-6 w-6" strokeWidth={2} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 class="text-base-content text-2xl font-bold">Monitoramento em Tempo Real</h2>
|
|
||||||
<p class="text-base-content/60 text-sm">
|
|
||||||
Atualizado a cada segundo • {new Date(status.ultimaAtualizacao).toLocaleTimeString(
|
|
||||||
'pt-BR'
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="badge badge-error badge-lg ml-auto gap-2">
|
|
||||||
<span
|
|
||||||
class="bg-error absolute inline-flex h-3 w-3 animate-ping rounded-full opacity-75"
|
|
||||||
></span>
|
|
||||||
<span class="bg-error relative inline-flex h-3 w-3 rounded-full"></span>
|
|
||||||
LIVE
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Cards de Status do Sistema -->
|
|
||||||
<div class="mb-6 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
||||||
<!-- Usuários Online -->
|
|
||||||
<div
|
<div
|
||||||
class="card from-primary/10 to-primary/5 border-primary/20 border-2 bg-linear-to-br shadow-lg"
|
class="fade-in-delay from-base-200 to-base-300 mb-8 rounded-2xl bg-linear-to-br p-8 shadow-xl"
|
||||||
>
|
>
|
||||||
<div class="card-body p-4">
|
<div class="mx-auto max-w-4xl text-center">
|
||||||
<div class="flex items-center justify-between">
|
<h2 class="text-base-content mb-4 text-3xl font-bold">Sobre o SGSE</h2>
|
||||||
<div>
|
<p
|
||||||
<p class="text-base-content/70 text-xs font-semibold uppercase">
|
class="from-primary to-secondary mb-6 bg-gradient-to-r bg-clip-text text-2xl font-bold text-transparent"
|
||||||
Usuários Online
|
>
|
||||||
|
Simplificando a Gestão Pública
|
||||||
|
</p>
|
||||||
|
<p class="text-base-content/80 mb-4 text-lg leading-relaxed">
|
||||||
|
O Sistema de Gerenciamento de Secretaria (SGSE) é uma solução completa e moderna
|
||||||
|
desenvolvida para otimizar e simplificar os processos administrativos da gestão pública.
|
||||||
|
Com tecnologia de ponta e interface intuitiva, oferecemos rapidez, comodidade e
|
||||||
|
disponibilidade 24 horas por dia para atender às necessidades dos nossos usuários.
|
||||||
|
</p>
|
||||||
|
<p class="text-base-content/70 text-base leading-relaxed">
|
||||||
|
Nossa plataforma integra todas as funcionalidades essenciais em um único ambiente,
|
||||||
|
permitindo gestão eficiente de funcionários, controle de ponto, férias, licenças, símbolos
|
||||||
|
e muito mais. Trabalhamos continuamente para garantir que você tenha acesso rápido e
|
||||||
|
seguro a todas as informações e ferramentas necessárias para uma gestão pública de
|
||||||
|
excelência.
|
||||||
</p>
|
</p>
|
||||||
<h3 class="text-primary mt-1 text-3xl font-bold">
|
|
||||||
{status.usuariosOnline}
|
|
||||||
</h3>
|
|
||||||
<p class="text-base-content/60 mt-1 text-xs">sessões ativas</p>
|
|
||||||
</div>
|
|
||||||
<div class="bg-primary/20 rounded-full p-3">
|
|
||||||
<Users class="text-primary h-6 w-6" strokeWidth={2} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Total de Registros -->
|
<!-- Grid de Funcionalidades Principais -->
|
||||||
<div
|
<div class="fade-in-delay-2 mb-8">
|
||||||
class="card from-success/10 to-success/5 border-success/20 border-2 bg-linear-to-br shadow-lg"
|
<h2 class="text-base-content mb-6 text-center text-3xl font-bold">
|
||||||
|
Principais Funcionalidades
|
||||||
|
</h2>
|
||||||
|
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<!-- Gestão de Funcionários -->
|
||||||
|
<a
|
||||||
|
href={resolve('/recursos-humanos/funcionarios')}
|
||||||
|
class="feature-card group card transform bg-linear-to-br from-blue-500/10 to-blue-600/20 shadow-lg transition-all duration-300 hover:-translate-y-2 hover:shadow-2xl"
|
||||||
>
|
>
|
||||||
<div class="card-body p-4">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p class="text-base-content/70 text-xs font-semibold uppercase">
|
|
||||||
Total Registros
|
|
||||||
</p>
|
|
||||||
<h3 class="text-success mt-1 text-3xl font-bold">
|
|
||||||
{status.totalRegistros.toLocaleString('pt-BR')}
|
|
||||||
</h3>
|
|
||||||
<p class="text-base-content/60 mt-1 text-xs">no banco de dados</p>
|
|
||||||
</div>
|
|
||||||
<div class="bg-success/20 rounded-full p-3">
|
|
||||||
<Database class="text-success h-6 w-6" strokeWidth={2} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tempo Médio de Resposta -->
|
|
||||||
<div
|
|
||||||
class="card from-info/10 to-info/5 border-info/20 border-2 bg-linear-to-br shadow-lg"
|
|
||||||
>
|
|
||||||
<div class="card-body p-4">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p class="text-base-content/70 text-xs font-semibold uppercase">
|
|
||||||
Tempo Resposta
|
|
||||||
</p>
|
|
||||||
<h3 class="text-info mt-1 text-3xl font-bold">
|
|
||||||
{status.tempoMedioResposta}ms
|
|
||||||
</h3>
|
|
||||||
<p class="text-base-content/60 mt-1 text-xs">média atual</p>
|
|
||||||
</div>
|
|
||||||
<div class="bg-info/20 rounded-full p-3">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="text-info h-6 w-6"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M13 10V3L4 14h7v7l9-11h-7z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Uso de Sistema -->
|
|
||||||
<div
|
|
||||||
class="card from-warning/10 to-warning/5 border-warning/20 border-2 bg-linear-to-br shadow-lg"
|
|
||||||
>
|
|
||||||
<div class="card-body p-4">
|
|
||||||
<div>
|
|
||||||
<p class="text-base-content/70 mb-2 text-xs font-semibold uppercase">
|
|
||||||
Uso do Sistema
|
|
||||||
</p>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div>
|
|
||||||
<div class="mb-1 flex justify-between text-xs">
|
|
||||||
<span class="text-base-content/70">CPU</span>
|
|
||||||
<span class="text-warning font-bold">{status.cpuUsada}%</span>
|
|
||||||
</div>
|
|
||||||
<progress
|
|
||||||
class="progress progress-warning w-full"
|
|
||||||
value={status.cpuUsada}
|
|
||||||
max="100"
|
|
||||||
></progress>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="mb-1 flex justify-between text-xs">
|
|
||||||
<span class="text-base-content/70">Memória</span>
|
|
||||||
<span class="text-warning font-bold">{status.memoriaUsada}%</span>
|
|
||||||
</div>
|
|
||||||
<progress
|
|
||||||
class="progress progress-warning w-full"
|
|
||||||
value={status.memoriaUsada}
|
|
||||||
max="100"
|
|
||||||
></progress>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Gráfico de Atividade do Banco de Dados em Tempo Real -->
|
|
||||||
<div class="card bg-base-100 mb-6 shadow-xl">
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="mb-4 flex items-center justify-between">
|
<div class="mb-4 flex items-center justify-between">
|
||||||
<div>
|
<div
|
||||||
<h3 class="text-base-content text-xl font-bold">Atividade do Banco de Dados</h3>
|
class="rounded-xl bg-blue-500/20 p-4 transition-colors duration-300 group-hover:bg-blue-500/30"
|
||||||
<p class="text-base-content/60 text-sm">
|
>
|
||||||
Entradas e saídas em tempo real (último minuto)
|
<Users class="h-10 w-10 text-blue-600" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-base-content mb-2 text-xl font-bold">Gestão de Funcionários</h3>
|
||||||
|
<p class="text-base-content/70 text-sm">
|
||||||
|
Gerencie o cadastro completo de funcionários, informações pessoais, documentos e muito
|
||||||
|
mais.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="badge badge-success gap-2">
|
</a>
|
||||||
<span class="loading loading-spinner loading-xs"></span>
|
|
||||||
Atualizando
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="relative h-64">
|
<!-- Controle de Ponto -->
|
||||||
<!-- Eixo Y -->
|
|
||||||
<div
|
|
||||||
class="absolute top-0 bottom-8 left-0 flex w-10 flex-col justify-between pr-2 text-right"
|
|
||||||
>
|
|
||||||
{#each [10, 8, 6, 4, 2, 0] as val (val)}
|
|
||||||
<span class="text-base-content/60 text-xs">{val}</span>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Grid e Barras -->
|
|
||||||
<div class="absolute top-0 right-4 bottom-8 left-12">
|
|
||||||
<!-- Grid horizontal -->
|
|
||||||
{#each [0, 1, 2, 3, 4, 5] as i (i)}
|
|
||||||
<div
|
|
||||||
class="border-base-content/10 absolute right-0 left-0 border-t"
|
|
||||||
style="top: {(i / 5) * 100}%;"
|
|
||||||
></div>
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
<!-- Barras de atividade -->
|
|
||||||
<div class="flex h-full items-end justify-around gap-1">
|
|
||||||
{#each atividade.historico || [] as ponto, idx (idx)}
|
|
||||||
{@const entradas = ponto?.entradas || 0}
|
|
||||||
{@const saidas = ponto?.saidas || 0}
|
|
||||||
<div class="group relative flex h-full flex-1 items-end gap-0.5">
|
|
||||||
<!-- Entradas (verde) -->
|
|
||||||
<div
|
|
||||||
class="from-success to-success/70 flex-1 rounded-t bg-linear-to-t transition-all duration-300 hover:scale-110"
|
|
||||||
style="height: {(entradas / maxAtividade) * 100}%; min-height: 2px;"
|
|
||||||
title="Entradas: {entradas}"
|
|
||||||
></div>
|
|
||||||
<!-- Saídas (vermelho) -->
|
|
||||||
<div
|
|
||||||
class="from-error to-error/70 flex-1 rounded-t bg-linear-to-t transition-all duration-300 hover:scale-110"
|
|
||||||
style="height: {(saidas / maxAtividade) * 100}%; min-height: 2px;"
|
|
||||||
title="Saídas: {saidas}"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<!-- Tooltip no hover -->
|
|
||||||
<div
|
|
||||||
class="bg-base-300 text-base-content absolute bottom-full left-1/2 z-10 mb-2 -translate-x-1/2 rounded px-2 py-1 text-xs whitespace-nowrap opacity-0 shadow-lg transition-opacity group-hover:opacity-100"
|
|
||||||
>
|
|
||||||
<div>↑ {entradas} entradas</div>
|
|
||||||
<div>↓ {saidas} saídas</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Linha do eixo X -->
|
|
||||||
<div
|
|
||||||
class="border-base-content/30 absolute right-4 bottom-8 left-12 border-t-2"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<!-- Labels do eixo X -->
|
|
||||||
<div
|
|
||||||
class="text-base-content/60 absolute right-4 bottom-0 left-12 flex justify-between text-xs"
|
|
||||||
>
|
|
||||||
<span>-60s</span>
|
|
||||||
<span>-30s</span>
|
|
||||||
<span>agora</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Legenda -->
|
|
||||||
<div class="border-base-300 mt-4 flex justify-center gap-6 border-t pt-4">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div class="from-success to-success/70 h-4 w-4 rounded bg-linear-to-t"></div>
|
|
||||||
<span class="text-base-content/70 text-sm">Entradas no BD</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div class="from-error to-error/70 h-4 w-4 rounded bg-linear-to-t"></div>
|
|
||||||
<span class="text-base-content/70 text-sm">Saídas do BD</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Distribuição de Requisições -->
|
|
||||||
<div class="mb-6 grid grid-cols-1 gap-6 md:grid-cols-2">
|
|
||||||
<div class="card bg-base-100 shadow-xl">
|
|
||||||
<div class="card-body">
|
|
||||||
<h3 class="text-base-content mb-4 text-lg font-bold">Tipos de Operações</h3>
|
|
||||||
<div class="space-y-3">
|
|
||||||
<div>
|
|
||||||
<div class="mb-1 flex justify-between text-sm">
|
|
||||||
<span>Queries (Leituras)</span>
|
|
||||||
<span class="text-primary font-bold">{distribuicao?.queries ?? 0}</span>
|
|
||||||
</div>
|
|
||||||
<progress
|
|
||||||
class="progress progress-primary w-full"
|
|
||||||
value={distribuicao?.queries ?? 0}
|
|
||||||
max={Math.max(
|
|
||||||
(distribuicao?.queries ?? 0) + (distribuicao?.mutations ?? 0),
|
|
||||||
1
|
|
||||||
)}
|
|
||||||
></progress>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="mb-1 flex justify-between text-sm">
|
|
||||||
<span>Mutations (Escritas)</span>
|
|
||||||
<span class="text-secondary font-bold">{distribuicao?.mutations ?? 0}</span>
|
|
||||||
</div>
|
|
||||||
<progress
|
|
||||||
class="progress progress-secondary w-full"
|
|
||||||
value={distribuicao?.mutations ?? 0}
|
|
||||||
max={Math.max(
|
|
||||||
(distribuicao?.queries ?? 0) + (distribuicao?.mutations ?? 0),
|
|
||||||
1
|
|
||||||
)}
|
|
||||||
></progress>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card bg-base-100 shadow-xl">
|
|
||||||
<div class="card-body">
|
|
||||||
<h3 class="text-base-content mb-4 text-lg font-bold">Operações no Banco</h3>
|
|
||||||
<div class="space-y-3">
|
|
||||||
<div>
|
|
||||||
<div class="mb-1 flex justify-between text-sm">
|
|
||||||
<span>Leituras</span>
|
|
||||||
<span class="text-info font-bold">{distribuicao?.leituras ?? 0}</span>
|
|
||||||
</div>
|
|
||||||
<progress
|
|
||||||
class="progress progress-info w-full"
|
|
||||||
value={distribuicao?.leituras ?? 0}
|
|
||||||
max={Math.max(
|
|
||||||
(distribuicao?.leituras ?? 0) + (distribuicao?.escritas ?? 0),
|
|
||||||
1
|
|
||||||
)}
|
|
||||||
></progress>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="mb-1 flex justify-between text-sm">
|
|
||||||
<span>Escritas</span>
|
|
||||||
<span class="text-warning font-bold">{distribuicao?.escritas ?? 0}</span>
|
|
||||||
</div>
|
|
||||||
<progress
|
|
||||||
class="progress progress-warning w-full"
|
|
||||||
value={distribuicao?.escritas ?? 0}
|
|
||||||
max={Math.max(
|
|
||||||
(distribuicao?.leituras ?? 0) + (distribuicao?.escritas ?? 0),
|
|
||||||
1
|
|
||||||
)}
|
|
||||||
></progress>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Cards de Status -->
|
|
||||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
|
|
||||||
<div class="card bg-base-100 shadow-xl">
|
|
||||||
<div class="card-body">
|
|
||||||
<h3 class="card-title text-lg">Status do Sistema</h3>
|
|
||||||
<div class="mt-4 space-y-2">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<span class="text-sm">Banco de Dados</span>
|
|
||||||
<span class="badge badge-success">Online</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<span class="text-sm">API</span>
|
|
||||||
<span class="badge badge-success">Operacional</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<span class="text-sm">Backup</span>
|
|
||||||
<span class="badge badge-success">Atualizado</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card bg-base-100 shadow-xl">
|
|
||||||
<div class="card-body">
|
|
||||||
<h3 class="card-title text-lg">Acesso Rápido</h3>
|
|
||||||
<div class="mt-4 space-y-2">
|
|
||||||
<a
|
<a
|
||||||
href={resolve('/recursos-humanos/funcionarios/cadastro')}
|
href={resolve('/recursos-humanos/registro-pontos')}
|
||||||
class="btn btn-sm btn-primary w-full"
|
class="feature-card group card transform bg-linear-to-br from-cyan-500/10 to-cyan-600/20 shadow-lg transition-all duration-300 hover:-translate-y-2 hover:shadow-2xl"
|
||||||
>
|
>
|
||||||
Novo Funcionário
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href={resolve('/recursos-humanos/simbolos/cadastro')}
|
|
||||||
class="btn btn-sm btn-primary w-full"
|
|
||||||
>
|
|
||||||
Novo Símbolo
|
|
||||||
</a>
|
|
||||||
<a href={resolve('/ti/painel-administrativo')} class="btn btn-sm btn-primary w-full">
|
|
||||||
Painel Admin
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card bg-base-100 shadow-xl">
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h3 class="card-title text-lg">Informações</h3>
|
<div class="mb-4 flex items-center justify-between">
|
||||||
<div class="mt-4 space-y-2 text-sm">
|
<div
|
||||||
<p class="text-base-content/70">
|
class="rounded-xl bg-cyan-500/20 p-4 transition-colors duration-300 group-hover:bg-cyan-500/30"
|
||||||
<strong>Versão:</strong> 1.0.0
|
>
|
||||||
</p>
|
<Clock class="h-10 w-10 text-cyan-600" strokeWidth={2} />
|
||||||
<p class="text-base-content/70">
|
</div>
|
||||||
<strong>Última Atualização:</strong>
|
</div>
|
||||||
{new Date().toLocaleDateString('pt-BR')}
|
<h3 class="text-base-content mb-2 text-xl font-bold">Controle de Ponto</h3>
|
||||||
</p>
|
<p class="text-base-content/70 text-sm">
|
||||||
<p class="text-base-content/70">
|
Registre e gerencie pontos de funcionários, banco de horas e homologações de forma
|
||||||
<strong>Suporte:</strong> TI SGSE
|
eficiente.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Gestão de Férias e Licenças -->
|
||||||
|
<a
|
||||||
|
href={resolve('/recursos-humanos/ferias')}
|
||||||
|
class="feature-card group card transform bg-linear-to-br from-purple-500/10 to-purple-600/20 shadow-lg transition-all duration-300 hover:-translate-y-2 hover:shadow-2xl"
|
||||||
|
>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-4 flex items-center justify-between">
|
||||||
|
<div
|
||||||
|
class="rounded-xl bg-purple-500/20 p-4 transition-colors duration-300 group-hover:bg-purple-500/30"
|
||||||
|
>
|
||||||
|
<Calendar class="h-10 w-10 text-purple-600" strokeWidth={2} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<h3 class="text-base-content mb-2 text-xl font-bold">Gestão de Férias e Licenças</h3>
|
||||||
|
<p class="text-base-content/70 text-sm">
|
||||||
|
Controle períodos de férias, atestados médicos e licenças de forma organizada e
|
||||||
|
simplificada.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Gestão de Símbolos -->
|
||||||
|
<a
|
||||||
|
href={resolve('/recursos-humanos/simbolos')}
|
||||||
|
class="feature-card group card transform bg-linear-to-br from-green-500/10 to-green-600/20 shadow-lg transition-all duration-300 hover:-translate-y-2 hover:shadow-2xl"
|
||||||
|
>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-4 flex items-center justify-between">
|
||||||
|
<div
|
||||||
|
class="rounded-xl bg-green-500/20 p-4 transition-colors duration-300 group-hover:bg-green-500/30"
|
||||||
|
>
|
||||||
|
<BadgeCheck class="h-10 w-10 text-green-600" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-base-content mb-2 text-xl font-bold">Gestão de Símbolos</h3>
|
||||||
|
<p class="text-base-content/70 text-sm">
|
||||||
|
Gerencie cargos comissionados e funções gratificadas com facilidade e organização.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Almoxarifado -->
|
||||||
|
<a
|
||||||
|
href={resolve('/almoxarifado')}
|
||||||
|
class="feature-card group card transform bg-linear-to-br from-amber-500/10 to-amber-600/20 shadow-lg transition-all duration-300 hover:-translate-y-2 hover:shadow-2xl"
|
||||||
|
>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-4 flex items-center justify-between">
|
||||||
|
<div
|
||||||
|
class="rounded-xl bg-amber-500/20 p-4 transition-colors duration-300 group-hover:bg-amber-500/30"
|
||||||
|
>
|
||||||
|
<Package class="h-10 w-10 text-amber-600" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-base-content mb-2 text-xl font-bold">Almoxarifado</h3>
|
||||||
|
<p class="text-base-content/70 text-sm">
|
||||||
|
Controle de estoque, materiais, movimentações e requisições de forma integrada e
|
||||||
|
eficiente.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
|
||||||
<!-- Mensagem de erro ou estado vazio -->
|
|
||||||
<div class="alert alert-warning">
|
|
||||||
<span>Não foi possível carregar os dados do dashboard.</span>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
</main>
|
</main>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
|
|
||||||
@@ -724,7 +493,37 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.fade-in {
|
||||||
animation: fadeIn 0.5s ease-out;
|
animation: fadeIn 0.6s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in-delay {
|
||||||
|
animation: fadeIn 0.8s ease-out 0.2s both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in-delay-2 {
|
||||||
|
animation: fadeIn 1s ease-out 0.4s both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card {
|
||||||
|
animation: fadeIn 0.6s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
animation: fadeIn 0.8s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-pulse {
|
||||||
|
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import FuncionarioMatriculaAutocomplete from '$lib/components/FuncionarioMatriculaAutocomplete.svelte';
|
import FuncionarioMatriculaAutocomplete from '$lib/components/FuncionarioMatriculaAutocomplete.svelte';
|
||||||
import FileUpload from '$lib/components/FileUpload.svelte';
|
import FileUpload from '$lib/components/FileUpload.svelte';
|
||||||
import ErrorModal from '$lib/components/ErrorModal.svelte';
|
import ErrorModal from '$lib/components/ErrorModal.svelte';
|
||||||
|
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
|
||||||
import CalendarioAfastamentos from '$lib/components/CalendarioAfastamentos.svelte';
|
import CalendarioAfastamentos from '$lib/components/CalendarioAfastamentos.svelte';
|
||||||
import AreaChart from '$lib/components/ti/charts/AreaChart.svelte';
|
import AreaChart from '$lib/components/ti/charts/AreaChart.svelte';
|
||||||
import UserAvatar from '$lib/components/chat/UserAvatar.svelte';
|
import UserAvatar from '$lib/components/chat/UserAvatar.svelte';
|
||||||
@@ -132,6 +133,14 @@
|
|||||||
titulo: ''
|
titulo: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Modal de exclusão
|
||||||
|
let exclusaoModal = $state({
|
||||||
|
aberto: false,
|
||||||
|
tipo: null as 'atestado' | 'licenca' | null,
|
||||||
|
id: null as string | null,
|
||||||
|
nome: ''
|
||||||
|
});
|
||||||
|
|
||||||
// Licenças maternidade para prorrogação (derivar dos dados já carregados)
|
// Licenças maternidade para prorrogação (derivar dos dados já carregados)
|
||||||
let licencasMaternidade = $derived.by(() => {
|
let licencasMaternidade = $derived.by(() => {
|
||||||
const dados = dadosQuery?.data;
|
const dados = dadosQuery?.data;
|
||||||
@@ -651,26 +660,42 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Excluir registro
|
// Abrir modal de exclusão
|
||||||
async function excluirRegistro(tipo: 'atestado' | 'licenca', id: string) {
|
function abrirModalExclusao(tipo: 'atestado' | 'licenca', id: string, nome: string) {
|
||||||
if (!confirm(`Tem certeza que deseja excluir este ${tipo}?`)) return;
|
exclusaoModal = {
|
||||||
|
aberto: true,
|
||||||
|
tipo,
|
||||||
|
id,
|
||||||
|
nome
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirmar exclusão
|
||||||
|
async function confirmarExclusao() {
|
||||||
|
if (!exclusaoModal.tipo || !exclusaoModal.id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (tipo === 'atestado') {
|
if (exclusaoModal.tipo === 'atestado') {
|
||||||
await client.mutation(api.atestadosLicencas.excluirAtestado, {
|
await client.mutation(api.atestadosLicencas.excluirAtestado, {
|
||||||
id: id as Id<'atestados'>
|
id: exclusaoModal.id as Id<'atestados'>
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await client.mutation(api.atestadosLicencas.excluirLicenca, {
|
await client.mutation(api.atestadosLicencas.excluirLicenca, {
|
||||||
id: id as Id<'licencas'>
|
id: exclusaoModal.id as Id<'licencas'>
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
toast.success('Registro excluído com sucesso!');
|
toast.success('Registro excluído com sucesso!');
|
||||||
|
exclusaoModal.aberto = false;
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
toast.error(getErrorMessage(error, 'Erro ao excluir registro'));
|
toast.error(getErrorMessage(error, 'Erro ao excluir registro'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cancelar exclusão
|
||||||
|
function cancelarExclusao() {
|
||||||
|
exclusaoModal.aberto = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Filtrar registros
|
// Filtrar registros
|
||||||
let registrosFiltrados = $derived.by(() => {
|
let registrosFiltrados = $derived.by(() => {
|
||||||
const dados = dadosQuery?.data;
|
const dados = dadosQuery?.data;
|
||||||
@@ -1677,7 +1702,12 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<button
|
<button
|
||||||
class="btn btn-xs btn-error"
|
class="btn btn-xs btn-error"
|
||||||
onclick={() => excluirRegistro('atestado', atestado._id)}
|
onclick={() =>
|
||||||
|
abrirModalExclusao(
|
||||||
|
'atestado',
|
||||||
|
atestado._id,
|
||||||
|
`${atestado.tipo === 'atestado_medico' ? 'Atestado Médico' : 'Declaração'} - ${atestado.funcionario?.nome || 'Funcionário'}`
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
Excluir
|
Excluir
|
||||||
</button>
|
</button>
|
||||||
@@ -1739,7 +1769,12 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<button
|
<button
|
||||||
class="btn btn-xs btn-error"
|
class="btn btn-xs btn-error"
|
||||||
onclick={() => excluirRegistro('licenca', licenca._id)}
|
onclick={() =>
|
||||||
|
abrirModalExclusao(
|
||||||
|
'licenca',
|
||||||
|
licenca._id,
|
||||||
|
`${licenca.tipo === 'maternidade' ? 'Licença Maternidade' : 'Licença Paternidade'} - ${licenca.funcionario?.nome || 'Funcionário'}`
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
Excluir
|
Excluir
|
||||||
</button>
|
</button>
|
||||||
@@ -2438,6 +2473,17 @@
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Modal de Confirmação de Exclusão -->
|
||||||
|
<ConfirmModal
|
||||||
|
bind:open={exclusaoModal.aberto}
|
||||||
|
title="Confirmar Exclusão"
|
||||||
|
message="Tem certeza que deseja excluir este registro? Esta ação não pode ser desfeita e todos os dados relacionados serão removidos permanentemente."
|
||||||
|
confirmText="Excluir"
|
||||||
|
cancelText="Cancelar"
|
||||||
|
onConfirm={confirmarExclusao}
|
||||||
|
onCancel={cancelarExclusao}
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Modal de Documento -->
|
<!-- Modal de Documento -->
|
||||||
{#if documentoModal.aberto}
|
{#if documentoModal.aberto}
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
let modoCriacao = $state(false);
|
let modoCriacao = $state(false);
|
||||||
let mostrandoModalExcluir = $state(false);
|
let mostrandoModalExcluir = $state(false);
|
||||||
let dispensaParaExcluir = $state<Id<'dispensasRegistro'> | null>(null);
|
let dispensaParaExcluir = $state<Id<'dispensasRegistro'> | null>(null);
|
||||||
|
let filtroStatus = $state<'todas' | 'ativas' | 'expiradas'>('todas');
|
||||||
|
|
||||||
// Formulário
|
// Formulário
|
||||||
let dataInicio = $state(new Date().toISOString().split('T')[0]!);
|
let dataInicio = $state(new Date().toISOString().split('T')[0]!);
|
||||||
@@ -25,22 +26,33 @@
|
|||||||
// Computed para converter time string para hora/minuto
|
// Computed para converter time string para hora/minuto
|
||||||
let horaInicio = $derived.by(() => {
|
let horaInicio = $derived.by(() => {
|
||||||
const [hora, minuto] = horaInicioTime.split(':').map(Number);
|
const [hora, minuto] = horaInicioTime.split(':').map(Number);
|
||||||
return { hora: hora || 8, minuto: minuto || 0 };
|
return { hora: isNaN(hora) ? 8 : hora, minuto: isNaN(minuto) ? 0 : minuto };
|
||||||
});
|
});
|
||||||
|
|
||||||
let horaFim = $derived.by(() => {
|
let horaFim = $derived.by(() => {
|
||||||
const [hora, minuto] = horaFimTime.split(':').map(Number);
|
const [hora, minuto] = horaFimTime.split(':').map(Number);
|
||||||
return { hora: hora || 18, minuto: minuto || 0 };
|
return { hora: isNaN(hora) ? 18 : hora, minuto: isNaN(minuto) ? 0 : minuto };
|
||||||
});
|
});
|
||||||
|
|
||||||
// Queries
|
// Queries
|
||||||
const subordinadosQuery = useQuery(api.times.listarSubordinadosDoGestorAtual, {});
|
const subordinadosQuery = useQuery(api.times.listarSubordinadosDoGestorAtual, {});
|
||||||
const dispensasQuery = useQuery(api.pontos.listarDispensas, {
|
const dispensasQuery = useQuery(api.pontos.listarDispensas, {});
|
||||||
apenasAtivas: true // Mostrar apenas dispensas ativas
|
|
||||||
});
|
|
||||||
|
|
||||||
let subordinados = $derived(subordinadosQuery?.data || []);
|
let subordinados = $derived(subordinadosQuery?.data || []);
|
||||||
let dispensas = $derived(dispensasQuery?.data || []);
|
let todasDispensas = $derived(dispensasQuery?.data || []);
|
||||||
|
|
||||||
|
// Filtrar dispensas baseado no filtro selecionado
|
||||||
|
let dispensas = $derived.by(() => {
|
||||||
|
if (filtroStatus === 'todas') {
|
||||||
|
return todasDispensas;
|
||||||
|
} else if (filtroStatus === 'ativas') {
|
||||||
|
// Ativas: não expiradas (inclui isentos que já começaram)
|
||||||
|
return todasDispensas.filter((d) => !d.expirada);
|
||||||
|
} else {
|
||||||
|
// Expiradas: apenas dispensas não isentas que expiraram
|
||||||
|
return todasDispensas.filter((d) => d.expirada && !d.isento);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Lista de funcionários do time
|
// Lista de funcionários do time
|
||||||
let funcionarios = $derived.by(() => {
|
let funcionarios = $derived.by(() => {
|
||||||
@@ -100,10 +112,9 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataInicioObj = new Date(dataInicio);
|
// Validar datas (comparar strings diretamente para evitar problemas de timezone)
|
||||||
const dataFimObj = new Date(dataFim);
|
// Formato YYYY-MM-DD permite comparação lexicográfica
|
||||||
|
if (dataFim < dataInicio) {
|
||||||
if (dataFimObj < dataInicioObj) {
|
|
||||||
toast.error('Data fim deve ser maior ou igual à data início');
|
toast.error('Data fim deve ser maior ou igual à data início');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -163,7 +174,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatarDataHora(data: string, hora: number, minuto: number): string {
|
function formatarDataHora(data: string, hora: number, minuto: number): string {
|
||||||
return `${new Date(data).toLocaleDateString('pt-BR')} ${hora.toString().padStart(2, '0')}:${minuto.toString().padStart(2, '0')}`;
|
// Converter YYYY-MM-DD para DD/MM/YYYY sem problemas de timezone
|
||||||
|
const [ano, mes, dia] = data.split('-');
|
||||||
|
const dataFormatada = `${dia}/${mes}/${ano}`;
|
||||||
|
return `${dataFormatada} ${hora.toString().padStart(2, '0')}:${minuto.toString().padStart(2, '0')}`;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -311,11 +325,42 @@
|
|||||||
<!-- Lista de Dispensas -->
|
<!-- Lista de Dispensas -->
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<div class="card bg-base-100 shadow-xl">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title mb-4">Dispensas Ativas</h2>
|
<div class="mb-4 flex items-center justify-between">
|
||||||
|
<h2 class="card-title">Dispensas</h2>
|
||||||
|
<!-- Filtro de Status -->
|
||||||
|
<div class="join">
|
||||||
|
<button
|
||||||
|
class="btn join-item btn-sm {filtroStatus === 'todas' ? 'btn-active' : ''}"
|
||||||
|
onclick={() => (filtroStatus = 'todas')}
|
||||||
|
>
|
||||||
|
Todas
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn join-item btn-sm {filtroStatus === 'ativas' ? 'btn-active' : ''}"
|
||||||
|
onclick={() => (filtroStatus = 'ativas')}
|
||||||
|
>
|
||||||
|
Ativas
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn join-item btn-sm {filtroStatus === 'expiradas' ? 'btn-active' : ''}"
|
||||||
|
onclick={() => (filtroStatus = 'expiradas')}
|
||||||
|
>
|
||||||
|
Expiradas
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if dispensas.length === 0}
|
{#if dispensas.length === 0}
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
<span>Nenhuma dispensa ativa encontrada</span>
|
<span>
|
||||||
|
{#if filtroStatus === 'todas'}
|
||||||
|
Nenhuma dispensa encontrada
|
||||||
|
{:else if filtroStatus === 'ativas'}
|
||||||
|
Nenhuma dispensa ativa encontrada
|
||||||
|
{:else}
|
||||||
|
Nenhuma dispensa expirada encontrada
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
@@ -371,7 +416,7 @@
|
|||||||
{#if dispensa.isento}
|
{#if dispensa.isento}
|
||||||
<span class="badge badge-warning">Isento (sem expiração)</span>
|
<span class="badge badge-warning">Isento (sem expiração)</span>
|
||||||
{:else if dispensa.expirada}
|
{:else if dispensa.expirada}
|
||||||
<span class="badge badge-error">Expirada</span>
|
<span class="badge badge-error">Não ativo</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="badge badge-success">Ativa</span>
|
<span class="badge badge-success">Ativa</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -264,6 +264,10 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Converter hora formato HH:mm para hora e minuto
|
||||||
|
const { hora: horaInicio, minuto: minutoInicio } = timeParaHoraMinuto(horaInicioAjuste);
|
||||||
|
const { hora: horaFim, minuto: minutoFim } = timeParaHoraMinuto(horaFimAjuste);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.mutation(api.pontos.ajustarBancoHoras, {
|
await client.mutation(api.pontos.ajustarBancoHoras, {
|
||||||
funcionarioId: funcionarioSelecionado,
|
funcionarioId: funcionarioSelecionado,
|
||||||
@@ -271,6 +275,13 @@
|
|||||||
periodoDias: dias,
|
periodoDias: dias,
|
||||||
periodoHoras: horas,
|
periodoHoras: horas,
|
||||||
periodoMinutos: minutos,
|
periodoMinutos: minutos,
|
||||||
|
dataAplicacao: dataInicioAjuste, // Data escolhida pelo usuário
|
||||||
|
dataInicio: dataInicioAjuste,
|
||||||
|
horaInicio,
|
||||||
|
minutoInicio,
|
||||||
|
dataFim: dataFimAjuste,
|
||||||
|
horaFim,
|
||||||
|
minutoFim,
|
||||||
motivoId: motivoId || undefined,
|
motivoId: motivoId || undefined,
|
||||||
motivoTipo: motivoTipo || undefined,
|
motivoTipo: motivoTipo || undefined,
|
||||||
motivoDescricao: motivoDescricao || undefined,
|
motivoDescricao: motivoDescricao || undefined,
|
||||||
@@ -910,7 +921,11 @@
|
|||||||
{#each homologacoes as homologacao (homologacao._id)}
|
{#each homologacoes as homologacao (homologacao._id)}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
{#if homologacao.dataAplicacaoAjuste}
|
||||||
|
{new Date(homologacao.dataAplicacaoAjuste + 'T00:00:00').toLocaleDateString('pt-BR')}
|
||||||
|
{:else}
|
||||||
{new Date(homologacao.criadoEm).toLocaleDateString('pt-BR')}
|
{new Date(homologacao.criadoEm).toLocaleDateString('pt-BR')}
|
||||||
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
{#if !funcionarioSelecionado}
|
{#if !funcionarioSelecionado}
|
||||||
<td>
|
<td>
|
||||||
@@ -958,10 +973,17 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{:else if homologacao.ajusteMinutos}
|
{:else if homologacao.ajusteMinutos}
|
||||||
<div class="text-sm">
|
<div class="text-sm space-y-1">
|
||||||
|
<div>
|
||||||
{homologacao.periodoDias || 0}d {homologacao.periodoHoras || 0}h
|
{homologacao.periodoDias || 0}d {homologacao.periodoHoras || 0}h
|
||||||
{homologacao.periodoMinutos || 0}min
|
{homologacao.periodoMinutos || 0}min
|
||||||
</div>
|
</div>
|
||||||
|
{#if homologacao.periodoAjuste?.dataInicio && homologacao.periodoAjuste?.horaInicio !== undefined && homologacao.periodoAjuste?.minutoInicio !== undefined && homologacao.periodoAjuste?.dataFim && homologacao.periodoAjuste?.horaFim !== undefined && homologacao.periodoAjuste?.minutoFim !== undefined}
|
||||||
|
<div class="text-xs text-base-content/60">
|
||||||
|
{new Date(homologacao.periodoAjuste.dataInicio + 'T00:00:00').toLocaleDateString('pt-BR')} {formatarHoraPonto(homologacao.periodoAjuste.horaInicio, homologacao.periodoAjuste.minutoInicio)} a {new Date(homologacao.periodoAjuste.dataFim + 'T00:00:00').toLocaleDateString('pt-BR')} {formatarHoraPonto(homologacao.periodoAjuste.horaFim, homologacao.periodoAjuste.minutoFim)}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -1026,6 +1048,10 @@
|
|||||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<span class="font-medium">Data: </span>
|
<span class="font-medium">Data: </span>
|
||||||
|
{#if homologacaoSelecionada.dataAplicacaoAjuste}
|
||||||
|
{new Date(homologacaoSelecionada.dataAplicacaoAjuste + 'T00:00:00').toLocaleDateString('pt-BR')}
|
||||||
|
<span class="text-base-content/60 text-xs"> (Aplicado em)</span>
|
||||||
|
{:else}
|
||||||
{new Date(homologacaoSelecionada.criadoEm).toLocaleDateString('pt-BR', {
|
{new Date(homologacaoSelecionada.criadoEm).toLocaleDateString('pt-BR', {
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
month: '2-digit',
|
month: '2-digit',
|
||||||
@@ -1033,6 +1059,7 @@
|
|||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit'
|
minute: '2-digit'
|
||||||
})}
|
})}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="font-medium">Funcionário:</span>
|
<span class="font-medium">Funcionário:</span>
|
||||||
@@ -1124,6 +1151,16 @@
|
|||||||
{homologacaoSelecionada.periodoHoras || 0}h
|
{homologacaoSelecionada.periodoHoras || 0}h
|
||||||
{homologacaoSelecionada.periodoMinutos || 0}min
|
{homologacaoSelecionada.periodoMinutos || 0}min
|
||||||
</div>
|
</div>
|
||||||
|
{#if homologacaoSelecionada.periodoAjuste?.dataInicio && homologacaoSelecionada.periodoAjuste?.horaInicio !== undefined && homologacaoSelecionada.periodoAjuste?.minutoInicio !== undefined && homologacaoSelecionada.periodoAjuste?.dataFim && homologacaoSelecionada.periodoAjuste?.horaFim !== undefined && homologacaoSelecionada.periodoAjuste?.minutoFim !== undefined}
|
||||||
|
<div>
|
||||||
|
<span class="font-medium">Data/Hora Início: </span>
|
||||||
|
{new Date(homologacaoSelecionada.periodoAjuste.dataInicio + 'T00:00:00').toLocaleDateString('pt-BR')} {formatarHoraPonto(homologacaoSelecionada.periodoAjuste.horaInicio, homologacaoSelecionada.periodoAjuste.minutoInicio)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="font-medium">Data/Hora Fim: </span>
|
||||||
|
{new Date(homologacaoSelecionada.periodoAjuste.dataFim + 'T00:00:00').toLocaleDateString('pt-BR')} {formatarHoraPonto(homologacaoSelecionada.periodoAjuste.horaFim, homologacaoSelecionada.periodoAjuste.minutoFim)}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{#if homologacaoSelecionada.ajusteMinutos}
|
{#if homologacaoSelecionada.ajusteMinutos}
|
||||||
<div>
|
<div>
|
||||||
<span class="font-medium">Ajuste Total: </span>
|
<span class="font-medium">Ajuste Total: </span>
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ export const listarTodos = query({
|
|||||||
fotoPerfilUrl,
|
fotoPerfilUrl,
|
||||||
criadoPorNome: criadoPor?.nome || 'Sistema',
|
criadoPorNome: criadoPor?.nome || 'Sistema',
|
||||||
dias: calcularDias(a.dataInicio, a.dataFim),
|
dias: calcularDias(a.dataInicio, a.dataFim),
|
||||||
status: new Date(a.dataFim) >= new Date() ? 'ativo' : 'finalizado'
|
status: new Date() > new Date(a.dataFim) ? 'finalizado' : 'ativo'
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erro ao buscar detalhes do atestado:', error);
|
console.error('Erro ao buscar detalhes do atestado:', error);
|
||||||
@@ -192,7 +192,7 @@ export const listarTodos = query({
|
|||||||
fotoPerfilUrl: null,
|
fotoPerfilUrl: null,
|
||||||
criadoPorNome: 'Sistema',
|
criadoPorNome: 'Sistema',
|
||||||
dias: calcularDias(a.dataInicio, a.dataFim),
|
dias: calcularDias(a.dataInicio, a.dataFim),
|
||||||
status: new Date(a.dataFim) >= new Date() ? 'ativo' : 'finalizado'
|
status: new Date() > new Date(a.dataFim) ? 'finalizado' : 'ativo'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -226,7 +226,7 @@ export const listarTodos = query({
|
|||||||
criadoPorNome: criadoPor?.nome || 'Sistema',
|
criadoPorNome: criadoPor?.nome || 'Sistema',
|
||||||
licencaOriginal,
|
licencaOriginal,
|
||||||
dias: calcularDias(l.dataInicio, l.dataFim),
|
dias: calcularDias(l.dataInicio, l.dataFim),
|
||||||
status: new Date(l.dataFim) >= new Date() ? 'ativo' : 'finalizado'
|
status: new Date() > new Date(l.dataFim) ? 'finalizado' : 'ativo'
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erro ao buscar detalhes da licença:', error);
|
console.error('Erro ao buscar detalhes da licença:', error);
|
||||||
@@ -237,7 +237,7 @@ export const listarTodos = query({
|
|||||||
criadoPorNome: 'Sistema',
|
criadoPorNome: 'Sistema',
|
||||||
licencaOriginal: null,
|
licencaOriginal: null,
|
||||||
dias: calcularDias(l.dataInicio, l.dataFim),
|
dias: calcularDias(l.dataInicio, l.dataFim),
|
||||||
status: new Date(l.dataFim) >= new Date() ? 'ativo' : 'finalizado'
|
status: new Date() > new Date(l.dataFim) ? 'finalizado' : 'ativo'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1254,6 +1254,33 @@ export const excluirAtestado = mutation({
|
|||||||
const funcionarioId = atestado.funcionarioId;
|
const funcionarioId = atestado.funcionarioId;
|
||||||
const dataInicio = atestado.dataInicio; // Data início do atestado
|
const dataInicio = atestado.dataInicio; // Data início do atestado
|
||||||
const dataFim = atestado.dataFim; // Data fim do atestado
|
const dataFim = atestado.dataFim; // Data fim do atestado
|
||||||
|
const atestadoId = args.id.toString(); // ID do atestado para remover ajustes
|
||||||
|
const documentoId = atestado.documentoId; // ID do documento para remover do storage
|
||||||
|
|
||||||
|
// Remover logs de atividades relacionados ao atestado
|
||||||
|
try {
|
||||||
|
const logs = await ctx.db
|
||||||
|
.query('logsAtividades')
|
||||||
|
.withIndex('by_recurso_id', (q) =>
|
||||||
|
q.eq('recurso', 'atestados').eq('recursoId', atestadoId)
|
||||||
|
)
|
||||||
|
.collect();
|
||||||
|
for (const log of logs) {
|
||||||
|
await ctx.db.delete(log._id);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[excluirAtestado] Erro ao remover logs de atividades:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remover documento do storage se existir
|
||||||
|
if (documentoId) {
|
||||||
|
try {
|
||||||
|
await ctx.storage.delete(documentoId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[excluirAtestado] Erro ao remover documento do storage:', error);
|
||||||
|
// Não falhar a exclusão se o documento não existir mais
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Excluir o registro do banco de dados
|
// Excluir o registro do banco de dados
|
||||||
await ctx.db.delete(args.id);
|
await ctx.db.delete(args.id);
|
||||||
@@ -1267,6 +1294,19 @@ export const excluirAtestado = mutation({
|
|||||||
args.id
|
args.id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Remover ajustes automáticos relacionados ao atestado excluído
|
||||||
|
try {
|
||||||
|
await ctx.runMutation(internal.pontos.removerAjustesAutomaticosInternal, {
|
||||||
|
funcionarioId,
|
||||||
|
motivoTipo: 'atestado',
|
||||||
|
motivoId: atestadoId,
|
||||||
|
dataInicio,
|
||||||
|
dataFim
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[excluirAtestado] Erro ao remover ajustes automáticos:', error);
|
||||||
|
}
|
||||||
|
|
||||||
// Recalcular banco de horas APENAS para o período específico do atestado excluído
|
// Recalcular banco de horas APENAS para o período específico do atestado excluído
|
||||||
// Isso garante que os dias do atestado sejam removidos corretamente dos registros de ponto
|
// Isso garante que os dias do atestado sejam removidos corretamente dos registros de ponto
|
||||||
await recalcularBancoHorasPeriodo(ctx, funcionarioId, dataInicio, dataFim);
|
await recalcularBancoHorasPeriodo(ctx, funcionarioId, dataInicio, dataFim);
|
||||||
@@ -1305,6 +1345,33 @@ export const excluirLicenca = mutation({
|
|||||||
const funcionarioId = licenca.funcionarioId;
|
const funcionarioId = licenca.funcionarioId;
|
||||||
const dataInicio = licenca.dataInicio; // Data início da licença
|
const dataInicio = licenca.dataInicio; // Data início da licença
|
||||||
const dataFim = licenca.dataFim; // Data fim da licença
|
const dataFim = licenca.dataFim; // Data fim da licença
|
||||||
|
const licencaId = args.id.toString(); // ID da licença para remover logs
|
||||||
|
const documentoId = licenca.documentoId; // ID do documento para remover do storage
|
||||||
|
|
||||||
|
// Remover logs de atividades relacionados à licença
|
||||||
|
try {
|
||||||
|
const logs = await ctx.db
|
||||||
|
.query('logsAtividades')
|
||||||
|
.withIndex('by_recurso_id', (q) =>
|
||||||
|
q.eq('recurso', 'licencas').eq('recursoId', licencaId)
|
||||||
|
)
|
||||||
|
.collect();
|
||||||
|
for (const log of logs) {
|
||||||
|
await ctx.db.delete(log._id);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[excluirLicenca] Erro ao remover logs de atividades:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remover documento do storage se existir
|
||||||
|
if (documentoId) {
|
||||||
|
try {
|
||||||
|
await ctx.storage.delete(documentoId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[excluirLicenca] Erro ao remover documento do storage:', error);
|
||||||
|
// Não falhar a exclusão se o documento não existir mais
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Excluir o registro do banco de dados
|
// Excluir o registro do banco de dados
|
||||||
await ctx.db.delete(args.id);
|
await ctx.db.delete(args.id);
|
||||||
@@ -1318,6 +1385,19 @@ export const excluirLicenca = mutation({
|
|||||||
args.id
|
args.id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Remover ajustes automáticos relacionados à licença excluída
|
||||||
|
try {
|
||||||
|
await ctx.runMutation(internal.pontos.removerAjustesAutomaticosInternal, {
|
||||||
|
funcionarioId,
|
||||||
|
motivoTipo: 'licenca',
|
||||||
|
motivoId: licencaId,
|
||||||
|
dataInicio,
|
||||||
|
dataFim
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[excluirLicenca] Erro ao remover ajustes automáticos:', error);
|
||||||
|
}
|
||||||
|
|
||||||
// Recalcular banco de horas APENAS para o período específico da licença excluída
|
// Recalcular banco de horas APENAS para o período específico da licença excluída
|
||||||
// Isso garante que os dias da licença sejam removidos corretamente dos registros de ponto
|
// Isso garante que os dias da licença sejam removidos corretamente dos registros de ponto
|
||||||
await recalcularBancoHorasPeriodo(ctx, funcionarioId, dataInicio, dataFim);
|
await recalcularBancoHorasPeriodo(ctx, funcionarioId, dataInicio, dataFim);
|
||||||
|
|||||||
@@ -941,10 +941,13 @@ export const excluirSolicitacao = mutation({
|
|||||||
throw new Error('Solicitação não encontrada');
|
throw new Error('Solicitação não encontrada');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apenas solicitações ainda não processadas podem ser excluídas
|
// IMPORTANTE: Salvar o período exato da ausência ANTES de excluir
|
||||||
if (solicitacao.status !== 'aguardando_aprovacao') {
|
// para recalcular o banco de horas apenas para esse período específico
|
||||||
throw new Error('Apenas solicitações pendentes podem ser excluídas');
|
const funcionarioId = solicitacao.funcionarioId;
|
||||||
}
|
const dataInicio = solicitacao.dataInicio;
|
||||||
|
const dataFim = solicitacao.dataFim;
|
||||||
|
const statusOriginal = solicitacao.status;
|
||||||
|
const ausenciaId = args.solicitacaoId.toString(); // ID da ausência para remover ajustes
|
||||||
|
|
||||||
// Verificar se o usuário é o criador original da solicitação
|
// Verificar se o usuário é o criador original da solicitação
|
||||||
const usuario = await ctx.db.get(args.usuarioId);
|
const usuario = await ctx.db.get(args.usuarioId);
|
||||||
@@ -963,7 +966,34 @@ export const excluirSolicitacao = mutation({
|
|||||||
throw new Error('Você não tem permissão para excluir esta solicitação');
|
throw new Error('Você não tem permissão para excluir esta solicitação');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Permitir exclusão de ausências aprovadas (não apenas pendentes)
|
||||||
|
// Se estiver aprovada, o gestor pode excluir para corrigir erros
|
||||||
|
if (statusOriginal === 'aprovado' && !usuarioEhGestor) {
|
||||||
|
throw new Error('Apenas o gestor pode excluir ausências aprovadas');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Excluir o registro do banco de dados
|
||||||
await ctx.db.delete(args.solicitacaoId);
|
await ctx.db.delete(args.solicitacaoId);
|
||||||
|
|
||||||
|
// Remover ajustes automáticos relacionados à ausência excluída (apenas se estava aprovada)
|
||||||
|
if (statusOriginal === 'aprovado') {
|
||||||
|
try {
|
||||||
|
await ctx.runMutation(internal.pontos.removerAjustesAutomaticosInternal, {
|
||||||
|
funcionarioId,
|
||||||
|
motivoTipo: 'ausencia',
|
||||||
|
motivoId: ausenciaId,
|
||||||
|
dataInicio,
|
||||||
|
dataFim
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[excluirSolicitacao] Erro ao remover ajustes automáticos:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalcular banco de horas APENAS para o período específico da ausência excluída
|
||||||
|
// Isso garante que os dias da ausência sejam removidos corretamente dos registros de ponto
|
||||||
|
await recalcularBancoHorasPeriodo(ctx, funcionarioId, dataInicio, dataFim);
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ export const getStats = query({
|
|||||||
returns: v.object({
|
returns: v.object({
|
||||||
totalFuncionarios: v.number(),
|
totalFuncionarios: v.number(),
|
||||||
totalSimbolos: v.number(),
|
totalSimbolos: v.number(),
|
||||||
|
totalUsuarios: v.number(),
|
||||||
funcionariosAtivos: v.number(),
|
funcionariosAtivos: v.number(),
|
||||||
funcionariosDesligados: v.number(),
|
funcionariosDesligados: v.number(),
|
||||||
cargoComissionado: v.number(),
|
cargoComissionado: v.number(),
|
||||||
funcaoGratificada: v.number()
|
funcaoGratificada: v.number(),
|
||||||
|
totalCadastros: v.number()
|
||||||
}),
|
}),
|
||||||
handler: async (ctx) => {
|
handler: async (ctx) => {
|
||||||
// Contar funcionários
|
// Contar funcionários
|
||||||
@@ -36,41 +38,22 @@ export const getStats = query({
|
|||||||
const simbolos = await ctx.db.query('simbolos').collect();
|
const simbolos = await ctx.db.query('simbolos').collect();
|
||||||
const totalSimbolos = simbolos.length;
|
const totalSimbolos = simbolos.length;
|
||||||
|
|
||||||
|
// Contar usuários cadastrados
|
||||||
|
const usuarios = await ctx.db.query('usuarios').collect();
|
||||||
|
const totalUsuarios = usuarios.length;
|
||||||
|
|
||||||
|
// Calcular total de cadastros (funcionários + símbolos + usuários)
|
||||||
|
const totalCadastros = totalFuncionarios + totalSimbolos + totalUsuarios;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalFuncionarios,
|
totalFuncionarios,
|
||||||
totalSimbolos,
|
totalSimbolos,
|
||||||
|
totalUsuarios,
|
||||||
funcionariosAtivos,
|
funcionariosAtivos,
|
||||||
funcionariosDesligados,
|
funcionariosDesligados,
|
||||||
cargoComissionado,
|
cargoComissionado,
|
||||||
funcaoGratificada
|
funcaoGratificada,
|
||||||
};
|
totalCadastros
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Obter atividades recentes (últimas 24 horas)
|
|
||||||
export const getRecentActivity = query({
|
|
||||||
args: {},
|
|
||||||
returns: v.object({
|
|
||||||
funcionariosCadastrados24h: v.number(),
|
|
||||||
simbolosCadastrados24h: v.number()
|
|
||||||
}),
|
|
||||||
handler: async (ctx) => {
|
|
||||||
const now = Date.now();
|
|
||||||
const last24h = now - 24 * 60 * 60 * 1000;
|
|
||||||
|
|
||||||
// Funcionários cadastrados nas últimas 24h
|
|
||||||
const funcionarios = await ctx.db.query('funcionarios').collect();
|
|
||||||
const funcionariosCadastrados24h = funcionarios.filter(
|
|
||||||
(f) => f._creationTime >= last24h
|
|
||||||
).length;
|
|
||||||
|
|
||||||
// Símbolos cadastrados nas últimas 24h
|
|
||||||
const simbolos = await ctx.db.query('simbolos').collect();
|
|
||||||
const simbolosCadastrados24h = simbolos.filter((s) => s._creationTime >= last24h).length;
|
|
||||||
|
|
||||||
return {
|
|
||||||
funcionariosCadastrados24h,
|
|
||||||
simbolosCadastrados24h
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,43 @@
|
|||||||
import { v } from 'convex/values';
|
import { v } from 'convex/values';
|
||||||
import { mutation, query, internalMutation } from './_generated/server';
|
import { mutation, query, internalMutation } from './_generated/server';
|
||||||
import { internal } from './_generated/api';
|
import { internal } from './_generated/api';
|
||||||
|
import type { MutationCtx } from './_generated/server';
|
||||||
import { Id, Doc } from './_generated/dataModel';
|
import { Id, Doc } from './_generated/dataModel';
|
||||||
import { verificarLicencaAtiva } from './atestadosLicencas';
|
import { verificarLicencaAtiva } from './atestadosLicencas';
|
||||||
import { getCurrentUserFunction } from './auth';
|
import { getCurrentUserFunction } from './auth';
|
||||||
import { formatarDataBR } from './utils/datas';
|
import { formatarDataBR } from './utils/datas';
|
||||||
import { api } from './_generated/api';
|
import { api } from './_generated/api';
|
||||||
|
|
||||||
|
// Helper: Recalcular banco de horas em um período
|
||||||
|
async function recalcularBancoHorasPeriodo(
|
||||||
|
ctx: MutationCtx,
|
||||||
|
funcionarioId: Id<'funcionarios'>,
|
||||||
|
dataInicio: string,
|
||||||
|
dataFim: string
|
||||||
|
): Promise<void> {
|
||||||
|
// Gerar todas as datas do período
|
||||||
|
const dataInicioObj = new Date(dataInicio);
|
||||||
|
const dataFimObj = new Date(dataFim);
|
||||||
|
const datas: string[] = [];
|
||||||
|
const dataAtual = new Date(dataInicioObj);
|
||||||
|
|
||||||
|
while (dataAtual <= dataFimObj) {
|
||||||
|
const ano = dataAtual.getFullYear();
|
||||||
|
const mes = String(dataAtual.getMonth() + 1).padStart(2, '0');
|
||||||
|
const dia = String(dataAtual.getDate()).padStart(2, '0');
|
||||||
|
datas.push(`${ano}-${mes}-${dia}`);
|
||||||
|
dataAtual.setDate(dataAtual.getDate() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalcular para cada data usando a mutation interna (agendar para execução assíncrona)
|
||||||
|
for (let i = 0; i < datas.length; i++) {
|
||||||
|
await ctx.scheduler.runAfter(i * 100, internal.pontos.recalcularBancoHorasData, {
|
||||||
|
funcionarioId,
|
||||||
|
data: datas[i]!
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Validador para períodos
|
// Validador para períodos
|
||||||
const periodoValidator = v.object({
|
const periodoValidator = v.object({
|
||||||
dataInicio: v.string(),
|
dataInicio: v.string(),
|
||||||
@@ -821,8 +852,13 @@ export const atualizarStatus = mutation({
|
|||||||
throw new Error('Período de férias não encontrado');
|
throw new Error('Período de férias não encontrado');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Atualizar status e histórico
|
// Buscar usuário que está alterando o status para incluir na mensagem quando for Cancelado_RH
|
||||||
const acao = `Status alterado para ${args.novoStatus}`;
|
let acao = `Status alterado para ${args.novoStatus}`;
|
||||||
|
if (args.novoStatus === 'Cancelado_RH') {
|
||||||
|
const usuarioRH = await ctx.db.get(args.usuarioId);
|
||||||
|
const nomeUsuario = usuarioRH?.nome || 'Usuário Desconhecido';
|
||||||
|
acao = `Status alterado para Cancelado_RH por ${nomeUsuario}`;
|
||||||
|
}
|
||||||
|
|
||||||
const updateData: {
|
const updateData: {
|
||||||
status: typeof args.novoStatus;
|
status: typeof args.novoStatus;
|
||||||
@@ -873,6 +909,26 @@ export const atualizarStatus = mutation({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Se o status foi alterado para Cancelado_RH, recalcular banco de horas
|
||||||
|
// para garantir que os dias de férias sejam removidos do registro de ponto
|
||||||
|
if (args.novoStatus === 'Cancelado_RH') {
|
||||||
|
// IMPORTANTE: Recalcular banco de horas para o período das férias canceladas
|
||||||
|
// Isso garante que os dias de férias sejam removidos corretamente dos registros de ponto
|
||||||
|
try {
|
||||||
|
await recalcularBancoHorasPeriodo(
|
||||||
|
ctx,
|
||||||
|
registro.funcionarioId,
|
||||||
|
registro.dataInicio,
|
||||||
|
registro.dataFim
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
'[ferias.atualizarStatus] Erro ao recalcular banco de horas após cancelamento:',
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Se o status foi alterado para Cancelado_RH, notificar o funcionário
|
// Se o status foi alterado para Cancelado_RH, notificar o funcionário
|
||||||
if (args.novoStatus === 'Cancelado_RH') {
|
if (args.novoStatus === 'Cancelado_RH') {
|
||||||
const funcionario = await ctx.db.get(registro.funcionarioId);
|
const funcionario = await ctx.db.get(registro.funcionarioId);
|
||||||
|
|||||||
@@ -847,224 +847,3 @@ export const obterHistoricoAlertas = query({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Status consolidado do sistema para o dashboard
|
|
||||||
*/
|
|
||||||
export const getStatusSistema = query({
|
|
||||||
args: {},
|
|
||||||
returns: v.object({
|
|
||||||
usuariosOnline: v.number(),
|
|
||||||
totalRegistros: v.number(),
|
|
||||||
tempoMedioResposta: v.number(),
|
|
||||||
cpuUsada: v.number(),
|
|
||||||
memoriaUsada: v.number(),
|
|
||||||
ultimaAtualizacao: v.number()
|
|
||||||
}),
|
|
||||||
handler: async (ctx) => {
|
|
||||||
try {
|
|
||||||
// Última métrica, se existir
|
|
||||||
const ultimaMetrica = (await ctx.db.query('systemMetrics').order('desc').first()) ?? null;
|
|
||||||
|
|
||||||
// Usuários online: usar métrica se disponível, senão derivar de usuários
|
|
||||||
let usuariosOnline = 0;
|
|
||||||
if (ultimaMetrica?.usuariosOnline !== undefined) {
|
|
||||||
usuariosOnline = ultimaMetrica.usuariosOnline;
|
|
||||||
} else {
|
|
||||||
const usuarios = await ctx.db.query('usuarios').collect();
|
|
||||||
usuariosOnline = usuarios.filter((u) => u.statusPresenca === 'online').length;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Total de registros (estimativa baseada em tabelas principais)
|
|
||||||
const [usuarios, funcionarios, simbolos, alertas, metricas] = await Promise.all([
|
|
||||||
ctx.db.query('usuarios').collect(),
|
|
||||||
ctx.db.query('funcionarios').collect(),
|
|
||||||
ctx.db.query('simbolos').collect(),
|
|
||||||
ctx.db.query('alertConfigurations').collect(),
|
|
||||||
ctx.db.query('systemMetrics').take(100) // não precisa contar tudo
|
|
||||||
]);
|
|
||||||
const totalRegistros =
|
|
||||||
usuarios.length + funcionarios.length + simbolos.length + alertas.length + metricas.length;
|
|
||||||
|
|
||||||
// Métricas de performance com fallbacks seguros
|
|
||||||
const tempoMedioResposta = ultimaMetrica?.tempoRespostaMedio ?? 0;
|
|
||||||
const cpuUsada = Math.max(
|
|
||||||
0,
|
|
||||||
Math.min(100, Math.round((ultimaMetrica?.cpuUsage ?? 0) * 100) / 100)
|
|
||||||
);
|
|
||||||
const memoriaUsada = Math.max(
|
|
||||||
0,
|
|
||||||
Math.min(100, Math.round((ultimaMetrica?.memoryUsage ?? 0) * 100) / 100)
|
|
||||||
);
|
|
||||||
const ultimaAtualizacao = ultimaMetrica?.timestamp ?? Date.now();
|
|
||||||
|
|
||||||
return {
|
|
||||||
usuariosOnline,
|
|
||||||
totalRegistros,
|
|
||||||
tempoMedioResposta,
|
|
||||||
cpuUsada,
|
|
||||||
memoriaUsada,
|
|
||||||
ultimaAtualizacao
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erro em getStatusSistema:', error);
|
|
||||||
// Retornar valores padrão em caso de erro
|
|
||||||
return {
|
|
||||||
usuariosOnline: 0,
|
|
||||||
totalRegistros: 0,
|
|
||||||
tempoMedioResposta: 0,
|
|
||||||
cpuUsada: 0,
|
|
||||||
memoriaUsada: 0,
|
|
||||||
ultimaAtualizacao: Date.now()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Atividade do banco no último minuto (agregada em buckets)
|
|
||||||
* Usa logsAtividades e systemMetrics para calcular atividade real.
|
|
||||||
*/
|
|
||||||
export const getAtividadeBancoDados = query({
|
|
||||||
args: {},
|
|
||||||
returns: v.object({
|
|
||||||
historico: v.array(
|
|
||||||
v.object({
|
|
||||||
entradas: v.number(),
|
|
||||||
saidas: v.number()
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
handler: async (ctx) => {
|
|
||||||
try {
|
|
||||||
const agora = Date.now();
|
|
||||||
const haUmMinuto = agora - 60 * 1000;
|
|
||||||
|
|
||||||
// Buscar atividades reais do sistema
|
|
||||||
const atividadesRecentes = await ctx.db
|
|
||||||
.query('logsAtividades')
|
|
||||||
.withIndex('by_timestamp', (q) => q.gte('timestamp', haUmMinuto))
|
|
||||||
.order('asc')
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Buscar métricas também (para mensagens se houver)
|
|
||||||
const metricasRecentes = await ctx.db
|
|
||||||
.query('systemMetrics')
|
|
||||||
.withIndex('by_timestamp', (q) => q.gte('timestamp', haUmMinuto))
|
|
||||||
.order('asc')
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Bucketizar em 30 pontos (~2s cada) para visualização
|
|
||||||
const numBuckets = 30;
|
|
||||||
const bucketSizeMs = Math.ceil(60_000 / numBuckets);
|
|
||||||
const historico: Array<{ entradas: number; saidas: number }> = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < numBuckets; i++) {
|
|
||||||
const inicio = haUmMinuto + i * bucketSizeMs;
|
|
||||||
const fim = inicio + bucketSizeMs;
|
|
||||||
|
|
||||||
// Contar atividades de criação/inserção (entradas)
|
|
||||||
const atividadesBucket = atividadesRecentes.filter(
|
|
||||||
(a) => a.timestamp >= inicio && a.timestamp < fim
|
|
||||||
);
|
|
||||||
const entradasAtividades = atividadesBucket.filter(
|
|
||||||
(a) => a.acao === 'criar' || a.acao === 'inserir' || a.acao === 'cadastrar'
|
|
||||||
).length;
|
|
||||||
|
|
||||||
// Contar atividades de exclusão/remoção (saídas)
|
|
||||||
const saidasAtividades = atividadesBucket.filter(
|
|
||||||
(a) => a.acao === 'excluir' || a.acao === 'remover' || a.acao === 'deletar'
|
|
||||||
).length;
|
|
||||||
|
|
||||||
// Usar mensagensPorMinuto como adicional se disponível
|
|
||||||
const bucketMetricas = metricasRecentes.filter(
|
|
||||||
(m) => m.timestamp >= inicio && m.timestamp < fim
|
|
||||||
);
|
|
||||||
const somaMensagens =
|
|
||||||
bucketMetricas.reduce((acc, m) => acc + (m.mensagensPorMinuto ?? 0), 0) || 0;
|
|
||||||
|
|
||||||
// Combinar atividades reais com métricas de mensagens
|
|
||||||
const entradas = Math.max(0, Math.round(entradasAtividades + somaMensagens * 0.3));
|
|
||||||
const saidas = Math.max(0, Math.round(saidasAtividades + somaMensagens * 0.2));
|
|
||||||
|
|
||||||
historico.push({ entradas, saidas });
|
|
||||||
}
|
|
||||||
|
|
||||||
return { historico };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erro em getAtividadeBancoDados:', error);
|
|
||||||
// Retornar histórico vazio em caso de erro
|
|
||||||
return { historico: Array(30).fill({ entradas: 0, saidas: 0 }) };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Distribuição de operações (calculada a partir de logsAtividades e métricas)
|
|
||||||
*/
|
|
||||||
export const getDistribuicaoRequisicoes = query({
|
|
||||||
args: {},
|
|
||||||
returns: v.object({
|
|
||||||
queries: v.number(),
|
|
||||||
mutations: v.number(),
|
|
||||||
leituras: v.number(),
|
|
||||||
escritas: v.number()
|
|
||||||
}),
|
|
||||||
handler: async (ctx) => {
|
|
||||||
try {
|
|
||||||
const umaHoraAtras = Date.now() - 60 * 60 * 1000;
|
|
||||||
|
|
||||||
// Buscar atividades reais do sistema
|
|
||||||
const atividades = await ctx.db
|
|
||||||
.query('logsAtividades')
|
|
||||||
.withIndex('by_timestamp', (q) => q.gte('timestamp', umaHoraAtras))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Buscar métricas também
|
|
||||||
const metricas = await ctx.db
|
|
||||||
.query('systemMetrics')
|
|
||||||
.withIndex('by_timestamp', (q) => q.gte('timestamp', umaHoraAtras))
|
|
||||||
.order('desc')
|
|
||||||
.take(100);
|
|
||||||
|
|
||||||
// Contar operações de leitura (consultas, visualizações)
|
|
||||||
const leituras = atividades.filter(
|
|
||||||
(a) =>
|
|
||||||
a.acao === 'consultar' ||
|
|
||||||
a.acao === 'visualizar' ||
|
|
||||||
a.acao === 'listar' ||
|
|
||||||
a.acao === 'buscar'
|
|
||||||
).length;
|
|
||||||
|
|
||||||
// Contar operações de escrita (criar, editar, excluir)
|
|
||||||
const escritas = atividades.filter(
|
|
||||||
(a) =>
|
|
||||||
a.acao === 'criar' ||
|
|
||||||
a.acao === 'editar' ||
|
|
||||||
a.acao === 'excluir' ||
|
|
||||||
a.acao === 'inserir' ||
|
|
||||||
a.acao === 'atualizar' ||
|
|
||||||
a.acao === 'deletar' ||
|
|
||||||
a.acao === 'cadastrar' ||
|
|
||||||
a.acao === 'remover'
|
|
||||||
).length;
|
|
||||||
|
|
||||||
// Adicionar estimativa baseada em mensagens se disponível
|
|
||||||
const totalMensagens = Math.max(
|
|
||||||
0,
|
|
||||||
Math.round(metricas.reduce((acc, m) => acc + (m.mensagensPorMinuto ?? 0), 0))
|
|
||||||
);
|
|
||||||
|
|
||||||
// Queries são leituras + parte das mensagens (como consultas de chat)
|
|
||||||
const queries = leituras + Math.round(totalMensagens * 0.5);
|
|
||||||
|
|
||||||
// Mutations são escritas + parte das mensagens (como envio de mensagens)
|
|
||||||
const mutations = escritas + Math.round(totalMensagens * 0.3);
|
|
||||||
|
|
||||||
return { queries, mutations, leituras, escritas };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erro em getDistribuicaoRequisicoes:', error);
|
|
||||||
// Retornar valores padrão em caso de erro
|
|
||||||
return { queries: 0, mutations: 0, leituras: 0, escritas: 0 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -636,45 +636,59 @@ export const registrarPonto = mutation({
|
|||||||
.filter((q) => q.eq(q.field('ativo'), true))
|
.filter((q) => q.eq(q.field('ativo'), true))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
const dataConsulta = new Date(data);
|
// Helper para criar timestamp UTC a partir de data (YYYY-MM-DD), hora e minuto em GMT-3
|
||||||
|
// A hora informada está em GMT-3, então precisamos adicionar 3 horas para obter UTC
|
||||||
|
const offsetGMT3ParaUTC = 3 * 60 * 60 * 1000; // 3 horas em milissegundos
|
||||||
|
function criarTimestampUTCDeGMT3(data: string, hora: number, minuto: number): number {
|
||||||
|
const [ano, mes, dia] = data.split('-').map(Number);
|
||||||
|
return Date.UTC(ano, mes - 1, dia, hora, minuto, 0, 0) + offsetGMT3ParaUTC;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper para criar timestamp UTC a partir de data (YYYY-MM-DD), hora e minuto que já estão em UTC
|
||||||
|
function criarTimestampUTC(data: string, horaUTC: number, minutoUTC: number): number {
|
||||||
|
const [ano, mes, dia] = data.split('-').map(Number);
|
||||||
|
return Date.UTC(ano, mes - 1, dia, horaUTC, minutoUTC, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obter timestamp atual em UTC
|
||||||
|
const agoraUTC = new Date();
|
||||||
|
const agoraTimestampUTC = agoraUTC.getTime();
|
||||||
|
|
||||||
|
// Timestamp da consulta (registro sendo feito) em UTC
|
||||||
|
// hora/minuto já estão em UTC (extraídos com getUTCHours/getUTCMinutes)
|
||||||
|
const timestampConsultaUTC = criarTimestampUTC(data, hora, minuto);
|
||||||
|
|
||||||
for (const dispensa of dispensas) {
|
for (const dispensa of dispensas) {
|
||||||
// Se for isento, sempre está dispensado
|
// Se for isento, sempre está dispensado
|
||||||
if (dispensa.isento) {
|
if (dispensa.isento) {
|
||||||
throw new Error('Registro dispensado pelo gestor: Isento de registro (caso excepcional)');
|
throw new Error('Registro dispensado pelo gestor: Isento de registro (caso excepcional)');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verificar se está no período
|
// Calcular timestamps de início e fim da dispensa em UTC
|
||||||
const dataInicio = new Date(dispensa.dataInicio);
|
const timestampInicioUTC = criarTimestampUTCDeGMT3(
|
||||||
const dataFim = new Date(dispensa.dataFim);
|
dispensa.dataInicio,
|
||||||
|
dispensa.horaInicio,
|
||||||
|
dispensa.minutoInicio
|
||||||
|
);
|
||||||
|
const timestampFimUTC = criarTimestampUTCDeGMT3(
|
||||||
|
dispensa.dataFim,
|
||||||
|
dispensa.horaFim,
|
||||||
|
dispensa.minutoFim
|
||||||
|
);
|
||||||
|
|
||||||
if (dataConsulta >= dataInicio && dataConsulta <= dataFim) {
|
// Desativar dispensa expirada ANTES de verificar bloqueio (após o fim)
|
||||||
// Verificar hora e minuto se necessário
|
// Verificar se AGORA já passou do horário de fim da dispensa
|
||||||
const timestampConsulta = new Date(
|
if (agoraTimestampUTC > timestampFimUTC) {
|
||||||
`${data}T${hora.toString().padStart(2, '0')}:${minuto.toString().padStart(2, '0')}:00`
|
|
||||||
).getTime();
|
|
||||||
const timestampInicio = new Date(
|
|
||||||
`${dispensa.dataInicio}T${dispensa.horaInicio.toString().padStart(2, '0')}:${dispensa.minutoInicio.toString().padStart(2, '0')}:00`
|
|
||||||
).getTime();
|
|
||||||
const timestampFim = new Date(
|
|
||||||
`${dispensa.dataFim}T${dispensa.horaFim.toString().padStart(2, '0')}:${dispensa.minutoFim.toString().padStart(2, '0')}:00`
|
|
||||||
).getTime();
|
|
||||||
|
|
||||||
if (timestampConsulta >= timestampInicio && timestampConsulta <= timestampFim) {
|
|
||||||
throw new Error(`Registro dispensado pelo gestor: ${dispensa.motivo}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verificar se expirou (desativar na mutation de registro)
|
|
||||||
const agora = new Date();
|
|
||||||
const dataFimTimestamp = new Date(
|
|
||||||
`${dispensa.dataFim}T${dispensa.horaFim.toString().padStart(2, '0')}:${dispensa.minutoFim.toString().padStart(2, '0')}:00`
|
|
||||||
).getTime();
|
|
||||||
|
|
||||||
if (agora.getTime() > dataFimTimestamp && !dispensa.isento) {
|
|
||||||
// Desativar dispensa expirada (mutation pode fazer isso)
|
|
||||||
await ctx.db.patch(dispensa._id, {
|
await ctx.db.patch(dispensa._id, {
|
||||||
ativo: false
|
ativo: false
|
||||||
});
|
});
|
||||||
|
continue; // Pular verificação de bloqueio se já expirou
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar se AGORA está dentro do período da dispensa (não o horário do registro)
|
||||||
|
// Se o momento atual está dentro do período, bloqueia qualquer tentativa de registro
|
||||||
|
if (agoraTimestampUTC >= timestampInicioUTC && agoraTimestampUTC <= timestampFimUTC) {
|
||||||
|
throw new Error(`Registro dispensado pelo gestor: ${dispensa.motivo}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1400,78 +1414,45 @@ function calcularHorasTrabalhadas(
|
|||||||
return minutosA - minutosB;
|
return minutosA - minutosB;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Procurar registros principais
|
let totalMinutos = 0;
|
||||||
const entrada = registrosOrdenados.find((r) => r.tipo === 'entrada');
|
let entradaPendente: { hora: number; minuto: number } | null = null;
|
||||||
const saidaAlmoco = registrosOrdenados.find((r) => r.tipo === 'saida_almoco');
|
|
||||||
const retornoAlmoco = registrosOrdenados.find((r) => r.tipo === 'retorno_almoco');
|
|
||||||
const saida = registrosOrdenados.find((r) => r.tipo === 'saida');
|
|
||||||
|
|
||||||
// Caso 1: Tem entrada e saída completas
|
// Processar registros sequencialmente para capturar todos os períodos de trabalho
|
||||||
if (entrada && saida) {
|
// Isso permite calcular múltiplas entradas/saídas no mesmo dia
|
||||||
const minutosEntrada = entrada.hora * 60 + entrada.minuto;
|
for (const registro of registrosOrdenados) {
|
||||||
const minutosSaida = saida.hora * 60 + saida.minuto;
|
const minutosRegistro = registro.hora * 60 + registro.minuto;
|
||||||
|
|
||||||
// Caso 1.1: Tem intervalo de almoço completo (saída almoço + retorno almoço)
|
if (registro.tipo === 'entrada') {
|
||||||
if (saidaAlmoco && retornoAlmoco) {
|
// Se já havia uma entrada pendente sem saída, ignorar a anterior (inconsistência)
|
||||||
const minutosSaidaAlmoco = saidaAlmoco.hora * 60 + saidaAlmoco.minuto;
|
// e usar a nova entrada
|
||||||
const minutosRetornoAlmoco = retornoAlmoco.hora * 60 + retornoAlmoco.minuto;
|
entradaPendente = { hora: registro.hora, minuto: registro.minuto };
|
||||||
|
} else if (registro.tipo === 'saida_almoco') {
|
||||||
// Validar ordem lógica
|
// Se há entrada pendente, calcular período da manhã
|
||||||
if (
|
if (entradaPendente) {
|
||||||
minutosSaidaAlmoco > minutosEntrada &&
|
const minutosEntrada = entradaPendente.hora * 60 + entradaPendente.minuto;
|
||||||
minutosRetornoAlmoco > minutosSaidaAlmoco &&
|
if (minutosRegistro > minutosEntrada) {
|
||||||
minutosSaida > minutosRetornoAlmoco
|
totalMinutos += minutosRegistro - minutosEntrada;
|
||||||
) {
|
|
||||||
const horasManha = minutosSaidaAlmoco - minutosEntrada;
|
|
||||||
const horasTarde = minutosSaida - minutosRetornoAlmoco;
|
|
||||||
return horasManha + horasTarde;
|
|
||||||
}
|
}
|
||||||
|
// Limpar entrada pendente após saída almoço (aguardar retorno)
|
||||||
|
entradaPendente = null;
|
||||||
}
|
}
|
||||||
|
} else if (registro.tipo === 'retorno_almoco') {
|
||||||
// Caso 1.2: Tem apenas saída almoço (sem retorno) - considerar apenas manhã
|
// Marcar como nova entrada para período da tarde
|
||||||
if (saidaAlmoco && !retornoAlmoco) {
|
entradaPendente = { hora: registro.hora, minuto: registro.minuto };
|
||||||
const minutosSaidaAlmoco = saidaAlmoco.hora * 60 + saidaAlmoco.minuto;
|
} else if (registro.tipo === 'saida') {
|
||||||
if (minutosSaidaAlmoco > minutosEntrada) {
|
// Se há entrada pendente (pode ser entrada inicial ou retorno almoço), calcular período
|
||||||
return minutosSaidaAlmoco - minutosEntrada;
|
if (entradaPendente) {
|
||||||
|
const minutosEntrada = entradaPendente.hora * 60 + entradaPendente.minuto;
|
||||||
|
if (minutosRegistro > minutosEntrada) {
|
||||||
|
totalMinutos += minutosRegistro - minutosEntrada;
|
||||||
}
|
}
|
||||||
}
|
// Limpar entrada pendente após saída
|
||||||
|
entradaPendente = null;
|
||||||
// Caso 1.3: Tem apenas retorno almoço (sem saída) - considerar apenas tarde
|
|
||||||
if (!saidaAlmoco && retornoAlmoco) {
|
|
||||||
const minutosRetornoAlmoco = retornoAlmoco.hora * 60 + retornoAlmoco.minuto;
|
|
||||||
if (minutosSaida > minutosRetornoAlmoco) {
|
|
||||||
return minutosSaida - minutosRetornoAlmoco;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Caso 1.4: Sem intervalo de almoço registrado - calcular direto
|
|
||||||
if (!saidaAlmoco && !retornoAlmoco) {
|
|
||||||
if (minutosSaida > minutosEntrada) {
|
|
||||||
return minutosSaida - minutosEntrada;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Caso 2: Tem apenas entrada (sem saída) - retornar 0 (dia incompleto)
|
return totalMinutos;
|
||||||
if (entrada && !saida) {
|
|
||||||
// Se tiver saída almoço, considerar até a saída almoço
|
|
||||||
if (saidaAlmoco) {
|
|
||||||
const minutosEntrada = entrada.hora * 60 + entrada.minuto;
|
|
||||||
const minutosSaidaAlmoco = saidaAlmoco.hora * 60 + saidaAlmoco.minuto;
|
|
||||||
if (minutosSaidaAlmoco > minutosEntrada) {
|
|
||||||
return minutosSaidaAlmoco - minutosEntrada;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Caso 3: Tem apenas saída (sem entrada) - inconsistência, retornar 0
|
|
||||||
if (saida && !entrada) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Caso 4: Não tem entrada nem saída - retornar 0
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1568,6 +1549,54 @@ async function verificarAusenciaAprovada(
|
|||||||
return { temAusencia: false };
|
return { temAusencia: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove ajustes automáticos relacionados a um registro excluído
|
||||||
|
* Busca e remove ajustes que referenciam o motivoId fornecido
|
||||||
|
*/
|
||||||
|
async function removerAjustesAutomaticos(
|
||||||
|
ctx: MutationCtx,
|
||||||
|
funcionarioId: Id<'funcionarios'>,
|
||||||
|
motivoTipo: 'atestado' | 'licenca' | 'ausencia',
|
||||||
|
motivoId: string,
|
||||||
|
dataInicio: string,
|
||||||
|
dataFim: string
|
||||||
|
): Promise<void> {
|
||||||
|
// Gerar todas as datas do período
|
||||||
|
const dataInicioObj = new Date(dataInicio);
|
||||||
|
const dataFimObj = new Date(dataFim);
|
||||||
|
const datas: string[] = [];
|
||||||
|
const dataAtual = new Date(dataInicioObj);
|
||||||
|
|
||||||
|
while (dataAtual <= dataFimObj) {
|
||||||
|
const ano = dataAtual.getFullYear();
|
||||||
|
const mes = String(dataAtual.getMonth() + 1).padStart(2, '0');
|
||||||
|
const dia = String(dataAtual.getDate()).padStart(2, '0');
|
||||||
|
datas.push(`${ano}-${mes}-${dia}`);
|
||||||
|
dataAtual.setDate(dataAtual.getDate() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buscar todos os ajustes automáticos relacionados ao motivoId no período
|
||||||
|
for (const data of datas) {
|
||||||
|
const ajustes = await ctx.db
|
||||||
|
.query('ajustesBancoHoras')
|
||||||
|
.filter((q) =>
|
||||||
|
q.and(
|
||||||
|
q.eq(q.field('funcionarioId'), funcionarioId),
|
||||||
|
q.eq(q.field('dataAplicacao'), data),
|
||||||
|
q.eq(q.field('motivoTipo'), motivoTipo),
|
||||||
|
q.eq(q.field('motivoId'), motivoId),
|
||||||
|
q.eq(q.field('aplicado'), true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Remover cada ajuste encontrado
|
||||||
|
for (const ajuste of ajustes) {
|
||||||
|
await ctx.db.delete(ajuste._id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifica ajustes manuais aplicados no dia
|
* Verifica ajustes manuais aplicados no dia
|
||||||
*/
|
*/
|
||||||
@@ -1726,10 +1755,13 @@ async function atualizarBancoHoras(
|
|||||||
const ajustesIds: Array<Id<'ajustesBancoHoras'>> = [];
|
const ajustesIds: Array<Id<'ajustesBancoHoras'>> = [];
|
||||||
|
|
||||||
// Aplicar ajustes automáticos se houver atestado, licença ou ausência
|
// Aplicar ajustes automáticos se houver atestado, licença ou ausência
|
||||||
if (atestadoInfo.temAtestado) {
|
// IMPORTANTE: Verificar se o registro ainda existe antes de criar ajuste
|
||||||
|
if (atestadoInfo.temAtestado && atestadoInfo.atestadoId) {
|
||||||
|
// Verificar se o atestado ainda existe no banco
|
||||||
|
const atestado = await ctx.db.get(atestadoInfo.atestadoId as Id<'atestados'>);
|
||||||
|
if (atestado) {
|
||||||
tipoDia = 'atestado';
|
tipoDia = 'atestado';
|
||||||
motivoAbono = atestadoInfo.motivo;
|
motivoAbono = atestadoInfo.motivo;
|
||||||
if (atestadoInfo.atestadoId) {
|
|
||||||
const ajusteId = await aplicarAjusteAutomatico(
|
const ajusteId = await aplicarAjusteAutomatico(
|
||||||
ctx,
|
ctx,
|
||||||
funcionarioId,
|
funcionarioId,
|
||||||
@@ -1741,10 +1773,12 @@ async function atualizarBancoHoras(
|
|||||||
);
|
);
|
||||||
ajustesIds.push(ajusteId);
|
ajustesIds.push(ajusteId);
|
||||||
}
|
}
|
||||||
} else if (licencaInfo.temLicenca) {
|
} else if (licencaInfo.temLicenca && licencaInfo.licencaId) {
|
||||||
|
// Verificar se a licença ainda existe no banco
|
||||||
|
const licenca = await ctx.db.get(licencaInfo.licencaId as Id<'licencas'>);
|
||||||
|
if (licenca) {
|
||||||
tipoDia = 'licenca';
|
tipoDia = 'licenca';
|
||||||
motivoAbono = licencaInfo.motivo;
|
motivoAbono = licencaInfo.motivo;
|
||||||
if (licencaInfo.licencaId) {
|
|
||||||
const ajusteId = await aplicarAjusteAutomatico(
|
const ajusteId = await aplicarAjusteAutomatico(
|
||||||
ctx,
|
ctx,
|
||||||
funcionarioId,
|
funcionarioId,
|
||||||
@@ -1756,10 +1790,12 @@ async function atualizarBancoHoras(
|
|||||||
);
|
);
|
||||||
ajustesIds.push(ajusteId);
|
ajustesIds.push(ajusteId);
|
||||||
}
|
}
|
||||||
} else if (ausenciaInfo.temAusencia) {
|
} else if (ausenciaInfo.temAusencia && ausenciaInfo.ausenciaId) {
|
||||||
|
// Verificar se a ausência ainda existe no banco e está aprovada
|
||||||
|
const ausencia = await ctx.db.get(ausenciaInfo.ausenciaId as Id<'solicitacoesAusencias'>);
|
||||||
|
if (ausencia && ausencia.status === 'aprovado') {
|
||||||
tipoDia = 'ausencia';
|
tipoDia = 'ausencia';
|
||||||
motivoAbono = ausenciaInfo.motivo;
|
motivoAbono = ausenciaInfo.motivo;
|
||||||
if (ausenciaInfo.ausenciaId) {
|
|
||||||
const ajusteId = await aplicarAjusteAutomatico(
|
const ajusteId = await aplicarAjusteAutomatico(
|
||||||
ctx,
|
ctx,
|
||||||
funcionarioId,
|
funcionarioId,
|
||||||
@@ -2582,6 +2618,14 @@ export const ajustarBancoHoras = mutation({
|
|||||||
periodoDias: v.number(),
|
periodoDias: v.number(),
|
||||||
periodoHoras: v.number(),
|
periodoHoras: v.number(),
|
||||||
periodoMinutos: v.number(),
|
periodoMinutos: v.number(),
|
||||||
|
dataAplicacao: v.string(), // YYYY-MM-DD - Data em que o ajuste deve ser aplicado
|
||||||
|
// Período do ajuste (data/hora início e fim)
|
||||||
|
dataInicio: v.optional(v.string()), // YYYY-MM-DD
|
||||||
|
horaInicio: v.optional(v.number()), // 0-23
|
||||||
|
minutoInicio: v.optional(v.number()), // 0-59
|
||||||
|
dataFim: v.optional(v.string()), // YYYY-MM-DD
|
||||||
|
horaFim: v.optional(v.number()), // 0-23
|
||||||
|
minutoFim: v.optional(v.number()), // 0-59
|
||||||
motivoId: v.optional(v.string()),
|
motivoId: v.optional(v.string()),
|
||||||
motivoTipo: v.optional(v.string()),
|
motivoTipo: v.optional(v.string()),
|
||||||
motivoDescricao: v.optional(v.string()),
|
motivoDescricao: v.optional(v.string()),
|
||||||
@@ -2619,8 +2663,8 @@ export const ajustarBancoHoras = mutation({
|
|||||||
ajusteFinal = -ajusteMinutos;
|
ajusteFinal = -ajusteMinutos;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buscar banco de horas mais recente ou criar um registro de ajuste
|
// Usar a data de aplicação fornecida pelo usuário
|
||||||
const hoje = new Date().toISOString().split('T')[0]!;
|
const dataAplicacao = args.dataAplicacao;
|
||||||
|
|
||||||
// Criar registro de ajuste na nova tabela
|
// Criar registro de ajuste na nova tabela
|
||||||
const ajusteId = await ctx.db.insert('ajustesBancoHoras', {
|
const ajusteId = await ctx.db.insert('ajustesBancoHoras', {
|
||||||
@@ -2630,7 +2674,13 @@ export const ajustarBancoHoras = mutation({
|
|||||||
motivoId: args.motivoId,
|
motivoId: args.motivoId,
|
||||||
motivoDescricao: args.motivoDescricao || `Ajuste ${args.tipoAjuste}`,
|
motivoDescricao: args.motivoDescricao || `Ajuste ${args.tipoAjuste}`,
|
||||||
valorMinutos: ajusteFinal,
|
valorMinutos: ajusteFinal,
|
||||||
dataAplicacao: hoje,
|
dataAplicacao: dataAplicacao,
|
||||||
|
dataInicio: args.dataInicio,
|
||||||
|
horaInicio: args.horaInicio,
|
||||||
|
minutoInicio: args.minutoInicio,
|
||||||
|
dataFim: args.dataFim,
|
||||||
|
horaFim: args.horaFim,
|
||||||
|
minutoFim: args.minutoFim,
|
||||||
gestorId: usuario._id,
|
gestorId: usuario._id,
|
||||||
observacoes: args.observacoes,
|
observacoes: args.observacoes,
|
||||||
aplicado: false,
|
aplicado: false,
|
||||||
@@ -2640,7 +2690,7 @@ export const ajustarBancoHoras = mutation({
|
|||||||
const bancoHorasAtual = await ctx.db
|
const bancoHorasAtual = await ctx.db
|
||||||
.query('bancoHoras')
|
.query('bancoHoras')
|
||||||
.withIndex('by_funcionario_data', (q) =>
|
.withIndex('by_funcionario_data', (q) =>
|
||||||
q.eq('funcionarioId', args.funcionarioId).eq('data', hoje)
|
q.eq('funcionarioId', args.funcionarioId).eq('data', dataAplicacao)
|
||||||
)
|
)
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
@@ -2672,7 +2722,7 @@ export const ajustarBancoHoras = mutation({
|
|||||||
|
|
||||||
await ctx.db.insert('bancoHoras', {
|
await ctx.db.insert('bancoHoras', {
|
||||||
funcionarioId: args.funcionarioId,
|
funcionarioId: args.funcionarioId,
|
||||||
data: hoje,
|
data: dataAplicacao,
|
||||||
cargaHorariaDiaria,
|
cargaHorariaDiaria,
|
||||||
horasTrabalhadas: 0,
|
horasTrabalhadas: 0,
|
||||||
saldoMinutos: ajusteFinal,
|
saldoMinutos: ajusteFinal,
|
||||||
@@ -2691,7 +2741,7 @@ export const ajustarBancoHoras = mutation({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Recalcular banco de horas mensal após ajuste
|
// Recalcular banco de horas mensal após ajuste
|
||||||
const mes = hoje.substring(0, 7); // YYYY-MM
|
const mes = dataAplicacao.substring(0, 7); // YYYY-MM
|
||||||
|
|
||||||
// Verificar se estamos ajustando um mês passado
|
// Verificar se estamos ajustando um mês passado
|
||||||
const hojeDate = new Date();
|
const hojeDate = new Date();
|
||||||
@@ -2702,6 +2752,7 @@ export const ajustarBancoHoras = mutation({
|
|||||||
await calcularBancoHorasMensal(ctx, args.funcionarioId, mes, estaAjustandoMesPassado);
|
await calcularBancoHorasMensal(ctx, args.funcionarioId, mes, estaAjustandoMesPassado);
|
||||||
|
|
||||||
// Criar registro de homologação (mantido para compatibilidade)
|
// Criar registro de homologação (mantido para compatibilidade)
|
||||||
|
// Armazenar o ajusteId para facilitar a busca posterior
|
||||||
const homologacaoId = await ctx.db.insert('homologacoesPonto', {
|
const homologacaoId = await ctx.db.insert('homologacoesPonto', {
|
||||||
funcionarioId: args.funcionarioId,
|
funcionarioId: args.funcionarioId,
|
||||||
gestorId: usuario._id,
|
gestorId: usuario._id,
|
||||||
@@ -2717,6 +2768,9 @@ export const ajustarBancoHoras = mutation({
|
|||||||
criadoEm: Date.now()
|
criadoEm: Date.now()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Armazenar ajusteId na homologação usando um campo customizado ou buscar depois
|
||||||
|
// Por enquanto, vamos buscar o ajuste no listarHomologacoes usando os critérios
|
||||||
|
|
||||||
return { success: true, homologacaoId, ajusteId, ajusteMinutos: ajusteFinal };
|
return { success: true, homologacaoId, ajusteId, ajusteMinutos: ajusteFinal };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -2784,6 +2838,62 @@ export const listarHomologacoes = query({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Buscar dataAplicacao e período do ajuste se for um ajuste de banco de horas
|
||||||
|
let dataAplicacaoAjuste: string | null = null;
|
||||||
|
let periodoAjuste: {
|
||||||
|
dataInicio?: string;
|
||||||
|
horaInicio?: number;
|
||||||
|
minutoInicio?: number;
|
||||||
|
dataFim?: string;
|
||||||
|
horaFim?: number;
|
||||||
|
minutoFim?: number;
|
||||||
|
} | null = null;
|
||||||
|
if (h.tipoAjuste && h.ajusteMinutos !== undefined) {
|
||||||
|
// Buscar ajustes criados próximo ao tempo da homologação (dentro de 5 minutos antes)
|
||||||
|
// O ajuste é criado logo antes da homologação em ajustarBancoHoras
|
||||||
|
const tempoLimite = h.criadoEm - 5 * 60 * 1000; // 5 minutos antes
|
||||||
|
|
||||||
|
// Buscar todos os ajustes do funcionário com os mesmos critérios
|
||||||
|
// e criados próximo ao tempo da homologação
|
||||||
|
const ajustes = await ctx.db
|
||||||
|
.query('ajustesBancoHoras')
|
||||||
|
.withIndex('by_funcionario', (q) => q.eq('funcionarioId', h.funcionarioId))
|
||||||
|
.filter((q) =>
|
||||||
|
q.and(
|
||||||
|
q.eq(q.field('motivoTipo'), 'manual'),
|
||||||
|
q.eq(q.field('tipo'), h.tipoAjuste),
|
||||||
|
q.eq(q.field('valorMinutos'), h.ajusteMinutos),
|
||||||
|
q.eq(q.field('gestorId'), h.gestorId),
|
||||||
|
q.gte(q.field('criadoEm'), tempoLimite),
|
||||||
|
q.lte(q.field('criadoEm'), h.criadoEm)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Se encontrou ajuste(s), encontrar o mais próximo em tempo à homologação
|
||||||
|
if (ajustes.length > 0) {
|
||||||
|
// Encontrar o ajuste com timestamp mais próximo ao da homologação
|
||||||
|
let ajusteMaisProximo = ajustes[0]!;
|
||||||
|
let menorDiferenca = Math.abs(ajustes[0]!.criadoEm - h.criadoEm);
|
||||||
|
for (const ajusteCandidato of ajustes) {
|
||||||
|
const diferenca = Math.abs(ajusteCandidato.criadoEm - h.criadoEm);
|
||||||
|
if (diferenca < menorDiferenca) {
|
||||||
|
menorDiferenca = diferenca;
|
||||||
|
ajusteMaisProximo = ajusteCandidato;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dataAplicacaoAjuste = ajusteMaisProximo.dataAplicacao;
|
||||||
|
periodoAjuste = {
|
||||||
|
dataInicio: ajusteMaisProximo.dataInicio,
|
||||||
|
horaInicio: ajusteMaisProximo.horaInicio,
|
||||||
|
minutoInicio: ajusteMaisProximo.minutoInicio,
|
||||||
|
dataFim: ajusteMaisProximo.dataFim,
|
||||||
|
horaFim: ajusteMaisProximo.horaFim,
|
||||||
|
minutoFim: ajusteMaisProximo.minutoFim
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...h,
|
...h,
|
||||||
funcionario: funcionario
|
funcionario: funcionario
|
||||||
@@ -2803,7 +2913,9 @@ export const listarHomologacoes = query({
|
|||||||
data: registro.data,
|
data: registro.data,
|
||||||
tipo: registro.tipo
|
tipo: registro.tipo
|
||||||
}
|
}
|
||||||
: null
|
: null,
|
||||||
|
dataAplicacaoAjuste,
|
||||||
|
periodoAjuste
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -2845,14 +2957,157 @@ export const excluirHomologacao = mutation({
|
|||||||
throw new Error('Você não tem permissão para excluir esta homologação');
|
throw new Error('Você não tem permissão para excluir esta homologação');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Se a homologação estiver vinculada a um registro, remover a referência
|
// Se a homologação estiver vinculada a um registro, restaurar valores originais
|
||||||
if (homologacao.registroId) {
|
if (homologacao.registroId) {
|
||||||
const registro = await ctx.db.get(homologacao.registroId);
|
const registro = await ctx.db.get(homologacao.registroId);
|
||||||
if (registro && registro.homologacaoId === args.homologacaoId) {
|
if (registro && registro.homologacaoId === args.homologacaoId) {
|
||||||
await ctx.db.patch(homologacao.registroId, {
|
// Restaurar valores originais se existirem
|
||||||
|
const patchData: {
|
||||||
|
homologacaoId: undefined;
|
||||||
|
editadoPorGestor: boolean;
|
||||||
|
hora?: number;
|
||||||
|
minuto?: number;
|
||||||
|
} = {
|
||||||
homologacaoId: undefined,
|
homologacaoId: undefined,
|
||||||
editadoPorGestor: false
|
editadoPorGestor: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Se a homologação tem valores anteriores, restaurar
|
||||||
|
if (homologacao.horaAnterior !== undefined && homologacao.minutoAnterior !== undefined) {
|
||||||
|
patchData.hora = homologacao.horaAnterior;
|
||||||
|
patchData.minuto = homologacao.minutoAnterior;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.db.patch(homologacao.registroId, patchData);
|
||||||
|
|
||||||
|
// Recalcular banco de horas após restaurar valores
|
||||||
|
const config = await ctx.db
|
||||||
|
.query('configuracaoPonto')
|
||||||
|
.withIndex('by_ativo', (q) => q.eq('ativo', true))
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (config) {
|
||||||
|
await atualizarBancoHoras(ctx, registro.funcionarioId, registro.data, config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se for um ajuste de banco de horas, remover completamente do banco de dados
|
||||||
|
if (homologacao.tipoAjuste && homologacao.ajusteMinutos !== undefined) {
|
||||||
|
// Buscar ajustes criados próximo ao tempo da homologação (dentro de 5 minutos antes)
|
||||||
|
// O ajuste é criado logo antes da homologação em ajustarBancoHoras
|
||||||
|
const tempoLimite = homologacao.criadoEm - 5 * 60 * 1000; // 5 minutos antes
|
||||||
|
|
||||||
|
// Buscar todos os ajustes do funcionário com os mesmos critérios
|
||||||
|
// e criados próximo ao tempo da homologação
|
||||||
|
const ajustes = await ctx.db
|
||||||
|
.query('ajustesBancoHoras')
|
||||||
|
.withIndex('by_funcionario', (q) => q.eq('funcionarioId', homologacao.funcionarioId))
|
||||||
|
.filter((q) =>
|
||||||
|
q.and(
|
||||||
|
q.eq(q.field('motivoTipo'), 'manual'),
|
||||||
|
q.eq(q.field('tipo'), homologacao.tipoAjuste),
|
||||||
|
q.eq(q.field('valorMinutos'), homologacao.ajusteMinutos),
|
||||||
|
q.eq(q.field('gestorId'), homologacao.gestorId),
|
||||||
|
q.gte(q.field('criadoEm'), tempoLimite),
|
||||||
|
q.lte(q.field('criadoEm'), homologacao.criadoEm)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Se encontrou ajuste(s), encontrar o mais próximo em tempo à homologação
|
||||||
|
if (ajustes.length > 0) {
|
||||||
|
// Encontrar o ajuste com timestamp mais próximo ao da homologação
|
||||||
|
// (o ajuste geralmente é criado um pouco antes da homologação)
|
||||||
|
let ajusteMaisProximo = ajustes[0]!;
|
||||||
|
let menorDiferenca = Math.abs(ajustes[0]!.criadoEm - homologacao.criadoEm);
|
||||||
|
for (const ajusteCandidato of ajustes) {
|
||||||
|
const diferenca = Math.abs(ajusteCandidato.criadoEm - homologacao.criadoEm);
|
||||||
|
if (diferenca < menorDiferenca) {
|
||||||
|
menorDiferenca = diferenca;
|
||||||
|
ajusteMaisProximo = ajusteCandidato;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const ajuste = ajusteMaisProximo;
|
||||||
|
|
||||||
|
// Buscar o banco de horas do dia onde o ajuste foi aplicado
|
||||||
|
const bancoHoras = await ctx.db
|
||||||
|
.query('bancoHoras')
|
||||||
|
.withIndex('by_funcionario_data', (q) =>
|
||||||
|
q.eq('funcionarioId', homologacao.funcionarioId).eq('data', ajuste.dataAplicacao)
|
||||||
|
)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (bancoHoras) {
|
||||||
|
// Remover o ajuste do array ajustesIds
|
||||||
|
const novosAjustesIds = (bancoHoras.ajustesIds || []).filter(
|
||||||
|
(id) => id !== ajuste._id
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reverter o ajuste do saldo (subtrair o valor que foi adicionado)
|
||||||
|
const novoSaldoMinutos = bancoHoras.saldoMinutos - ajuste.valorMinutos;
|
||||||
|
|
||||||
|
// Verificar se ainda há outros ajustes ou se precisa resetar tipoDia
|
||||||
|
let novoTipoDia = bancoHoras.tipoDia;
|
||||||
|
if (novosAjustesIds.length > 0) {
|
||||||
|
// Se ainda há outros ajustes, verificar qual tipoDia deve ser mantido
|
||||||
|
const outrosAjustes = await Promise.all(
|
||||||
|
novosAjustesIds.map((id) => ctx.db.get(id))
|
||||||
|
);
|
||||||
|
const temAjusteAbonar = outrosAjustes.some((a) => a?.tipo === 'abonar');
|
||||||
|
const temAjusteDescontar = outrosAjustes.some((a) => a?.tipo === 'descontar');
|
||||||
|
|
||||||
|
// Se há ajuste de abonar, manter ou definir como 'abonado'
|
||||||
|
if (temAjusteAbonar) {
|
||||||
|
novoTipoDia = 'abonado';
|
||||||
|
} else if (temAjusteDescontar) {
|
||||||
|
// Se há ajuste de descontar, manter ou definir como 'descontado'
|
||||||
|
novoTipoDia = 'descontado';
|
||||||
|
} else {
|
||||||
|
// Se não há ajustes que determinem tipoDia, resetar
|
||||||
|
novoTipoDia = undefined;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Se não há mais ajustes, verificar se deve resetar tipoDia
|
||||||
|
// Se o tipoDia estava relacionado ao ajuste removido, resetar
|
||||||
|
if (
|
||||||
|
(bancoHoras.tipoDia === 'abonado' && ajuste.tipo === 'abonar') ||
|
||||||
|
(bancoHoras.tipoDia === 'descontado' && ajuste.tipo === 'descontar')
|
||||||
|
) {
|
||||||
|
novoTipoDia = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atualizar banco de horas
|
||||||
|
await ctx.db.patch(bancoHoras._id, {
|
||||||
|
saldoMinutos: novoSaldoMinutos,
|
||||||
|
ajustesIds: novosAjustesIds.length > 0 ? novosAjustesIds : undefined,
|
||||||
|
tipoDia: novoTipoDia
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Recalcular banco de horas do dia específico para garantir consistência
|
||||||
|
const config = await ctx.db
|
||||||
|
.query('configuracaoPonto')
|
||||||
|
.withIndex('by_ativo', (q) => q.eq('ativo', true))
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (config) {
|
||||||
|
// Recalcular banco de horas do dia para atualizar valores baseados nos registros
|
||||||
|
await atualizarBancoHoras(ctx, homologacao.funcionarioId, ajuste.dataAplicacao, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalcular banco de horas mensal após remover ajuste
|
||||||
|
const mes = ajuste.dataAplicacao.substring(0, 7); // YYYY-MM
|
||||||
|
const hojeDate = new Date();
|
||||||
|
const mesAtual = `${hojeDate.getFullYear()}-${String(hojeDate.getMonth() + 1).padStart(2, '0')}`;
|
||||||
|
const estaRemovendoMesPassado = mes < mesAtual;
|
||||||
|
|
||||||
|
// Recalcular em cascata se for mês passado
|
||||||
|
await calcularBancoHorasMensal(ctx, homologacao.funcionarioId, mes, estaRemovendoMesPassado);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Excluir o registro de ajuste do banco de dados
|
||||||
|
await ctx.db.delete(ajuste._id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2929,11 +3184,9 @@ export const criarDispensaRegistro = mutation({
|
|||||||
throw new Error('Você não tem permissão para criar dispensa para este funcionário');
|
throw new Error('Você não tem permissão para criar dispensa para este funcionário');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validar datas
|
// Validar datas (comparar strings diretamente para evitar problemas de timezone)
|
||||||
const dataInicioObj = new Date(args.dataInicio);
|
// Formato YYYY-MM-DD permite comparação lexicográfica
|
||||||
const dataFimObj = new Date(args.dataFim);
|
if (args.dataFim < args.dataInicio) {
|
||||||
|
|
||||||
if (dataFimObj < dataInicioObj) {
|
|
||||||
throw new Error('Data fim deve ser maior ou igual à data início');
|
throw new Error('Data fim deve ser maior ou igual à data início');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2986,10 +3239,8 @@ export const removerDispensaRegistro = mutation({
|
|||||||
throw new Error('Você não tem permissão para remover esta dispensa');
|
throw new Error('Você não tem permissão para remover esta dispensa');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Desativar dispensa
|
// Deletar dispensa do banco de dados
|
||||||
await ctx.db.patch(args.dispensaId, {
|
await ctx.db.delete(args.dispensaId);
|
||||||
ativo: false
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
@@ -3070,14 +3321,49 @@ export const listarDispensas = query({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verificar se expirou (se não for isento)
|
// Verificar se está ativa ou expirada (considerando data, hora e minuto em GMT-3)
|
||||||
let expirada = false;
|
let expirada = false;
|
||||||
|
|
||||||
|
// GMT-3 está 3 horas ATRÁS do UTC
|
||||||
|
// Offset: +3 horas para converter GMT-3 para UTC
|
||||||
|
const offsetGMT3ParaUTC = 3 * 60 * 60 * 1000; // 3 horas em milissegundos
|
||||||
|
|
||||||
|
// Obter data/hora atual em UTC
|
||||||
|
const agoraUTC = new Date();
|
||||||
|
const agoraTimestampUTC = agoraUTC.getTime();
|
||||||
|
|
||||||
|
// Helper para criar timestamp UTC a partir de data (YYYY-MM-DD), hora e minuto em GMT-3
|
||||||
|
// A hora informada está em GMT-3, então precisamos adicionar 3 horas para obter UTC
|
||||||
|
// Exemplo: 08:00 GMT-3 = 11:00 UTC
|
||||||
|
function criarTimestampUTCDeGMT3(data: string, hora: number, minuto: number): number {
|
||||||
|
const [ano, mes, dia] = data.split('-').map(Number);
|
||||||
|
// Date.UTC cria timestamp UTC
|
||||||
|
// Se a hora está em GMT-3, adicionamos 3 horas para obter o equivalente UTC
|
||||||
|
return Date.UTC(ano, mes - 1, dia, hora, minuto, 0, 0) + offsetGMT3ParaUTC;
|
||||||
|
}
|
||||||
|
|
||||||
if (!d.isento) {
|
if (!d.isento) {
|
||||||
const agora = new Date();
|
// Para dispensas não isentas, verificar se está dentro do período
|
||||||
const dataFimTimestamp = new Date(
|
const dataInicioTimestamp = criarTimestampUTCDeGMT3(
|
||||||
`${d.dataFim}T${d.horaFim.toString().padStart(2, '0')}:${d.minutoFim.toString().padStart(2, '0')}:00`
|
d.dataInicio,
|
||||||
).getTime();
|
d.horaInicio,
|
||||||
expirada = agora.getTime() > dataFimTimestamp;
|
d.minutoInicio
|
||||||
|
);
|
||||||
|
const dataFimTimestamp = criarTimestampUTCDeGMT3(d.dataFim, d.horaFim, d.minutoFim);
|
||||||
|
|
||||||
|
// Está expirada se estiver antes do início OU depois do fim
|
||||||
|
// Está ativa se: dataInicioTimestamp <= agoraTimestampUTC <= dataFimTimestamp
|
||||||
|
expirada =
|
||||||
|
agoraTimestampUTC < dataInicioTimestamp || agoraTimestampUTC > dataFimTimestamp;
|
||||||
|
} else {
|
||||||
|
// Se for isento, verificar apenas se já passou do início
|
||||||
|
const dataInicioTimestamp = criarTimestampUTCDeGMT3(
|
||||||
|
d.dataInicio,
|
||||||
|
d.horaInicio,
|
||||||
|
d.minutoInicio
|
||||||
|
);
|
||||||
|
// Se ainda não começou, está expirada (não ativa ainda)
|
||||||
|
expirada = agoraTimestampUTC < dataInicioTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -3302,7 +3588,16 @@ export const verificarDispensaAtiva = query({
|
|||||||
.filter((q) => q.eq(q.field('ativo'), true))
|
.filter((q) => q.eq(q.field('ativo'), true))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
const dataConsulta = new Date(args.data);
|
// Helper para criar timestamp UTC a partir de data (YYYY-MM-DD), hora e minuto em GMT-3
|
||||||
|
const offsetGMT3ParaUTC = 3 * 60 * 60 * 1000; // 3 horas em milissegundos
|
||||||
|
function criarTimestampUTCDeGMT3(data: string, hora: number, minuto: number): number {
|
||||||
|
const [ano, mes, dia] = data.split('-').map(Number);
|
||||||
|
return Date.UTC(ano, mes - 1, dia, hora, minuto, 0, 0) + offsetGMT3ParaUTC;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obter timestamp atual em UTC
|
||||||
|
const agoraUTC = new Date();
|
||||||
|
const agoraTimestampUTC = agoraUTC.getTime();
|
||||||
|
|
||||||
for (const dispensa of dispensas) {
|
for (const dispensa of dispensas) {
|
||||||
// Se for isento, sempre está dispensado
|
// Se for isento, sempre está dispensado
|
||||||
@@ -3314,25 +3609,29 @@ export const verificarDispensaAtiva = query({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verificar se está no período
|
// Calcular timestamps de início e fim da dispensa em UTC
|
||||||
const dataInicio = new Date(dispensa.dataInicio);
|
const timestampInicioUTC = criarTimestampUTCDeGMT3(
|
||||||
const dataFim = new Date(dispensa.dataFim);
|
dispensa.dataInicio,
|
||||||
|
dispensa.horaInicio,
|
||||||
|
dispensa.minutoInicio
|
||||||
|
);
|
||||||
|
const timestampFimUTC = criarTimestampUTCDeGMT3(
|
||||||
|
dispensa.dataFim,
|
||||||
|
dispensa.horaFim,
|
||||||
|
dispensa.minutoFim
|
||||||
|
);
|
||||||
|
|
||||||
// Se a data está dentro do período
|
// Verificar se AGORA já passou do horário de fim da dispensa
|
||||||
if (dataConsulta >= dataInicio && dataConsulta <= dataFim) {
|
// Se já expirou, não está mais dispensado
|
||||||
// Se hora e minuto foram fornecidos, verificar também
|
if (agoraTimestampUTC > timestampFimUTC) {
|
||||||
|
// Dispensa expirada, continuar para próxima
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se hora e minuto foram fornecidos, verificar timestamp completo
|
||||||
if (args.hora !== undefined && args.minuto !== undefined) {
|
if (args.hora !== undefined && args.minuto !== undefined) {
|
||||||
const timestampConsulta = new Date(
|
const timestampConsultaUTC = criarTimestampUTCDeGMT3(args.data, args.hora, args.minuto);
|
||||||
`${args.data}T${args.hora.toString().padStart(2, '0')}:${args.minuto.toString().padStart(2, '0')}:00`
|
if (timestampConsultaUTC >= timestampInicioUTC && timestampConsultaUTC <= timestampFimUTC) {
|
||||||
).getTime();
|
|
||||||
const timestampInicio = new Date(
|
|
||||||
`${dispensa.dataInicio}T${dispensa.horaInicio.toString().padStart(2, '0')}:${dispensa.minutoInicio.toString().padStart(2, '0')}:00`
|
|
||||||
).getTime();
|
|
||||||
const timestampFim = new Date(
|
|
||||||
`${dispensa.dataFim}T${dispensa.horaFim.toString().padStart(2, '0')}:${dispensa.minutoFim.toString().padStart(2, '0')}:00`
|
|
||||||
).getTime();
|
|
||||||
|
|
||||||
if (timestampConsulta >= timestampInicio && timestampConsulta <= timestampFim) {
|
|
||||||
return {
|
return {
|
||||||
dispensado: true,
|
dispensado: true,
|
||||||
dispensa,
|
dispensa,
|
||||||
@@ -3340,7 +3639,9 @@ export const verificarDispensaAtiva = query({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Apenas verificar data
|
// Se apenas data foi fornecida, verificar se AGORA está dentro do período
|
||||||
|
// (não apenas a data, mas também o horário)
|
||||||
|
if (agoraTimestampUTC >= timestampInicioUTC && agoraTimestampUTC <= timestampFimUTC) {
|
||||||
return {
|
return {
|
||||||
dispensado: true,
|
dispensado: true,
|
||||||
dispensa,
|
dispensa,
|
||||||
@@ -3476,7 +3777,13 @@ export const obterBancoHorasCompleto = query({
|
|||||||
tipo: a.tipo,
|
tipo: a.tipo,
|
||||||
valorMinutos: a.valorMinutos,
|
valorMinutos: a.valorMinutos,
|
||||||
motivoDescricao: a.motivoDescricao,
|
motivoDescricao: a.motivoDescricao,
|
||||||
motivoTipo: a.motivoTipo
|
motivoTipo: a.motivoTipo,
|
||||||
|
dataInicio: a.dataInicio,
|
||||||
|
horaInicio: a.horaInicio,
|
||||||
|
minutoInicio: a.minutoInicio,
|
||||||
|
dataFim: a.dataFim,
|
||||||
|
horaFim: a.horaFim,
|
||||||
|
minutoFim: a.minutoFim
|
||||||
})),
|
})),
|
||||||
inconsistencias: inconsistenciasFiltradas.map((i) => ({
|
inconsistencias: inconsistenciasFiltradas.map((i) => ({
|
||||||
_id: i._id,
|
_id: i._id,
|
||||||
@@ -3513,6 +3820,12 @@ export const listarAjustesBancoHoras = query({
|
|||||||
),
|
),
|
||||||
motivoDescricao: v.optional(v.string()),
|
motivoDescricao: v.optional(v.string()),
|
||||||
dataAplicacao: v.string(),
|
dataAplicacao: v.string(),
|
||||||
|
dataInicio: v.optional(v.string()),
|
||||||
|
horaInicio: v.optional(v.number()),
|
||||||
|
minutoInicio: v.optional(v.number()),
|
||||||
|
dataFim: v.optional(v.string()),
|
||||||
|
horaFim: v.optional(v.number()),
|
||||||
|
minutoFim: v.optional(v.number()),
|
||||||
aplicado: v.boolean(),
|
aplicado: v.boolean(),
|
||||||
gestor: v.union(
|
gestor: v.union(
|
||||||
v.object({
|
v.object({
|
||||||
@@ -3567,6 +3880,12 @@ export const listarAjustesBancoHoras = query({
|
|||||||
motivoTipo: ajuste.motivoTipo,
|
motivoTipo: ajuste.motivoTipo,
|
||||||
motivoDescricao: ajuste.motivoDescricao,
|
motivoDescricao: ajuste.motivoDescricao,
|
||||||
dataAplicacao: ajuste.dataAplicacao,
|
dataAplicacao: ajuste.dataAplicacao,
|
||||||
|
dataInicio: ajuste.dataInicio,
|
||||||
|
horaInicio: ajuste.horaInicio,
|
||||||
|
minutoInicio: ajuste.minutoInicio,
|
||||||
|
dataFim: ajuste.dataFim,
|
||||||
|
horaFim: ajuste.horaFim,
|
||||||
|
minutoFim: ajuste.minutoFim,
|
||||||
aplicado: ajuste.aplicado,
|
aplicado: ajuste.aplicado,
|
||||||
gestor: gestor ? { nome: gestor.nome } : null
|
gestor: gestor ? { nome: gestor.nome } : null
|
||||||
};
|
};
|
||||||
@@ -4271,6 +4590,31 @@ export const recalcularBancoHoras = mutation({
|
|||||||
/**
|
/**
|
||||||
* Mutation interna para recalcular banco de horas de uma data específica
|
* Mutation interna para recalcular banco de horas de uma data específica
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Internal mutation para remover ajustes automáticos relacionados a um registro excluído
|
||||||
|
*/
|
||||||
|
export const removerAjustesAutomaticosInternal = internalMutation({
|
||||||
|
args: {
|
||||||
|
funcionarioId: v.id('funcionarios'),
|
||||||
|
motivoTipo: v.union(v.literal('atestado'), v.literal('licenca'), v.literal('ausencia')),
|
||||||
|
motivoId: v.string(),
|
||||||
|
dataInicio: v.string(),
|
||||||
|
dataFim: v.string()
|
||||||
|
},
|
||||||
|
returns: v.null(),
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
await removerAjustesAutomaticos(
|
||||||
|
ctx,
|
||||||
|
args.funcionarioId,
|
||||||
|
args.motivoTipo,
|
||||||
|
args.motivoId,
|
||||||
|
args.dataInicio,
|
||||||
|
args.dataFim
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export const recalcularBancoHorasData = internalMutation({
|
export const recalcularBancoHorasData = internalMutation({
|
||||||
args: {
|
args: {
|
||||||
funcionarioId: v.id('funcionarios'),
|
funcionarioId: v.id('funcionarios'),
|
||||||
|
|||||||
@@ -368,6 +368,13 @@ export const pontoTables = {
|
|||||||
valorMinutos: v.number(), // Valor em minutos (positivo para abonar, negativo para descontar)
|
valorMinutos: v.number(), // Valor em minutos (positivo para abonar, negativo para descontar)
|
||||||
// Data de aplicação
|
// Data de aplicação
|
||||||
dataAplicacao: v.string(), // YYYY-MM-DD
|
dataAplicacao: v.string(), // YYYY-MM-DD
|
||||||
|
// Período do ajuste (data/hora início e fim)
|
||||||
|
dataInicio: v.optional(v.string()), // YYYY-MM-DD
|
||||||
|
horaInicio: v.optional(v.number()), // 0-23
|
||||||
|
minutoInicio: v.optional(v.number()), // 0-59
|
||||||
|
dataFim: v.optional(v.string()), // YYYY-MM-DD
|
||||||
|
horaFim: v.optional(v.number()), // 0-23
|
||||||
|
minutoFim: v.optional(v.number()), // 0-59
|
||||||
// Gestor responsável (null se automático)
|
// Gestor responsável (null se automático)
|
||||||
gestorId: v.optional(v.id('usuarios')),
|
gestorId: v.optional(v.id('usuarios')),
|
||||||
// Observações
|
// Observações
|
||||||
|
|||||||
Reference in New Issue
Block a user