feat: unify editing and adjustment forms for point management

- Replaced separate editing and adjustment modes with a unified form that allows users to switch between editing hours and adjusting bank hours.
- Introduced new state management for active tab selection and formatted time input.
- Implemented functions to convert between time formats and calculate periods between dates.
- Enhanced user experience with improved layout and validation for date and time inputs.
- Updated the UI to reflect changes in the form structure, ensuring a more cohesive interaction for users managing point records.
This commit is contained in:
2025-11-19 16:59:26 -03:00
parent db61df1fb4
commit 57c37fedef

View File

@@ -13,7 +13,7 @@
let funcionarioSelecionado = $state<Id<'funcionarios'> | ''>(''); let funcionarioSelecionado = $state<Id<'funcionarios'> | ''>('');
let registroSelecionado = $state<Id<'registrosPonto'> | ''>(''); let registroSelecionado = $state<Id<'registrosPonto'> | ''>('');
let modoEdicao = $state(false); let modoEdicao = $state(false);
let modoAjuste = $state(false); let abaAtiva = $state<'editar' | 'ajustar'>('editar');
// Formulário de edição // Formulário de edição
let horaNova = $state(8); let horaNova = $state(8);
@@ -23,11 +23,72 @@
let motivoDescricao = $state(''); let motivoDescricao = $state('');
let observacoes = $state(''); let observacoes = $state('');
// Campo de hora unificado (formato HH:mm)
let novaHoraFormatada = $state('08:00');
// Converter hora/minuto para formato HH:mm
function horaMinutoParaTime(hora: number, minuto: number): string {
return `${hora.toString().padStart(2, '0')}:${minuto.toString().padStart(2, '0')}`;
}
// Converter formato HH:mm para hora/minuto
function timeParaHoraMinuto(time: string): { hora: number; minuto: number } {
const [h, m] = time.split(':').map(Number);
return { hora: h || 0, minuto: m || 0 };
}
// Calcular período entre duas datas/horas em dias, horas e minutos
function calcularPeriodo(
dataInicio: string,
horaInicio: string,
dataFim: string,
horaFim: string
): { dias: number; horas: number; minutos: number } {
if (!dataInicio || !horaInicio || !dataFim || !horaFim) {
return { dias: 0, horas: 0, minutos: 0 };
}
const inicio = new Date(`${dataInicio}T${horaInicio}:00`);
const fim = new Date(`${dataFim}T${horaFim}:00`);
if (fim < inicio) {
return { dias: 0, horas: 0, minutos: 0 };
}
const diffMs = fim.getTime() - inicio.getTime();
const diffMinutos = Math.floor(diffMs / (1000 * 60));
const dias = Math.floor(diffMinutos / (24 * 60));
const minutosRestantes = diffMinutos % (24 * 60);
const horas = Math.floor(minutosRestantes / 60);
const minutos = minutosRestantes % 60;
return { dias, horas, minutos };
}
// Obter registro selecionado
const registroEmEdicao = $derived.by(() => {
if (!registroSelecionado) return null;
return registros.find((r) => r._id === registroSelecionado) || null;
});
// Formatar data do registro
const dataRegistroFormatada = $derived.by(() => {
if (!registroEmEdicao) return '';
const data = new Date(registroEmEdicao.data);
return data.toLocaleDateString('pt-BR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
});
});
// Formulário de ajuste // Formulário de ajuste
let tipoAjuste = $state<'compensar' | 'abonar' | 'descontar'>('compensar'); let tipoAjuste = $state<'compensar' | 'abonar' | 'descontar'>('compensar');
let periodoDias = $state(0); let dataInicioAjuste = $state(new Date().toISOString().split('T')[0]!);
let periodoHoras = $state(0); let horaInicioAjuste = $state('08:00');
let periodoMinutos = $state(0); let dataFimAjuste = $state(new Date().toISOString().split('T')[0]!);
let horaFimAjuste = $state('18:00');
// Queries // Queries
const subordinadosQuery = useQuery(api.times.listarSubordinadosDoGestorAtual, {}); const subordinadosQuery = useQuery(api.times.listarSubordinadosDoGestorAtual, {});
@@ -75,6 +136,7 @@
registroSelecionado = registroId; registroSelecionado = registroId;
horaNova = registro.hora; horaNova = registro.hora;
minutoNova = registro.minuto; minutoNova = registro.minuto;
novaHoraFormatada = horaMinutoParaTime(registro.hora, registro.minuto);
motivoId = ''; motivoId = '';
motivoTipo = ''; motivoTipo = '';
motivoDescricao = ''; motivoDescricao = '';
@@ -83,24 +145,40 @@
modoAjuste = false; modoAjuste = false;
} }
function abrirAjuste() { function abrirEdicaoComAjuste(registroId: Id<'registrosPonto'>) {
modoAjuste = true; const registro = registros.find((r) => r._id === registroId);
modoEdicao = false; if (!registro) return;
registroSelecionado = '';
tipoAjuste = 'compensar'; registroSelecionado = registroId;
periodoDias = 0; horaNova = registro.hora;
periodoHoras = 0; minutoNova = registro.minuto;
periodoMinutos = 0; novaHoraFormatada = horaMinutoParaTime(registro.hora, registro.minuto);
motivoId = ''; motivoId = '';
motivoTipo = ''; motivoTipo = '';
motivoDescricao = ''; motivoDescricao = '';
observacoes = ''; observacoes = '';
modoEdicao = true;
abaAtiva = 'editar';
// Resetar campos de ajuste
tipoAjuste = 'compensar';
const hoje = new Date().toISOString().split('T')[0]!;
dataInicioAjuste = hoje;
dataFimAjuste = hoje;
horaInicioAjuste = '08:00';
horaFimAjuste = '18:00';
} }
function cancelar() { function cancelar() {
modoEdicao = false; modoEdicao = false;
modoAjuste = false;
registroSelecionado = ''; registroSelecionado = '';
abaAtiva = 'editar';
novaHoraFormatada = '08:00';
const hoje = new Date().toISOString().split('T')[0]!;
dataInicioAjuste = hoje;
dataFimAjuste = hoje;
horaInicioAjuste = '08:00';
horaFimAjuste = '18:00';
} }
async function salvarEdicao() { async function salvarEdicao() {
@@ -109,11 +187,14 @@
return; return;
} }
// Converter hora formatada para hora/minuto
const { hora, minuto } = timeParaHoraMinuto(novaHoraFormatada);
try { try {
await client.mutation(api.pontos.editarRegistroPonto, { await client.mutation(api.pontos.editarRegistroPonto, {
registroId: registroSelecionado, registroId: registroSelecionado,
horaNova, horaNova: hora,
minutoNova, minutoNova: minuto,
motivoId: motivoId || undefined, motivoId: motivoId || undefined,
motivoTipo: motivoTipo || undefined, motivoTipo: motivoTipo || undefined,
motivoDescricao: motivoDescricao || undefined, motivoDescricao: motivoDescricao || undefined,
@@ -134,8 +215,21 @@
return; return;
} }
if (periodoDias === 0 && periodoHoras === 0 && periodoMinutos === 0) { if (!dataInicioAjuste || !horaInicioAjuste || !dataFimAjuste || !horaFimAjuste) {
toast.error('Informe pelo menos um período (dias, horas ou minutos)'); toast.error('Preencha todas as datas e horários do período');
return;
}
// Calcular período entre início e fim
const { dias, horas, minutos } = calcularPeriodo(
dataInicioAjuste,
horaInicioAjuste,
dataFimAjuste,
horaFimAjuste
);
if (dias === 0 && horas === 0 && minutos === 0) {
toast.error('A data/hora final deve ser maior que a inicial');
return; return;
} }
@@ -143,9 +237,9 @@
await client.mutation(api.pontos.ajustarBancoHoras, { await client.mutation(api.pontos.ajustarBancoHoras, {
funcionarioId: funcionarioSelecionado, funcionarioId: funcionarioSelecionado,
tipoAjuste, tipoAjuste,
periodoDias, periodoDias: dias,
periodoHoras, periodoHoras: horas,
periodoMinutos, periodoMinutos: minutos,
motivoId: motivoId || undefined, motivoId: motivoId || undefined,
motivoTipo: motivoTipo || undefined, motivoTipo: motivoTipo || undefined,
motivoDescricao: motivoDescricao || undefined, motivoDescricao: motivoDescricao || undefined,
@@ -182,7 +276,7 @@
<select <select
class="select select-bordered w-full" class="select select-bordered w-full"
bind:value={funcionarioSelecionado} bind:value={funcionarioSelecionado}
disabled={modoEdicao || modoAjuste} disabled={modoEdicao}
> >
<option value="">Selecione um funcionário</option> <option value="">Selecione um funcionário</option>
{#each funcionarios as funcionario} {#each funcionarios as funcionario}
@@ -194,193 +288,297 @@
</div> </div>
</div> </div>
<!-- Botões de Ação --> <!-- Formulário Unificado de Edição e Ajuste -->
{#if funcionarioSelecionado && !modoEdicao && !modoAjuste}
<div class="flex gap-4 mb-6">
<button class="btn btn-primary gap-2" onclick={abrirAjuste}>
<TrendingUp class="h-4 w-4" />
Ajustar Banco de Horas
</button>
</div>
{/if}
<!-- Formulário de Edição -->
{#if modoEdicao && registroSelecionado} {#if modoEdicao && registroSelecionado}
<div class="card bg-base-100 shadow-xl mb-6"> <div class="card bg-base-100 shadow-xl mb-6 border-2 border-primary/20">
<div class="card-body"> <div class="card-body">
<h2 class="card-title mb-4">Editar Registro de Ponto</h2> <!-- Título com data -->
<div class="flex items-center justify-between mb-6 pb-4 border-b border-base-300">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div>
<div class="form-control"> <h2 class="card-title text-2xl text-primary mb-1">
<label class="label"> <Edit class="h-6 w-6" strokeWidth={2} />
<span class="label-text font-medium">Nova Hora</span> Homologar Registro de Ponto
</label> </h2>
<input {#if dataRegistroFormatada}
type="number" <p class="text-base-content/70 text-sm mt-1">
min="0" Registro do dia <span class="font-semibold text-primary">{dataRegistroFormatada}</span>
max="23" </p>
class="input input-bordered" {/if}
bind:value={horaNova}
/>
</div> </div>
</div>
<div class="form-control"> <!-- Abas -->
<label class="label"> <div class="tabs tabs-boxed mb-6 bg-base-200">
<span class="label-text font-medium">Novo Minuto</span> <button
</label> class="tab tab-lg {abaAtiva === 'editar' ? 'tab-active' : ''}"
<input onclick={() => (abaAtiva = 'editar')}
type="number" >
min="0" <Edit class="h-4 w-4 mr-2" />
max="59" Editar Horário
class="input input-bordered" </button>
bind:value={minutoNova} <button
/> class="tab tab-lg {abaAtiva === 'ajustar' ? 'tab-active' : ''}"
onclick={() => (abaAtiva = 'ajustar')}
>
<TrendingUp class="h-4 w-4 mr-2" />
Ajustar Banco de Horas
</button>
</div>
<!-- Conteúdo da Aba: Editar Horário -->
{#if abaAtiva === 'editar'}
<div class="space-y-6">
<!-- Hora Unificada -->
<div class="form-control">
<label class="label">
<span class="label-text font-semibold text-base">
<Clock class="h-4 w-4 inline-block mr-2" />
Nova Hora
</span>
</label>
<input
type="time"
class="input input-bordered input-primary w-full max-w-xs"
bind:value={novaHoraFormatada}
/>
</div>
<!-- Grid de Motivos -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="form-control">
<label class="label">
<span class="label-text font-semibold">Motivo (Tipo)</span>
<span class="label-text-alt text-error">*</span>
</label>
<select class="select select-bordered select-primary" bind:value={motivoTipo}>
<option value="">Selecione um tipo</option>
{#if motivos?.opcoesPadrao}
{#each motivos.opcoesPadrao as opcao}
<option value={opcao}>{opcao}</option>
{/each}
{/if}
</select>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-semibold">Descrição do Motivo</span>
</label>
<input
type="text"
class="input input-bordered input-primary"
bind:value={motivoDescricao}
placeholder="Informe detalhes adicionais sobre o motivo"
/>
</div>
</div>
<!-- Observações -->
<div class="form-control">
<label class="label">
<span class="label-text font-semibold">Observações</span>
</label>
<textarea
class="textarea textarea-bordered textarea-primary"
bind:value={observacoes}
rows="4"
placeholder="Adicione observações relevantes sobre esta edição..."
></textarea>
</div>
<!-- Botões de ação -->
<div class="flex gap-3 justify-end mt-8 pt-6 border-t border-base-300">
<button class="btn btn-ghost gap-2" onclick={cancelar}>
<X class="h-4 w-4" />
Cancelar
</button>
<button class="btn btn-primary gap-2" onclick={salvarEdicao}>
<Save class="h-4 w-4" />
Salvar Alterações
</button>
</div>
</div> </div>
{/if}
<div class="form-control"> <!-- Conteúdo da Aba: Ajustar Banco de Horas -->
<label class="label"> {#if abaAtiva === 'ajustar'}
<span class="label-text font-medium">Motivo (Tipo)</span> <div class="space-y-6">
</label> <!-- Tipo de Ajuste -->
<select class="select select-bordered" bind:value={motivoTipo}> <div class="form-control">
<option value="">Selecione um tipo</option> <label class="label">
{#if motivos?.opcoesPadrao} <span class="label-text font-semibold text-base">Tipo de Ajuste</span>
{#each motivos.opcoesPadrao as opcao} <span class="label-text-alt text-error">*</span>
<option value={opcao}>{opcao}</option> </label>
{/each} <select class="select select-bordered select-primary w-full max-w-md" bind:value={tipoAjuste}>
<option value="compensar">Compensar</option>
<option value="abonar">Abonar</option>
<option value="descontar">Descontar em Folha</option>
</select>
</div>
<!-- Período com Data e Hora -->
<div class="bg-base-200/50 p-6 rounded-xl border border-base-300">
<label class="label mb-4">
<span class="label-text font-semibold text-base">
<Clock class="h-4 w-4 inline-block mr-2" />
Período do Ajuste
</span>
<span class="label-text-alt text-error">*</span>
</label>
<!-- Data e Hora Início -->
<div class="mb-6">
<h4 class="text-sm font-semibold text-base-content/80 mb-3 flex items-center gap-2">
<div class="w-2 h-2 rounded-full bg-primary"></div>
Início do Período
</h4>
<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>
<span class="label-text-alt text-error">*</span>
</label>
<input
type="date"
class="input input-bordered input-primary"
bind:value={dataInicioAjuste}
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Hora Início</span>
<span class="label-text-alt text-error">*</span>
</label>
<input
type="time"
class="input input-bordered input-primary"
bind:value={horaInicioAjuste}
/>
</div>
</div>
</div>
<!-- Separador Visual -->
<div class="divider my-4">
<Clock class="h-4 w-4 text-base-content/40" />
</div>
<!-- Data e Hora Fim -->
<div>
<h4 class="text-sm font-semibold text-base-content/80 mb-3 flex items-center gap-2">
<div class="w-2 h-2 rounded-full bg-secondary"></div>
Fim do Período
</h4>
<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 Fim</span>
<span class="label-text-alt text-error">*</span>
</label>
<input
type="date"
class="input input-bordered input-primary"
bind:value={dataFimAjuste}
min={dataInicioAjuste}
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Hora Fim</span>
<span class="label-text-alt text-error">*</span>
</label>
<input
type="time"
class="input input-bordered input-primary"
bind:value={horaFimAjuste}
/>
</div>
</div>
</div>
<!-- Preview do Período Calculado -->
{#if dataInicioAjuste && horaInicioAjuste && dataFimAjuste && horaFimAjuste}
{@const periodoCalculado = calcularPeriodo(dataInicioAjuste, horaInicioAjuste, dataFimAjuste, horaFimAjuste)}
{#if periodoCalculado.dias > 0 || periodoCalculado.horas > 0 || periodoCalculado.minutos > 0}
<div class="mt-4 p-3 bg-primary/10 rounded-lg border border-primary/20">
<div class="text-xs font-semibold text-primary mb-1">Período Calculado:</div>
<div class="text-sm text-base-content">
{periodoCalculado.dias > 0 ? `${periodoCalculado.dias} dia${periodoCalculado.dias > 1 ? 's' : ''} ` : ''}
{periodoCalculado.horas > 0 ? `${periodoCalculado.horas} hora${periodoCalculado.horas > 1 ? 's' : ''} ` : ''}
{periodoCalculado.minutos > 0 ? `${periodoCalculado.minutos} minuto${periodoCalculado.minutos > 1 ? 's' : ''}` : ''}
{#if periodoCalculado.dias === 0 && periodoCalculado.horas === 0 && periodoCalculado.minutos === 0}
<span class="text-base-content/60">Período inválido</span>
{/if}
</div>
</div>
{/if}
{/if} {/if}
</select> </div>
<!-- Grid de Motivos -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="form-control">
<label class="label">
<span class="label-text font-semibold">Motivo (Tipo)</span>
<span class="label-text-alt text-error">*</span>
</label>
<select class="select select-bordered select-primary" bind:value={motivoTipo}>
<option value="">Selecione um tipo</option>
{#if motivos?.opcoesPadrao}
{#each motivos.opcoesPadrao as opcao}
<option value={opcao}>{opcao}</option>
{/each}
{/if}
</select>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-semibold">Descrição do Motivo</span>
</label>
<input
type="text"
class="input input-bordered input-primary"
bind:value={motivoDescricao}
placeholder="Informe detalhes adicionais sobre o motivo"
/>
</div>
</div>
<!-- Observações -->
<div class="form-control">
<label class="label">
<span class="label-text font-semibold">Observações</span>
</label>
<textarea
class="textarea textarea-bordered textarea-primary"
bind:value={observacoes}
rows="4"
placeholder="Adicione observações relevantes sobre este ajuste..."
></textarea>
</div>
<!-- Botões de ação -->
<div class="flex gap-3 justify-end mt-8 pt-6 border-t border-base-300">
<button class="btn btn-ghost gap-2" onclick={cancelar}>
<X class="h-4 w-4" />
Cancelar
</button>
<button class="btn btn-primary gap-2" onclick={salvarAjuste}>
<Save class="h-4 w-4" />
Salvar Ajuste
</button>
</div>
</div> </div>
{/if}
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Descrição do Motivo</span>
</label>
<input type="text" class="input input-bordered" bind:value={motivoDescricao} />
</div>
<div class="form-control md:col-span-2">
<label class="label">
<span class="label-text font-medium">Observações</span>
</label>
<textarea class="textarea textarea-bordered" bind:value={observacoes} rows="3"></textarea>
</div>
</div>
<div class="flex gap-2 mt-4">
<button class="btn btn-primary gap-2" onclick={salvarEdicao}>
<Save class="h-4 w-4" />
Salvar
</button>
<button class="btn btn-ghost gap-2" onclick={cancelar}>
<X class="h-4 w-4" />
Cancelar
</button>
</div>
</div>
</div>
{/if}
<!-- Formulário de Ajuste -->
{#if modoAjuste}
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<h2 class="card-title mb-4">Ajustar Banco de Horas</h2>
<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">Tipo de Ajuste</span>
</label>
<select class="select select-bordered" bind:value={tipoAjuste}>
<option value="compensar">Compensar</option>
<option value="abonar">Abonar</option>
<option value="descontar">Descontar em Folha</option>
</select>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Dias</span>
</label>
<input
type="number"
min="0"
class="input input-bordered"
bind:value={periodoDias}
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Horas</span>
</label>
<input
type="number"
min="0"
max="23"
class="input input-bordered"
bind:value={periodoHoras}
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Minutos</span>
</label>
<input
type="number"
min="0"
max="59"
class="input input-bordered"
bind:value={periodoMinutos}
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Motivo (Tipo)</span>
</label>
<select class="select select-bordered" bind:value={motivoTipo}>
<option value="">Selecione um tipo</option>
{#if motivos?.opcoesPadrao}
{#each motivos.opcoesPadrao as opcao}
<option value={opcao}>{opcao}</option>
{/each}
{/if}
</select>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Descrição do Motivo</span>
</label>
<input type="text" class="input input-bordered" bind:value={motivoDescricao} />
</div>
<div class="form-control md:col-span-2">
<label class="label">
<span class="label-text font-medium">Observações</span>
</label>
<textarea class="textarea textarea-bordered" bind:value={observacoes} rows="3"></textarea>
</div>
</div>
<div class="flex gap-2 mt-4">
<button class="btn btn-primary gap-2" onclick={salvarAjuste}>
<Save class="h-4 w-4" />
Salvar
</button>
<button class="btn btn-ghost gap-2" onclick={cancelar}>
<X class="h-4 w-4" />
Cancelar
</button>
</div>
</div> </div>
</div> </div>
{/if} {/if}
<!-- Lista de Registros --> <!-- Lista de Registros -->
{#if funcionarioSelecionado && !modoEdicao && !modoAjuste} {#if funcionarioSelecionado && !modoEdicao}
<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">Registros do Funcionário</h2> <h2 class="card-title mb-4">Registros do Funcionário</h2>
@@ -419,7 +617,7 @@
<td> <td>
<button <button
class="btn btn-sm btn-outline btn-primary gap-2" class="btn btn-sm btn-outline btn-primary gap-2"
onclick={() => abrirEdicao(registro._id)} onclick={() => abrirEdicaoComAjuste(registro._id)}
> >
<Edit class="h-4 w-4" /> <Edit class="h-4 w-4" />
Editar Editar
@@ -436,7 +634,7 @@
{/if} {/if}
<!-- Histórico de Homologações --> <!-- Histórico de Homologações -->
{#if !modoEdicao && !modoAjuste} {#if !modoEdicao}
<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"> <h2 class="card-title mb-4">