Merge remote-tracking branch 'origin' into feat-pedidos

This commit is contained in:
2025-12-11 10:08:12 -03:00
194 changed files with 30374 additions and 10247 deletions

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { CheckCircle2, Clock, XCircle } from 'lucide-svelte';
import { Clock, CheckCircle2, XCircle, ChevronRight, TrendingUp } from 'lucide-svelte';
import { resolve } from '$app/paths';
</script>
@@ -16,7 +16,7 @@
</div>
<!-- Grid de Cards -->
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
<!-- Card 1: Gestão de Pontos -->
<a
href={resolve('/(dashboard)/recursos-humanos/registro-pontos')}
@@ -27,20 +27,7 @@
<div class="rounded-2xl bg-blue-500/20 p-4">
<Clock class="h-8 w-8 text-blue-600" strokeWidth={2} />
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
class="text-base-content/30 h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
<ChevronRight class="text-base-content/30 h-5 w-5" strokeWidth={2} />
</div>
<h2 class="card-title mb-2 text-xl">Gestão de Pontos</h2>
<p class="text-base-content/70">
@@ -59,20 +46,7 @@
<div class="rounded-2xl bg-green-500/20 p-4">
<CheckCircle2 class="h-8 w-8 text-green-600" strokeWidth={2} />
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
class="text-base-content/30 h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
<ChevronRight class="text-base-content/30 h-5 w-5" strokeWidth={2} />
</div>
<h2 class="card-title mb-2 text-xl">Homologação de Registro</h2>
<p class="text-base-content/70">
@@ -92,20 +66,7 @@
<div class="rounded-2xl bg-orange-500/20 p-4">
<XCircle class="h-8 w-8 text-orange-600" strokeWidth={2} />
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
class="text-base-content/30 h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
<ChevronRight class="text-base-content/30 h-5 w-5" strokeWidth={2} />
</div>
<h2 class="card-title mb-2 text-xl">Dispensa de Registro</h2>
<p class="text-base-content/70">
@@ -113,5 +74,24 @@
</p>
</div>
</a>
<!-- Card 4: Banco de Horas -->
<a
href={resolve('/(dashboard)/recursos-humanos/controle-ponto/banco-horas')}
class="card bg-base-100 transform shadow-xl transition-all duration-300 hover:-translate-y-1 hover:shadow-2xl"
>
<div class="card-body">
<div class="mb-4 flex items-start justify-between">
<div class="rounded-2xl bg-purple-500/20 p-4">
<TrendingUp class="h-8 w-8 text-purple-600" strokeWidth={2} />
</div>
<ChevronRight class="text-base-content/30 h-5 w-5" strokeWidth={2} />
</div>
<h2 class="card-title mb-2 text-xl">Banco de Horas</h2>
<p class="text-base-content/70">
Visão gerencial do banco de horas dos funcionários, com filtros, estatísticas e relatórios
</p>
</div>
</a>
</div>
</div>

View File

@@ -4,6 +4,7 @@
import { useConvexClient, useQuery } from 'convex-svelte';
import { AlertTriangle, Clock, Plus, Trash2, X } from 'lucide-svelte';
import { toast } from 'svelte-sonner';
import UserAvatar from '$lib/components/chat/UserAvatar.svelte';
const client = useConvexClient();
@@ -47,6 +48,7 @@
_id: Id<'funcionarios'>;
nome: string;
matricula?: string;
fotoPerfilUrl?: string | null;
}> = [];
for (const time of subordinados) {
for (const membro of time.membros) {
@@ -54,7 +56,8 @@
funcs.push({
_id: membro.funcionario._id,
nome: membro.funcionario.nome,
matricula: membro.funcionario.matricula
matricula: membro.funcionario.matricula,
fotoPerfilUrl: membro.funcionario.fotoPerfilUrl
});
}
}
@@ -201,16 +204,23 @@
{/if}
</label>
<div class="border-base-300 max-h-60 space-y-2 overflow-y-auto rounded-lg border p-4">
{#each funcionarios as funcionario}
{#each funcionarios as funcionario (funcionario._id)}
<label
class="hover:bg-base-200 flex cursor-pointer items-center justify-between rounded-lg p-3 transition-colors"
>
<span class="label-text font-medium">
{funcionario.nome}
{#if funcionario.matricula}
<span class="text-base-content/60 ml-2">({funcionario.matricula})</span>
{/if}
</span>
<div class="flex items-center gap-3">
<UserAvatar
fotoPerfilUrl={funcionario.fotoPerfilUrl}
nome={funcionario.nome}
size="sm"
/>
<span class="label-text font-medium">
{funcionario.nome}
{#if funcionario.matricula}
<span class="text-base-content/60 ml-2">({funcionario.matricula})</span>
{/if}
</span>
</div>
<input
type="checkbox"
class="checkbox checkbox-primary"
@@ -321,16 +331,24 @@
</tr>
</thead>
<tbody>
{#each dispensas as dispensa}
{#each dispensas as dispensa (dispensa._id)}
<tr>
<td>
{dispensa.funcionario?.nome || '-'}
{#if dispensa.funcionario?.matricula}
<br />
<span class="text-base-content/70 text-sm">
Mat: {dispensa.funcionario.matricula}
</span>
{/if}
<div class="flex items-center gap-3">
<UserAvatar
fotoPerfilUrl={dispensa.fotoPerfilUrl}
nome={dispensa.funcionario?.nome || 'Funcionário'}
size="sm"
/>
<div>
<div class="font-medium">{dispensa.funcionario?.nome || '-'}</div>
{#if dispensa.funcionario?.matricula}
<span class="text-base-content/70 text-sm">
Mat: {dispensa.funcionario.matricula}
</span>
{/if}
</div>
</div>
</td>
<td>
<div class="text-sm">

View File

@@ -1,20 +1,10 @@
<script lang="ts">
import { api } from '@sgse-app/backend/convex/_generated/api';
import { Clock, Edit, TrendingUp, Save, X, Trash2, Eye } from 'lucide-svelte';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import { useConvexClient, useQuery } from 'convex-svelte';
import {
Clock,
Edit,
Eye,
MoreVertical,
Save,
Trash2,
TrendingDown,
TrendingUp,
X
} from 'lucide-svelte';
import { onMount } from 'svelte';
import { toast } from 'svelte-sonner';
import UserAvatar from '$lib/components/chat/UserAvatar.svelte';
import { formatarHoraPonto, getTipoRegistroLabel } from '$lib/utils/ponto';
const client = useConvexClient();
@@ -45,8 +35,6 @@
});
// Formulário de edição
let horaNova = $state(8);
let minutoNova = $state(0);
let motivoId = $state('');
let motivoTipo = $state('');
let motivoDescricao = $state('');
@@ -141,6 +129,7 @@
};
});
// svelte-ignore state_referenced_locally
const homologacoesQuery = useQuery(api.pontos.listarHomologacoes, homologacoesParams);
let registrosQuery = $derived(
registrosQueryParams ? useQuery(api.pontos.listarRegistrosPeriodo, registrosQueryParams) : null
@@ -172,6 +161,7 @@
_id: Id<'funcionarios'>;
nome: string;
matricula?: string;
fotoPerfilUrl?: string | null;
}> = [];
for (const time of subordinados) {
for (const membro of time.membros) {
@@ -179,7 +169,8 @@
funcs.push({
_id: membro.funcionario._id,
nome: membro.funcionario.nome,
matricula: membro.funcionario.matricula
matricula: membro.funcionario.matricula,
fotoPerfilUrl: membro.funcionario.fotoPerfilUrl
});
}
}
@@ -187,29 +178,11 @@
return funcs;
});
function abrirEdicao(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';
}
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 = '';
@@ -390,16 +363,17 @@
<h2 class="card-title mb-4">Selecionar Funcionário</h2>
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<div class="form-control">
<label class="label">
<label class="label" for="funcionario-select">
<span class="label-text font-medium">Funcionário</span>
</label>
<select
class="select select-bordered w-full"
id="funcionario-select"
bind:value={funcionarioSelecionado}
disabled={modoEdicao}
>
<option value="">Selecione um funcionário</option>
{#each funcionarios as funcionario}
{#each funcionarios as funcionario (funcionario._id)}
<option value={funcionario._id as string}>
{funcionario.nome}
{funcionario.matricula ? `(${funcionario.matricula})` : ''}
@@ -408,24 +382,26 @@
</select>
</div>
<div class="form-control">
<label class="label">
<label class="label" for="data-inicio-filtro">
<span class="label-text font-medium">Data Início</span>
</label>
<input
type="date"
class="input input-bordered w-full"
id="data-inicio-filtro"
bind:value={dataInicioFiltro}
disabled={modoEdicao}
max={dataFimFiltro}
/>
</div>
<div class="form-control">
<label class="label">
<label class="label" for="data-fim-filtro">
<span class="label-text font-medium">Data Fim</span>
</label>
<input
type="date"
class="input input-bordered w-full"
id="data-fim-filtro"
bind:value={dataFimFiltro}
disabled={modoEdicao}
min={dataInicioFiltro}
@@ -488,7 +464,7 @@
<div class="space-y-8">
<!-- Card: Nova Hora -->
<div
class="card from-primary/5 to-primary/10 border-primary/20 border bg-gradient-to-br shadow-sm"
class="card from-primary/5 to-primary/10 border-primary/20 border bg-linear-to-br shadow-sm"
>
<div class="card-body p-6">
<div class="mb-4 flex items-center gap-3">
@@ -521,17 +497,18 @@
</h3>
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<div class="form-control">
<label class="label">
<label class="label" for="motivo-tipo-edicao">
<span class="label-text font-semibold">Tipo de Motivo</span>
<span class="label-text-alt text-error font-bold">*</span>
</label>
<select
class="select select-bordered select-primary w-full"
id="motivo-tipo-edicao"
bind:value={motivoTipo}
>
<option value="">Selecione um tipo</option>
{#if motivos?.opcoesPadrao}
{#each motivos.opcoesPadrao as opcao}
{#each motivos.opcoesPadrao as opcao (opcao)}
<option value={opcao}>{opcao}</option>
{/each}
{/if}
@@ -539,12 +516,13 @@
</div>
<div class="form-control">
<label class="label">
<label class="label" for="motivo-descricao-edicao">
<span class="label-text font-semibold">Descrição do Motivo</span>
</label>
<input
type="text"
class="input input-bordered input-primary w-full"
id="motivo-descricao-edicao"
bind:value={motivoDescricao}
placeholder="Informe detalhes adicionais sobre o motivo"
/>
@@ -590,7 +568,7 @@
<div class="space-y-8">
<!-- Card: Tipo de Ajuste -->
<div
class="card from-warning/5 to-warning/10 border-warning/20 border bg-gradient-to-br shadow-sm"
class="card from-warning/5 to-warning/10 border-warning/20 border bg-linear-to-br shadow-sm"
>
<div class="card-body p-6">
<div class="mb-4 flex items-center gap-3">
@@ -645,25 +623,27 @@
</h4>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="form-control">
<label class="label">
<label class="label" for="data-inicio-ajuste">
<span class="label-text font-semibold">Data Início</span>
<span class="label-text-alt text-error font-bold">*</span>
</label>
<input
type="date"
class="input input-bordered input-primary w-full"
id="data-inicio-ajuste"
bind:value={dataInicioAjuste}
/>
</div>
<div class="form-control">
<label class="label">
<label class="label" for="hora-inicio-ajuste">
<span class="label-text font-semibold">Hora Início</span>
<span class="label-text-alt text-error font-bold">*</span>
</label>
<input
type="time"
class="input input-bordered input-primary w-full"
id="hora-inicio-ajuste"
bind:value={horaInicioAjuste}
/>
</div>
@@ -685,26 +665,28 @@
</h4>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="form-control">
<label class="label">
<label class="label" for="data-fim-ajuste">
<span class="label-text font-semibold">Data Fim</span>
<span class="label-text-alt text-error font-bold">*</span>
</label>
<input
type="date"
class="input input-bordered input-secondary w-full"
id="data-fim-ajuste"
bind:value={dataFimAjuste}
min={dataInicioAjuste}
/>
</div>
<div class="form-control">
<label class="label">
<label class="label" for="hora-fim-ajuste">
<span class="label-text font-semibold">Hora Fim</span>
<span class="label-text-alt text-error font-bold">*</span>
</label>
<input
type="time"
class="input input-bordered input-secondary w-full"
id="hora-fim-ajuste"
bind:value={horaFimAjuste}
/>
</div>
@@ -751,17 +733,18 @@
</h3>
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<div class="form-control">
<label class="label">
<label class="label" for="motivo-tipo-ajuste">
<span class="label-text font-semibold">Tipo de Motivo</span>
<span class="label-text-alt text-error font-bold">*</span>
</label>
<select
class="select select-bordered select-warning w-full"
id="motivo-tipo-ajuste"
bind:value={motivoTipo}
>
<option value="">Selecione um tipo</option>
{#if motivos?.opcoesPadrao}
{#each motivos.opcoesPadrao as opcao}
{#each motivos.opcoesPadrao as opcao (opcao)}
<option value={opcao}>{opcao}</option>
{/each}
{/if}
@@ -769,12 +752,13 @@
</div>
<div class="form-control">
<label class="label">
<label class="label" for="motivo-descricao-ajuste">
<span class="label-text font-semibold">Descrição do Motivo</span>
</label>
<input
type="text"
class="input input-bordered input-warning w-full"
id="motivo-descricao-ajuste"
bind:value={motivoDescricao}
placeholder="Informe detalhes adicionais sobre o motivo"
/>
@@ -824,7 +808,7 @@
<div class="card-body">
<h2 class="card-title mb-4">Registros do Funcionário</h2>
{#if registrosQuery?.status === 'Loading'}
{#if registrosQuery?.isLoading}
<div class="flex items-center justify-center py-8">
<span class="loading loading-spinner loading-lg"></span>
</div>
@@ -846,7 +830,7 @@
</tr>
</thead>
<tbody>
{#each registros as registro}
{#each registros as registro (registro._id)}
<tr>
<td class="whitespace-nowrap">{registro.data}</td>
<td class="whitespace-nowrap">
@@ -896,7 +880,7 @@
{/if}
</h2>
{#if homologacoesQuery?.status === 'Loading'}
{#if homologacoesQuery?.isLoading}
<div class="flex items-center justify-center py-8">
<span class="loading loading-spinner loading-lg"></span>
</div>
@@ -923,20 +907,28 @@
</tr>
</thead>
<tbody>
{#each homologacoes as homologacao}
{#each homologacoes as homologacao (homologacao._id)}
<tr>
<td>
{new Date(homologacao.criadoEm).toLocaleDateString('pt-BR')}
</td>
{#if !funcionarioSelecionado}
<td>
{homologacao.funcionario?.nome || '-'}
{#if homologacao.funcionario?.matricula}
<br />
<span class="text-base-content/70 text-xs">
Mat: {homologacao.funcionario.matricula}
</span>
{/if}
<div class="flex items-center gap-3">
<UserAvatar
fotoPerfilUrl={homologacao.fotoPerfilUrl}
nome={homologacao.funcionario?.nome || 'Funcionário'}
size="sm"
/>
<div>
<div class="font-medium">{homologacao.funcionario?.nome || '-'}</div>
{#if homologacao.funcionario?.matricula}
<span class="text-base-content/70 text-xs">
Mat: {homologacao.funcionario.matricula}
</span>
{/if}
</div>
</div>
</td>
{/if}
<td>
@@ -957,7 +949,7 @@
homologacao.minutoAnterior || 0
)}
</span>
{' → '}
<span aria-hidden="true" class="mx-1"></span>
<span>
{formatarHoraPonto(
homologacao.horaNova || 0,
@@ -967,7 +959,7 @@
</div>
{:else if homologacao.ajusteMinutos}
<div class="text-sm">
{homologacao.periodoDias || 0}d {homologacao.periodoHoras || 0}h{' '}
{homologacao.periodoDias || 0}d {homologacao.periodoHoras || 0}h
{homologacao.periodoMinutos || 0}min
</div>
{/if}
@@ -1033,7 +1025,7 @@
<h4 class="mb-3 font-semibold">Informações Gerais</h4>
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<span class="font-medium">Data:</span>{' '}
<span class="font-medium">Data:&nbsp;</span>
{new Date(homologacaoSelecionada.criadoEm).toLocaleDateString('pt-BR', {
day: '2-digit',
month: '2-digit',
@@ -1043,21 +1035,31 @@
})}
</div>
<div>
<span class="font-medium">Funcionário:</span>{' '}
{homologacaoSelecionada.funcionario?.nome || '-'}
{#if homologacaoSelecionada.funcionario?.matricula}
<br />
<span class="text-base-content/70 text-xs">
Mat: {homologacaoSelecionada.funcionario.matricula}
</span>
{/if}
<span class="font-medium">Funcionário:</span>
<div class="mt-2 flex items-center gap-3">
<UserAvatar
fotoPerfilUrl={homologacaoSelecionada.fotoPerfilUrl}
nome={homologacaoSelecionada.funcionario?.nome || 'Funcionário'}
size="sm"
/>
<div>
<div class="font-medium">
{homologacaoSelecionada.funcionario?.nome || '-'}
</div>
{#if homologacaoSelecionada.funcionario?.matricula}
<span class="text-base-content/70 text-xs">
Mat: {homologacaoSelecionada.funcionario.matricula}
</span>
{/if}
</div>
</div>
</div>
<div>
<span class="font-medium">Gestor:</span>{' '}
<span class="font-medium">Gestor:&nbsp;</span>
{homologacaoSelecionada.gestor?.nome || '-'}
</div>
<div>
<span class="font-medium">Tipo:</span>{' '}
<span class="font-medium">Tipo:&nbsp;</span>
{#if homologacaoSelecionada.registroId}
<span class="badge badge-info">Edição de Registro</span>
{:else if homologacaoSelecionada.tipoAjuste}
@@ -1077,7 +1079,7 @@
<h4 class="mb-3 font-semibold">Edição de Registro</h4>
<div class="space-y-2 text-sm">
<div>
<span class="font-medium">Horário Anterior:</span>{' '}
<span class="font-medium">Horário Anterior:&nbsp;</span>
<span class="line-through opacity-70">
{formatarHoraPonto(
homologacaoSelecionada.horaAnterior || 0,
@@ -1086,7 +1088,7 @@
</span>
</div>
<div>
<span class="font-medium">Horário Novo:</span>{' '}
<span class="font-medium">Horário Novo:&nbsp;</span>
<span>
{formatarHoraPonto(
homologacaoSelecionada.horaNova || 0,
@@ -1096,11 +1098,11 @@
</div>
{#if homologacaoSelecionada.registro}
<div>
<span class="font-medium">Data do Registro:</span>{' '}
<span class="font-medium">Data do Registro:&nbsp;</span>
{new Date(homologacaoSelecionada.registro.data).toLocaleDateString('pt-BR')}
</div>
<div>
<span class="font-medium">Tipo de Registro:</span>{' '}
<span class="font-medium">Tipo de Registro:&nbsp;</span>
{getTipoRegistroLabel(homologacaoSelecionada.registro.tipo)}
</div>
{/if}
@@ -1113,20 +1115,20 @@
<h4 class="mb-3 font-semibold">Ajuste de Banco de Horas</h4>
<div class="space-y-2 text-sm">
<div>
<span class="font-medium">Tipo de Ajuste:</span>{' '}
<span class="font-medium">Tipo de Ajuste:&nbsp;</span>
{homologacaoSelecionada.tipoAjuste}
</div>
<div>
<span class="font-medium">Período:</span>{' '}
{homologacaoSelecionada.periodoDias || 0}d {homologacaoSelecionada.periodoHoras ||
0}h{' '}
<span class="font-medium">Período:&nbsp;</span>
{homologacaoSelecionada.periodoDias || 0}d
{homologacaoSelecionada.periodoHoras || 0}h
{homologacaoSelecionada.periodoMinutos || 0}min
</div>
{#if homologacaoSelecionada.ajusteMinutos}
<div>
<span class="font-medium">Ajuste Total:</span>{' '}
<span class="font-medium">Ajuste Total:&nbsp;</span>
{homologacaoSelecionada.ajusteMinutos > 0 ? '+' : ''}
{Math.floor(Math.abs(homologacaoSelecionada.ajusteMinutos) / 60)}h{' '}
{Math.floor(Math.abs(homologacaoSelecionada.ajusteMinutos) / 60)}h
{Math.abs(homologacaoSelecionada.ajusteMinutos) % 60}min
</div>
{/if}
@@ -1141,11 +1143,11 @@
<h4 class="mb-3 font-semibold">Motivo e Observações</h4>
<div class="space-y-2 text-sm">
<div>
<span class="font-medium">Tipo de Motivo:</span>{' '}
<span class="font-medium">Tipo de Motivo:&nbsp;</span>
{homologacaoSelecionada.motivoTipo || '-'}
</div>
<div>
<span class="font-medium">Descrição:</span>{' '}
<span class="font-medium">Descrição:&nbsp;</span>
{homologacaoSelecionada.motivoDescricao || '-'}
</div>
{#if homologacaoSelecionada.observacoes}
@@ -1165,7 +1167,12 @@
<button class="btn" onclick={fecharDetalhes}>Fechar</button>
</div>
</div>
<div class="modal-backdrop" onclick={fecharDetalhes}></div>
<button
type="button"
class="modal-backdrop"
onclick={fecharDetalhes}
aria-label="Fechar detalhes da homologação"
></button>
</div>
{/if}
@@ -1185,7 +1192,12 @@
</button>
</div>
</div>
<div class="modal-backdrop" onclick={fecharModalExcluir}></div>
<button
type="button"
class="modal-backdrop"
onclick={fecharModalExcluir}
aria-label="Fechar modal de confirmação de exclusão"
></button>
</div>
{/if}
</div>