Feat controle ponto #35

Merged
deyvisonwanderley merged 7 commits from feat-controle-ponto into master 2025-11-21 15:48:44 +00:00
57 changed files with 2839 additions and 3065 deletions
Showing only changes of commit e029cd1d6b - Show all commits

View File

@@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte';
import { useQuery, useConvexClient } from 'convex-svelte'; import { useQuery, useConvexClient } from 'convex-svelte';
import { api } from '@sgse-app/backend/convex/_generated/api'; import { api } from '@sgse-app/backend/convex/_generated/api';
import { Clock, Plus, X, Trash2 } from 'lucide-svelte'; import { Clock, Plus, X, Trash2, AlertTriangle } from 'lucide-svelte';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel'; import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
@@ -11,21 +10,32 @@
// Estados // Estados
let funcionariosSelecionados = $state<Id<'funcionarios'>[]>([]); let funcionariosSelecionados = $state<Id<'funcionarios'>[]>([]);
let modoCriacao = $state(false); let modoCriacao = $state(false);
let mostrandoModalExcluir = $state(false);
let dispensaParaExcluir = $state<Id<'dispensasRegistro'> | null>(null);
// Formulário // Formulário
let dataInicio = $state(new Date().toISOString().split('T')[0]!); let dataInicio = $state(new Date().toISOString().split('T')[0]!);
let horaInicio = $state(8); let horaInicioTime = $state('08:00');
let minutoInicio = $state(0);
let dataFim = $state(new Date().toISOString().split('T')[0]!); let dataFim = $state(new Date().toISOString().split('T')[0]!);
let horaFim = $state(18); let horaFimTime = $state('18:00');
let minutoFim = $state(0);
let motivo = $state(''); let motivo = $state('');
let isento = $state(false); let isento = $state(false);
// Computed para converter time string para hora/minuto
const horaInicio = $derived.by(() => {
const [hora, minuto] = horaInicioTime.split(':').map(Number);
return { hora: hora || 8, minuto: minuto || 0 };
});
const horaFim = $derived.by(() => {
const [hora, minuto] = horaFimTime.split(':').map(Number);
return { hora: hora || 18, minuto: minuto || 0 };
});
// 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: false, // Mostrar todas para o gestor ver histórico apenasAtivas: true, // Mostrar apenas dispensas ativas
}); });
const subordinados = $derived(subordinadosQuery?.data || []); const subordinados = $derived(subordinadosQuery?.data || []);
@@ -52,11 +62,9 @@
modoCriacao = true; modoCriacao = true;
funcionariosSelecionados = []; funcionariosSelecionados = [];
dataInicio = new Date().toISOString().split('T')[0]!; dataInicio = new Date().toISOString().split('T')[0]!;
horaInicio = 8; horaInicioTime = '08:00';
minutoInicio = 0;
dataFim = new Date().toISOString().split('T')[0]!; dataFim = new Date().toISOString().split('T')[0]!;
horaFim = 18; horaFimTime = '18:00';
minutoFim = 0;
motivo = ''; motivo = '';
isento = false; isento = false;
} }
@@ -99,11 +107,11 @@
client.mutation(api.pontos.criarDispensaRegistro, { client.mutation(api.pontos.criarDispensaRegistro, {
funcionarioId, funcionarioId,
dataInicio, dataInicio,
horaInicio, horaInicio: horaInicio.hora,
minutoInicio, minutoInicio: horaInicio.minuto,
dataFim, dataFim,
horaFim, horaFim: horaFim.hora,
minutoFim, minutoFim: horaFim.minuto,
motivo, motivo,
isento, isento,
}) })
@@ -121,15 +129,26 @@
} }
} }
async function removerDispensa(dispensaId: Id<'dispensasRegistro'>) { function abrirModalExcluir(dispensaId: Id<'dispensasRegistro'>) {
if (!confirm('Deseja realmente remover esta dispensa?')) return; dispensaParaExcluir = dispensaId;
mostrandoModalExcluir = true;
}
function fecharModalExcluir() {
mostrandoModalExcluir = false;
dispensaParaExcluir = null;
}
async function confirmarRemoverDispensa() {
if (!dispensaParaExcluir) return;
try { try {
await client.mutation(api.pontos.removerDispensaRegistro, { await client.mutation(api.pontos.removerDispensaRegistro, {
dispensaId, dispensaId: dispensaParaExcluir,
}); });
toast.success('Dispensa removida com sucesso'); toast.success('Dispensa removida com sucesso');
fecharModalExcluir();
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error); const errorMessage = error instanceof Error ? error.message : String(error);
toast.error(`Erro ao remover dispensa: ${errorMessage}`); toast.error(`Erro ao remover dispensa: ${errorMessage}`);
@@ -164,19 +183,27 @@
<!-- Formulário de Criação --> <!-- Formulário de Criação -->
{#if modoCriacao} {#if modoCriacao}
<div class="card bg-base-100 shadow-xl mb-6"> <div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body"> <div class="card-body space-y-6">
<h2 class="card-title mb-4">Criar Dispensa de Registro</h2> <h2 class="card-title border-b pb-3 text-xl">Criar Dispensa de Registro</h2>
<!-- Seleção de Funcionários --> <!-- Seleção de Funcionários -->
<div class="form-control mb-4"> <div class="form-control">
<label class="label"> <label class="label">
<span class="label-text font-medium">Funcionários</span> <span class="label-text font-medium">Funcionários</span>
{#if funcionariosSelecionados.length > 0}
<span class="label-text-alt text-primary">
{funcionariosSelecionados.length} selecionado(s)
</span>
{/if}
</label> </label>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2 max-h-60 overflow-y-auto border border-base-300 rounded-lg p-4"> <div class="max-h-60 overflow-y-auto border border-base-300 rounded-lg p-4 space-y-2">
{#each funcionarios as funcionario} {#each funcionarios as funcionario}
<label class="label cursor-pointer"> <label class="flex items-center justify-between p-3 rounded-lg hover:bg-base-200 transition-colors cursor-pointer">
<span class="label-text"> <span class="label-text font-medium">
{funcionario.nome} {funcionario.matricula ? `(${funcionario.matricula})` : ''} {funcionario.nome}
{#if funcionario.matricula}
<span class="text-base-content/60 ml-2">({funcionario.matricula})</span>
{/if}
</span> </span>
<input <input
type="checkbox" type="checkbox"
@@ -186,90 +213,74 @@
/> />
</label> </label>
{/each} {/each}
{#if funcionarios.length === 0}
<div class="text-center py-4 text-base-content/60">
Nenhum funcionário disponível
</div>
{/if}
</div> </div>
</div> </div>
<!-- Período -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label">
<span class="label-text font-medium">Data Início</span> <span class="label-text font-medium">Data Início</span>
</label> </label>
<input type="date" class="input input-bordered" bind:value={dataInicio} /> <input type="date" class="input input-bordered w-full" bind:value={dataInicio} />
</div> </div>
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label">
<span class="label-text font-medium">Hora Início</span> <span class="label-text font-medium">Hora Início</span>
</label> </label>
<div class="flex gap-2"> <input type="time" class="input input-bordered w-full" bind:value={horaInicioTime} />
<input
type="number"
min="0"
max="23"
class="input input-bordered flex-1"
bind:value={horaInicio}
/>
<span class="self-center">:</span>
<input
type="number"
min="0"
max="59"
class="input input-bordered flex-1"
bind:value={minutoInicio}
/>
</div>
</div> </div>
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label">
<span class="label-text font-medium">Data Fim</span> <span class="label-text font-medium">Data Fim</span>
</label> </label>
<input type="date" class="input input-bordered" bind:value={dataFim} /> <input type="date" class="input input-bordered w-full" bind:value={dataFim} />
</div> </div>
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label">
<span class="label-text font-medium">Hora Fim</span> <span class="label-text font-medium">Hora Fim</span>
</label> </label>
<div class="flex gap-2"> <input type="time" class="input input-bordered w-full" bind:value={horaFimTime} />
<input
type="number"
min="0"
max="23"
class="input input-bordered flex-1"
bind:value={horaFim}
/>
<span class="self-center">:</span>
<input
type="number"
min="0"
max="59"
class="input input-bordered flex-1"
bind:value={minutoFim}
/>
</div>
</div>
<div class="form-control md:col-span-2">
<label class="label">
<span class="label-text font-medium">Motivo</span>
</label>
<textarea class="textarea textarea-bordered" bind:value={motivo} rows="3"></textarea>
</div>
<div class="form-control md:col-span-2">
<label class="label cursor-pointer">
<span class="label-text font-medium">Isento de Registro (caso excepcional - sem expiração)</span>
<input type="checkbox" class="checkbox checkbox-primary" bind:checked={isento} />
</label>
<p class="text-sm text-base-content/70 mt-1">
Se marcado, o funcionário ficará permanentemente dispensado de registrar ponto
</p>
</div> </div>
</div> </div>
<div class="flex gap-2 mt-4"> <!-- Motivo -->
<button class="btn btn-primary gap-2" onclick={salvarDispensa}> <div class="form-control">
<label class="label">
<span class="label-text font-medium">Motivo</span>
</label>
<textarea
class="textarea textarea-bordered"
bind:value={motivo}
rows="3"
placeholder="Descreva o motivo da dispensa de registro de ponto"
></textarea>
</div>
<!-- Isento -->
<div class="form-control">
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox" class="checkbox checkbox-primary" bind:checked={isento} />
<div>
<span class="label-text font-medium">Isento de Registro</span>
<p class="text-sm text-base-content/70 mt-1">
Caso excepcional - sem expiração. O funcionário ficará permanentemente dispensado de registrar ponto.
</p>
</div>
</label>
</div>
<!-- Ações -->
<div class="flex gap-2 pt-4 border-t">
<button class="btn btn-primary gap-2 flex-1" onclick={salvarDispensa}>
<Plus class="h-4 w-4" /> <Plus class="h-4 w-4" />
Criar Dispensa Criar Dispensa
</button> </button>
@@ -342,7 +353,7 @@
<td> <td>
<button <button
class="btn btn-sm btn-error gap-2" class="btn btn-sm btn-error gap-2"
onclick={() => removerDispensa(dispensa._id)} onclick={() => abrirModalExcluir(dispensa._id)}
> >
<Trash2 class="h-4 w-4" /> <Trash2 class="h-4 w-4" />
Remover Remover
@@ -356,5 +367,32 @@
{/if} {/if}
</div> </div>
</div> </div>
<!-- Modal de Confirmação de Remoção -->
{#if mostrandoModalExcluir && dispensaParaExcluir}
<dialog class="modal modal-open">
<div class="modal-box">
<div class="flex items-center gap-3 mb-4">
<div class="p-2 bg-error/10 rounded-lg">
<AlertTriangle class="h-6 w-6 text-error" strokeWidth={2} />
</div>
<h3 class="font-bold text-lg">Confirmar Remoção</h3>
</div>
<p class="text-base-content mb-6">
Deseja realmente remover esta dispensa? Esta ação não pode ser desfeita.
</p>
<div class="modal-action">
<button class="btn btn-ghost" onclick={fecharModalExcluir}>Cancelar</button>
<button class="btn btn-error gap-2" onclick={confirmarRemoverDispensa}>
<Trash2 class="h-4 w-4" />
Remover
</button>
</div>
</div>
<form method="dialog" class="modal-backdrop" onclick={fecharModalExcluir}>
<button type="button">fechar</button>
</form>
</dialog>
{/if}
</div> </div>

View File

@@ -19,6 +19,11 @@
let mostrandoModalDetalhes = $state(false); let mostrandoModalDetalhes = $state(false);
let mostrandoModalExcluir = $state(false); let mostrandoModalExcluir = $state(false);
// Monitorar mudanças em funcionarioSelecionado
$effect(() => {
console.log('🔄 [DEBUG] funcionarioSelecionado mudou:', funcionarioSelecionado, typeof funcionarioSelecionado);
});
// Formulário de edição // Formulário de edição
let horaNova = $state(8); let horaNova = $state(8);
let minutoNova = $state(0); let minutoNova = $state(0);
@@ -102,10 +107,21 @@
const homologacoesParams = $derived({ const homologacoesParams = $derived({
funcionarioId: funcionarioSelecionado || undefined, funcionarioId: funcionarioSelecionado || undefined,
}); });
const registrosQueryParams = $derived({
funcionarioId: funcionarioSelecionado || undefined, // Parâmetros para query de registros - só executa quando há funcionário selecionado
dataInicio: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]!, const registrosQueryParams = $derived.by(() => {
dataFim: new Date().toISOString().split('T')[0]!, // Verificar se funcionarioSelecionado não é string vazia
if (!funcionarioSelecionado || funcionarioSelecionado === '') {
console.log('⏭️ [DEBUG] registrosQueryParams: skip (sem funcionário selecionado)');
return 'skip';
}
const params = {
funcionarioId: funcionarioSelecionado as Id<'funcionarios'>,
dataInicio: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]!,
dataFim: new Date().toISOString().split('T')[0]!,
};
console.log('📤 [DEBUG] registrosQueryParams:', params);
return params;
}); });
const homologacoesQuery = useQuery(api.pontos.listarHomologacoes, homologacoesParams); const homologacoesQuery = useQuery(api.pontos.listarHomologacoes, homologacoesParams);
@@ -114,7 +130,26 @@
const subordinados = $derived(subordinadosQuery?.data || []); const subordinados = $derived(subordinadosQuery?.data || []);
const motivos = $derived(motivosQuery?.data); const motivos = $derived(motivosQuery?.data);
const homologacoes = $derived(homologacoesQuery?.data || []); const homologacoes = $derived(homologacoesQuery?.data || []);
const registros = $derived(registrosQuery?.data || []);
// Registros já filtrados pela query no backend
const registros = $derived.by(() => {
if (!funcionarioSelecionado || funcionarioSelecionado === '') {
return [];
}
const dados = registrosQuery?.data;
console.log('🔍 [DEBUG] funcionarioSelecionado:', funcionarioSelecionado);
console.log('🔍 [DEBUG] registrosQuery?.data:', dados);
console.log('🔍 [DEBUG] registrosQuery?.status:', registrosQuery?.status);
if (!dados || !Array.isArray(dados)) {
console.log('⚠️ [DEBUG] Dados não são array ou estão vazios');
return [];
}
// A query do backend já filtra pelo funcionário, mas adicionamos verificação extra
// Converter ambos para string para garantir comparação correta
const filtrados = dados.filter((r) => String(r.funcionarioId) === String(funcionarioSelecionado));
console.log('✅ [DEBUG] Registros filtrados:', filtrados.length, filtrados);
return filtrados;
});
// Verificar se é gestor (tem subordinados) // Verificar se é gestor (tem subordinados)
const isGestor = $derived(subordinados.length > 0); const isGestor = $derived(subordinados.length > 0);
@@ -335,14 +370,14 @@
<div class="card bg-base-100 shadow-xl mb-6"> <div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body"> <div class="card-body">
<h2 class="card-title mb-4">Selecionar Funcionário</h2> <h2 class="card-title mb-4">Selecionar Funcionário</h2>
<select <select
class="select select-bordered w-full" class="select select-bordered w-full"
bind:value={funcionarioSelecionado} bind:value={funcionarioSelecionado}
disabled={modoEdicao} disabled={modoEdicao}
> >
<option value="">Selecione um funcionário</option> <option value="">Selecione um funcionário</option>
{#each funcionarios as funcionario} {#each funcionarios as funcionario}
<option value={funcionario._id}> <option value={funcionario._id as string}>
{funcionario.nome} {funcionario.matricula ? `(${funcionario.matricula})` : ''} {funcionario.nome} {funcionario.matricula ? `(${funcionario.matricula})` : ''}
</option> </option>
{/each} {/each}

View File

@@ -384,15 +384,32 @@ export const listarRegistrosPeriodo = query({
const dataFim = new Date(args.dataFim); const dataFim = new Date(args.dataFim);
dataFim.setHours(23, 59, 59, 999); dataFim.setHours(23, 59, 59, 999);
const registros = await ctx.db let registrosFiltrados;
.query('registrosPonto')
.withIndex('by_data', (q) => q.gte('data', args.dataInicio).lte('data', args.dataFim)) // Se funcionário foi especificado, usar índice por funcionário e data (mais eficiente)
.collect();
// Filtrar por funcionário se especificado
let registrosFiltrados = registros;
if (args.funcionarioId) { if (args.funcionarioId) {
registrosFiltrados = registros.filter((r) => r.funcionarioId === args.funcionarioId); // Garantir que funcionarioId não é undefined para TypeScript
const funcionarioId = args.funcionarioId;
// Buscar todos os registros do funcionário
const todosRegistrosFuncionario = await ctx.db
.query('registrosPonto')
.withIndex('by_funcionario_data', (q) => q.eq('funcionarioId', funcionarioId))
.collect();
// Filtrar por período de data
registrosFiltrados = todosRegistrosFuncionario.filter((r) => {
const dataRegistro = new Date(r.data);
return dataRegistro >= new Date(args.dataInicio) && dataRegistro <= dataFim;
});
} else {
// Se não há funcionário especificado, buscar todos e filtrar (menos eficiente, mas necessário)
const registros = await ctx.db
.query('registrosPonto')
.withIndex('by_data', (q) => q.gte('data', args.dataInicio).lte('data', args.dataFim))
.collect();
registrosFiltrados = registros;
} }
// Buscar informações dos funcionários // Buscar informações dos funcionários