Feat controle ponto #32

Merged
deyvisonwanderley merged 2 commits from feat-controle-ponto into master 2025-11-19 20:00:26 +00:00
9 changed files with 2800 additions and 164 deletions
Showing only changes of commit 57c37fedef - Show all commits

View File

@@ -13,7 +13,7 @@
let funcionarioSelecionado = $state<Id<'funcionarios'> | ''>('');
let registroSelecionado = $state<Id<'registrosPonto'> | ''>('');
let modoEdicao = $state(false);
let modoAjuste = $state(false);
let abaAtiva = $state<'editar' | 'ajustar'>('editar');
// Formulário de edição
let horaNova = $state(8);
@@ -23,11 +23,72 @@
let motivoDescricao = $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
let tipoAjuste = $state<'compensar' | 'abonar' | 'descontar'>('compensar');
let periodoDias = $state(0);
let periodoHoras = $state(0);
let periodoMinutos = $state(0);
let dataInicioAjuste = $state(new Date().toISOString().split('T')[0]!);
let horaInicioAjuste = $state('08:00');
let dataFimAjuste = $state(new Date().toISOString().split('T')[0]!);
let horaFimAjuste = $state('18:00');
// Queries
const subordinadosQuery = useQuery(api.times.listarSubordinadosDoGestorAtual, {});
@@ -75,6 +136,7 @@
registroSelecionado = registroId;
horaNova = registro.hora;
minutoNova = registro.minuto;
novaHoraFormatada = horaMinutoParaTime(registro.hora, registro.minuto);
motivoId = '';
motivoTipo = '';
motivoDescricao = '';
@@ -83,24 +145,40 @@
modoAjuste = false;
}
function abrirAjuste() {
modoAjuste = true;
modoEdicao = false;
registroSelecionado = '';
tipoAjuste = 'compensar';
periodoDias = 0;
periodoHoras = 0;
periodoMinutos = 0;
function abrirEdicaoComAjuste(registroId: Id<'registrosPonto'>) {
const registro = registros.find((r) => r._id === registroId);
if (!registro) return;
registroSelecionado = registroId;
horaNova = registro.hora;
minutoNova = registro.minuto;
novaHoraFormatada = horaMinutoParaTime(registro.hora, registro.minuto);
motivoId = '';
motivoTipo = '';
motivoDescricao = '';
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() {
modoEdicao = false;
modoAjuste = false;
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() {
@@ -109,11 +187,14 @@
return;
}
// Converter hora formatada para hora/minuto
const { hora, minuto } = timeParaHoraMinuto(novaHoraFormatada);
try {
await client.mutation(api.pontos.editarRegistroPonto, {
registroId: registroSelecionado,
horaNova,
minutoNova,
horaNova: hora,
minutoNova: minuto,
motivoId: motivoId || undefined,
motivoTipo: motivoTipo || undefined,
motivoDescricao: motivoDescricao || undefined,
@@ -134,8 +215,21 @@
return;
}
if (periodoDias === 0 && periodoHoras === 0 && periodoMinutos === 0) {
toast.error('Informe pelo menos um período (dias, horas ou minutos)');
if (!dataInicioAjuste || !horaInicioAjuste || !dataFimAjuste || !horaFimAjuste) {
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;
}
@@ -143,9 +237,9 @@
await client.mutation(api.pontos.ajustarBancoHoras, {
funcionarioId: funcionarioSelecionado,
tipoAjuste,
periodoDias,
periodoHoras,
periodoMinutos,
periodoDias: dias,
periodoHoras: horas,
periodoMinutos: minutos,
motivoId: motivoId || undefined,
motivoTipo: motivoTipo || undefined,
motivoDescricao: motivoDescricao || undefined,
@@ -182,7 +276,7 @@
<select
class="select select-bordered w-full"
bind:value={funcionarioSelecionado}
disabled={modoEdicao || modoAjuste}
disabled={modoEdicao}
>
<option value="">Selecione um funcionário</option>
{#each funcionarios as funcionario}
@@ -194,193 +288,297 @@
</div>
</div>
<!-- Botões de Ação -->
{#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 -->
<!-- Formulário Unificado de Edição e Ajuste -->
{#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">
<h2 class="card-title mb-4">Editar Registro de Ponto</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">Nova Hora</span>
</label>
<input
type="number"
min="0"
max="23"
class="input input-bordered"
bind:value={horaNova}
/>
<!-- Título com data -->
<div class="flex items-center justify-between mb-6 pb-4 border-b border-base-300">
<div>
<h2 class="card-title text-2xl text-primary mb-1">
<Edit class="h-6 w-6" strokeWidth={2} />
Homologar Registro de Ponto
</h2>
{#if dataRegistroFormatada}
<p class="text-base-content/70 text-sm mt-1">
Registro do dia <span class="font-semibold text-primary">{dataRegistroFormatada}</span>
</p>
{/if}
</div>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Novo Minuto</span>
</label>
<input
type="number"
min="0"
max="59"
class="input input-bordered"
bind:value={minutoNova}
/>
<!-- Abas -->
<div class="tabs tabs-boxed mb-6 bg-base-200">
<button
class="tab tab-lg {abaAtiva === 'editar' ? 'tab-active' : ''}"
onclick={() => (abaAtiva = 'editar')}
>
<Edit class="h-4 w-4 mr-2" />
Editar Horário
</button>
<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>
{/if}
<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}
<!-- Conteúdo da Aba: Ajustar Banco de Horas -->
{#if abaAtiva === 'ajustar'}
<div class="space-y-6">
<!-- Tipo de Ajuste -->
<div class="form-control">
<label class="label">
<span class="label-text font-semibold text-base">Tipo de Ajuste</span>
<span class="label-text-alt text-error">*</span>
</label>
<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}
</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 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>
{/if}
</div>
</div>
{/if}
<!-- Lista de Registros -->
{#if funcionarioSelecionado && !modoEdicao && !modoAjuste}
{#if funcionarioSelecionado && !modoEdicao}
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<h2 class="card-title mb-4">Registros do Funcionário</h2>
@@ -419,7 +617,7 @@
<td>
<button
class="btn btn-sm btn-outline btn-primary gap-2"
onclick={() => abrirEdicao(registro._id)}
onclick={() => abrirEdicaoComAjuste(registro._id)}
>
<Edit class="h-4 w-4" />
Editar
@@ -436,7 +634,7 @@
{/if}
<!-- Histórico de Homologações -->
{#if !modoEdicao && !modoAjuste}
{#if !modoEdicao}
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title mb-4">