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:
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user