feat: enhance dispensa management with modal confirmation and time input improvements

- Introduced a modal for confirming the removal of dispensas, improving user interaction and preventing accidental deletions.
- Updated time input fields to use a more user-friendly format, allowing for direct time selection.
- Refactored state management for dispensa creation, ensuring better handling of time and date inputs.
- Enhanced UI elements for better feedback and clarity during the dispensa creation process.
This commit is contained in:
2025-11-20 13:58:12 -03:00
parent 8ea5c0316b
commit e029cd1d6b
3 changed files with 190 additions and 100 deletions

View File

@@ -1,8 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { useQuery, useConvexClient } from 'convex-svelte';
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 { toast } from 'svelte-sonner';
@@ -11,21 +10,32 @@
// Estados
let funcionariosSelecionados = $state<Id<'funcionarios'>[]>([]);
let modoCriacao = $state(false);
let mostrandoModalExcluir = $state(false);
let dispensaParaExcluir = $state<Id<'dispensasRegistro'> | null>(null);
// Formulário
let dataInicio = $state(new Date().toISOString().split('T')[0]!);
let horaInicio = $state(8);
let minutoInicio = $state(0);
let horaInicioTime = $state('08:00');
let dataFim = $state(new Date().toISOString().split('T')[0]!);
let horaFim = $state(18);
let minutoFim = $state(0);
let horaFimTime = $state('18:00');
let motivo = $state('');
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
const subordinadosQuery = useQuery(api.times.listarSubordinadosDoGestorAtual, {});
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 || []);
@@ -52,11 +62,9 @@
modoCriacao = true;
funcionariosSelecionados = [];
dataInicio = new Date().toISOString().split('T')[0]!;
horaInicio = 8;
minutoInicio = 0;
horaInicioTime = '08:00';
dataFim = new Date().toISOString().split('T')[0]!;
horaFim = 18;
minutoFim = 0;
horaFimTime = '18:00';
motivo = '';
isento = false;
}
@@ -99,11 +107,11 @@
client.mutation(api.pontos.criarDispensaRegistro, {
funcionarioId,
dataInicio,
horaInicio,
minutoInicio,
horaInicio: horaInicio.hora,
minutoInicio: horaInicio.minuto,
dataFim,
horaFim,
minutoFim,
horaFim: horaFim.hora,
minutoFim: horaFim.minuto,
motivo,
isento,
})
@@ -121,15 +129,26 @@
}
}
async function removerDispensa(dispensaId: Id<'dispensasRegistro'>) {
if (!confirm('Deseja realmente remover esta dispensa?')) return;
function abrirModalExcluir(dispensaId: Id<'dispensasRegistro'>) {
dispensaParaExcluir = dispensaId;
mostrandoModalExcluir = true;
}
function fecharModalExcluir() {
mostrandoModalExcluir = false;
dispensaParaExcluir = null;
}
async function confirmarRemoverDispensa() {
if (!dispensaParaExcluir) return;
try {
await client.mutation(api.pontos.removerDispensaRegistro, {
dispensaId,
dispensaId: dispensaParaExcluir,
});
toast.success('Dispensa removida com sucesso');
fecharModalExcluir();
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
toast.error(`Erro ao remover dispensa: ${errorMessage}`);
@@ -164,19 +183,27 @@
<!-- Formulário de Criação -->
{#if modoCriacao}
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<h2 class="card-title mb-4">Criar Dispensa de Registro</h2>
<div class="card-body space-y-6">
<h2 class="card-title border-b pb-3 text-xl">Criar Dispensa de Registro</h2>
<!-- Seleção de Funcionários -->
<div class="form-control mb-4">
<div class="form-control">
<label class="label">
<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>
<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}
<label class="label cursor-pointer">
<span class="label-text">
{funcionario.nome} {funcionario.matricula ? `(${funcionario.matricula})` : ''}
<label class="flex items-center justify-between p-3 rounded-lg hover:bg-base-200 transition-colors cursor-pointer">
<span class="label-text font-medium">
{funcionario.nome}
{#if funcionario.matricula}
<span class="text-base-content/60 ml-2">({funcionario.matricula})</span>
{/if}
</span>
<input
type="checkbox"
@@ -186,90 +213,74 @@
/>
</label>
{/each}
{#if funcionarios.length === 0}
<div class="text-center py-4 text-base-content/60">
Nenhum funcionário disponível
</div>
{/if}
</div>
</div>
<!-- Período -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Data Início</span>
</label>
<input type="date" class="input input-bordered" bind:value={dataInicio} />
<input type="date" class="input input-bordered w-full" bind:value={dataInicio} />
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Hora Início</span>
</label>
<div class="flex gap-2">
<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>
<input type="time" class="input input-bordered w-full" bind:value={horaInicioTime} />
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Data Fim</span>
</label>
<input type="date" class="input input-bordered" bind:value={dataFim} />
<input type="date" class="input input-bordered w-full" bind:value={dataFim} />
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Hora Fim</span>
</label>
<div class="flex gap-2">
<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>
<input type="time" class="input input-bordered w-full" bind:value={horaFimTime} />
</div>
</div>
<div class="flex gap-2 mt-4">
<button class="btn btn-primary gap-2" onclick={salvarDispensa}>
<!-- Motivo -->
<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" />
Criar Dispensa
</button>
@@ -342,7 +353,7 @@
<td>
<button
class="btn btn-sm btn-error gap-2"
onclick={() => removerDispensa(dispensa._id)}
onclick={() => abrirModalExcluir(dispensa._id)}
>
<Trash2 class="h-4 w-4" />
Remover
@@ -356,5 +367,32 @@
{/if}
</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>

View File

@@ -19,6 +19,11 @@
let mostrandoModalDetalhes = $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
let horaNova = $state(8);
let minutoNova = $state(0);
@@ -102,10 +107,21 @@
const homologacoesParams = $derived({
funcionarioId: funcionarioSelecionado || undefined,
});
const registrosQueryParams = $derived({
funcionarioId: funcionarioSelecionado || undefined,
dataInicio: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]!,
dataFim: new Date().toISOString().split('T')[0]!,
// Parâmetros para query de registros - só executa quando há funcionário selecionado
const registrosQueryParams = $derived.by(() => {
// 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);
@@ -114,7 +130,26 @@
const subordinados = $derived(subordinadosQuery?.data || []);
const motivos = $derived(motivosQuery?.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)
const isGestor = $derived(subordinados.length > 0);
@@ -335,14 +370,14 @@
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<h2 class="card-title mb-4">Selecionar Funcionário</h2>
<select
class="select select-bordered w-full"
bind:value={funcionarioSelecionado}
disabled={modoEdicao}
>
<select
class="select select-bordered w-full"
bind:value={funcionarioSelecionado}
disabled={modoEdicao}
>
<option value="">Selecione um funcionário</option>
{#each funcionarios as funcionario}
<option value={funcionario._id}>
<option value={funcionario._id as string}>
{funcionario.nome} {funcionario.matricula ? `(${funcionario.matricula})` : ''}
</option>
{/each}