Compare commits

...

13 Commits

Author SHA1 Message Date
b76c308aab feat: add ratelimit model support in API and data model definitions, enhancing rate limiting functionality 2025-12-14 20:56:27 -03:00
67b2091d96 Merge remote-tracking branch 'origin/master' into ajustes_gerais 2025-12-14 20:54:55 -03:00
1b1d2fb97e chore: add empty lines to enhance code readability in error handling components 2025-12-14 20:39:50 -03:00
16ede85bc2 chore: add empty lines to improve code readability in error handling components and fichaPontoPDF 2025-12-14 20:35:19 -03:00
Kilder Costa
a951f61676 Merge pull request #65 from killer-cf/refactor-auth
Refactor auth
2025-12-13 19:13:28 -03:00
Kilder Costa
98d12d40ef Merge pull request #64 from killer-cf/ajustes_gerais
Ajustes gerais
2025-12-12 11:15:21 -03:00
457e89e386 feat: enhance time synchronization logic with timeout and loading state management 2025-12-12 11:13:56 -03:00
Kilder Costa
10454b38ea Merge pull request #63 from killer-cf/feat-pedidos
Feat pedidos
2025-12-12 10:22:53 -03:00
6936a59c21 feat: implement cascading recalculation of monthly hour banks when past months are updated or adjusted 2025-12-11 16:52:07 -03:00
Kilder Costa
813d614648 Merge pull request #62 from killer-cf/ajustes_gerais
chore: add empty lines to improve code readability in fichaPontoPDF a…
2025-12-11 11:54:27 -03:00
196ef90643 chore: add empty lines to improve code readability in fichaPontoPDF and error handling components 2025-12-11 11:53:20 -03:00
Kilder Costa
1a56f2ab64 Merge pull request #61 from killer-cf/feat-pedidos
feat: add optional 'aceitoPor' field to pedidos query for enhanced it…
2025-12-11 11:51:07 -03:00
Kilder Costa
52e6805c09 Merge pull request #60 from killer-cf/feat-pedidos
Feat pedidos
2025-12-11 10:36:38 -03:00
7 changed files with 277 additions and 18 deletions

View File

@@ -9,12 +9,21 @@
let tempoAtual = $state<Date>(new Date()); let tempoAtual = $state<Date>(new Date());
let sincronizado = $state(false); let sincronizado = $state(false);
let sincronizando = $state(false);
let usandoServidorExterno = $state(false); let usandoServidorExterno = $state(false);
let offsetSegundos = $state(0); let offsetSegundos = $state(0);
let erro = $state<string | null>(null); let erro = $state<string | null>(null);
let intervalId: ReturnType<typeof setInterval> | null = null; let intervalId: ReturnType<typeof setInterval> | null = null;
let intervaloSincronizacao: ReturnType<typeof setInterval> | null = null;
let sincronizacaoEmAndamento = $state(false); // Flag para evitar múltiplas sincronizações simultâneas
async function atualizarTempo() { async function atualizarTempo() {
// Evitar múltiplas sincronizações simultâneas
if (sincronizacaoEmAndamento) {
return;
}
sincronizacaoEmAndamento = true;
sincronizando = true;
try { try {
const config = await client.query(api.configuracaoRelogio.obterConfiguracao, {}); const config = await client.query(api.configuracaoRelogio.obterConfiguracao, {});
// Usar gmtOffset da configuração, sem valor padrão, pois 0 é um valor válido // Usar gmtOffset da configuração, sem valor padrão, pois 0 é um valor válido
@@ -25,7 +34,12 @@
if (config.usarServidorExterno) { if (config.usarServidorExterno) {
try { try {
const resultado = await client.action(api.configuracaoRelogio.sincronizarTempo, {}); // Adicionar timeout de 10 segundos para sincronização
const sincronizacaoPromise = client.action(api.configuracaoRelogio.sincronizarTempo, {});
const timeoutPromise = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Timeout na sincronização (10s)')), 10000)
);
const resultado = await Promise.race([sincronizacaoPromise, timeoutPromise]);
if (resultado.sucesso && resultado.timestamp) { if (resultado.sucesso && resultado.timestamp) {
timestampBase = resultado.timestamp; timestampBase = resultado.timestamp;
sincronizado = true; sincronizado = true;
@@ -43,7 +57,11 @@
usandoServidorExterno = false; usandoServidorExterno = false;
erro = 'Usando relógio do PC (falha na sincronização)'; erro = 'Usando relógio do PC (falha na sincronização)';
} else { } else {
throw error; // Mesmo sem fallback configurado, usar PC como última opção
timestampBase = obterTempoPC();
sincronizado = false;
usandoServidorExterno = false;
erro = 'Usando relógio do PC (servidor indisponível)';
} }
} }
} else { } else {
@@ -71,6 +89,9 @@
tempoAtual = new Date(obterTempoPC()); tempoAtual = new Date(obterTempoPC());
sincronizado = false; sincronizado = false;
erro = 'Erro ao obter tempo do servidor'; erro = 'Erro ao obter tempo do servidor';
} finally {
sincronizando = false;
sincronizacaoEmAndamento = false;
} }
} }
@@ -81,17 +102,34 @@
} }
onMount(async () => { onMount(async () => {
await atualizarTempo(); // Inicializar com relógio do PC imediatamente para não bloquear a interface
// Sincronizar a cada 30 segundos tempoAtual = new Date(obterTempoPC());
setInterval(atualizarTempo, 30000); sincronizado = false;
erro = 'Usando relógio do PC';
// Atualizar display a cada segundo // Atualizar display a cada segundo
intervalId = setInterval(atualizarRelogio, 1000); intervalId = setInterval(atualizarRelogio, 1000);
// Sincronizar em background (não bloquear) após um pequeno delay para garantir que a UI está renderizada
setTimeout(() => {
atualizarTempo().catch((error) => {
console.error('Erro ao sincronizar tempo em background:', error);
});
}, 100);
// Sincronizar a cada 30 segundos
intervaloSincronizacao = setInterval(() => {
atualizarTempo().catch((error) => {
console.error('Erro ao sincronizar tempo periódico:', error);
});
}, 30000);
}); });
onDestroy(() => { onDestroy(() => {
if (intervalId) { if (intervalId) {
clearInterval(intervalId); clearInterval(intervalId);
} }
if (intervaloSincronizacao) {
clearInterval(intervaloSincronizacao);
}
sincronizacaoEmAndamento = false;
}); });
const horaFormatada = $derived.by(() => { const horaFormatada = $derived.by(() => {
@@ -131,13 +169,18 @@
<!-- Status de Sincronização --> <!-- Status de Sincronização -->
<div <div
class="flex items-center gap-2 rounded-full px-4 py-2 {sincronizado class="flex items-center gap-2 rounded-full px-4 py-2 {sincronizando
? 'bg-info/20 text-info border-info/30 border animate-pulse'
: sincronizado
? 'bg-success/20 text-success border-success/30 border' ? 'bg-success/20 text-success border-success/30 border'
: erro : erro
? 'bg-warning/20 text-warning border-warning/30 border' ? 'bg-warning/20 text-warning border-warning/30 border'
: 'bg-base-300/50 text-base-content/60 border-base-300 border'}" : 'bg-base-300/50 text-base-content/60 border-base-300 border'}"
> >
{#if sincronizado} {#if sincronizando}
<span class="loading loading-spinner loading-sm text-info"></span>
<span class="text-sm font-semibold">Sincronizando com servidor...</span>
{:else if sincronizado}
<CheckCircle2 class="h-4 w-4" strokeWidth={2.5} /> <CheckCircle2 class="h-4 w-4" strokeWidth={2.5} />
<span class="text-sm font-semibold"> <span class="text-sm font-semibold">
{#if usandoServidorExterno} {#if usandoServidorExterno}

View File

@@ -444,3 +444,9 @@ export function adicionarRodape(doc: jsPDF): void {

View File

@@ -82,4 +82,10 @@

View File

@@ -82,4 +82,10 @@

View File

@@ -352,6 +352,10 @@ export declare const components: {
lastRequest?: null | number; lastRequest?: null | number;
}; };
model: "rateLimit"; model: "rateLimit";
}
| {
data: { count: number; key: string; lastRequest: number };
model: "ratelimit";
}; };
onCreateHandle?: string; onCreateHandle?: string;
select?: Array<string>; select?: Array<string>;
@@ -729,6 +733,32 @@ export declare const components: {
| Array<number> | Array<number>
| null; | null;
}>; }>;
}
| {
model: "ratelimit";
where?: Array<{
connector?: "AND" | "OR";
field: "key" | "count" | "lastRequest" | "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}; };
onDeleteHandle?: string; onDeleteHandle?: string;
paginationOpts: { paginationOpts: {
@@ -1113,6 +1143,32 @@ export declare const components: {
| Array<number> | Array<number>
| null; | null;
}>; }>;
}
| {
model: "ratelimit";
where?: Array<{
connector?: "AND" | "OR";
field: "key" | "count" | "lastRequest" | "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}; };
onDeleteHandle?: string; onDeleteHandle?: string;
}, },
@@ -1134,7 +1190,8 @@ export declare const components: {
| "oauthAccessToken" | "oauthAccessToken"
| "oauthConsent" | "oauthConsent"
| "jwks" | "jwks"
| "rateLimit"; | "rateLimit"
| "ratelimit";
offset?: number; offset?: number;
paginationOpts: { paginationOpts: {
cursor: string | null; cursor: string | null;
@@ -1186,7 +1243,8 @@ export declare const components: {
| "oauthAccessToken" | "oauthAccessToken"
| "oauthConsent" | "oauthConsent"
| "jwks" | "jwks"
| "rateLimit"; | "rateLimit"
| "ratelimit";
select?: Array<string>; select?: Array<string>;
where?: Array<{ where?: Array<{
connector?: "AND" | "OR"; connector?: "AND" | "OR";
@@ -1695,6 +1753,33 @@ export declare const components: {
| Array<number> | Array<number>
| null; | null;
}>; }>;
}
| {
model: "ratelimit";
update: { count?: number; key?: string; lastRequest?: number };
where?: Array<{
connector?: "AND" | "OR";
field: "key" | "count" | "lastRequest" | "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}; };
onUpdateHandle?: string; onUpdateHandle?: string;
paginationOpts: { paginationOpts: {
@@ -2183,6 +2268,33 @@ export declare const components: {
| Array<number> | Array<number>
| null; | null;
}>; }>;
}
| {
model: "ratelimit";
update: { count?: number; key?: string; lastRequest?: number };
where?: Array<{
connector?: "AND" | "OR";
field: "key" | "count" | "lastRequest" | "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}; };
onUpdateHandle?: string; onUpdateHandle?: string;
}, },

View File

@@ -38,7 +38,7 @@ export type Doc<TableName extends TableNames> = DocumentByName<
* Convex documents are uniquely identified by their `Id`, which is accessible * Convex documents are uniquely identified by their `Id`, which is accessible
* on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids). * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
* *
* Documents can be loaded using `db.get(tableName, id)` in query and mutation functions. * Documents can be loaded using `db.get(id)` in query and mutation functions.
* *
* IDs are just strings at runtime, but this type can be used to distinguish them from other * IDs are just strings at runtime, but this type can be used to distinguish them from other
* strings when type checking. * strings when type checking.

View File

@@ -1848,7 +1848,14 @@ async function atualizarBancoHoras(
// Atualizar banco de horas mensal // Atualizar banco de horas mensal
const mes = data.substring(0, 7); // YYYY-MM const mes = data.substring(0, 7); // YYYY-MM
await calcularBancoHorasMensal(ctx, funcionarioId, mes);
// Verificar se estamos editando um mês passado
const hoje = new Date();
const mesAtual = `${hoje.getFullYear()}-${String(hoje.getMonth() + 1).padStart(2, '0')}`;
const estaEditandoMesPassado = mes < mesAtual;
// Se estamos editando um mês passado, recalcular em cascata para atualizar meses seguintes
await calcularBancoHorasMensal(ctx, funcionarioId, mes, estaEditandoMesPassado);
} }
/** /**
@@ -1979,14 +1986,74 @@ export const obterBancoHorasFuncionario = query({
} }
}); });
/**
* Recalcula meses seguintes em cascata quando um mês anterior é atualizado
* Isso garante que os saldos iniciais dos meses seguintes sejam atualizados corretamente
*/
async function recalcularMesesSeguintes(
ctx: MutationCtx,
funcionarioId: Id<'funcionarios'>,
mesAtualizado: string // YYYY-MM do mês que foi atualizado
): Promise<void> {
const hoje = new Date();
const mesAtual = `${hoje.getFullYear()}-${String(hoje.getMonth() + 1).padStart(2, '0')}`;
// Se o mês atualizado já é o mês atual ou futuro, não precisa recalcular nada
if (mesAtualizado >= mesAtual) {
return;
}
// Recalcular todos os meses do mês seguinte ao atualizado até o mês atual
// Calcular primeiro mês a recalcular (mês seguinte ao atualizado)
const [anoAtualizado, mesNumAtualizado] = mesAtualizado.split('-').map(Number);
let anoIter = anoAtualizado;
let mesNumIter = mesNumAtualizado + 1;
if (mesNumIter > 12) {
mesNumIter = 1;
anoIter += 1;
}
// Continuar enquanto o mês iterado for menor ou igual ao mês atual
while (true) {
const mesIterStr = `${anoIter}-${String(mesNumIter).padStart(2, '0')}`;
// Se passou do mês atual, parar
if (mesIterStr > mesAtual) {
break;
}
// Verificar se existe registro mensal para este mês
const bancoMensalExistente = await ctx.db
.query('bancoHorasMensal')
.withIndex('by_funcionario_mes', (q) =>
q.eq('funcionarioId', funcionarioId).eq('mes', mesIterStr)
)
.first();
// Se existe registro, recalcular (o saldo inicial mudou porque o mês anterior mudou)
if (bancoMensalExistente) {
await calcularBancoHorasMensal(ctx, funcionarioId, mesIterStr, false); // false = não recalcular cascata novamente
}
// Avançar para o próximo mês
mesNumIter += 1;
if (mesNumIter > 12) {
mesNumIter = 1;
anoIter += 1;
}
}
}
/** /**
* Calcula e atualiza banco de horas mensal para um funcionário * Calcula e atualiza banco de horas mensal para um funcionário
* Esta função deve ser chamada após atualizações no banco de horas diário * Esta função deve ser chamada após atualizações no banco de horas diário
* @param recalcularCascata - Se true, recalcula automaticamente os meses seguintes (padrão: true)
*/ */
async function calcularBancoHorasMensal( async function calcularBancoHorasMensal(
ctx: MutationCtx, ctx: MutationCtx,
funcionarioId: Id<'funcionarios'>, funcionarioId: Id<'funcionarios'>,
mes: string // YYYY-MM mes: string, // YYYY-MM
recalcularCascata: boolean = true // Por padrão, recalcula em cascata
): Promise<void> { ): Promise<void> {
// Buscar todos os bancoHoras do mês // Buscar todos os bancoHoras do mês
const dataInicio = `${mes}-01`; const dataInicio = `${mes}-01`;
@@ -2106,6 +2173,11 @@ async function calcularBancoHorasMensal(
atualizadoEm: agora atualizadoEm: agora
}); });
} }
// Recalcular meses seguintes em cascata se solicitado
if (recalcularCascata) {
await recalcularMesesSeguintes(ctx, funcionarioId, mes);
}
} }
/** /**
@@ -2611,7 +2683,14 @@ export const ajustarBancoHoras = mutation({
// Recalcular banco de horas mensal após ajuste // Recalcular banco de horas mensal após ajuste
const mes = hoje.substring(0, 7); // YYYY-MM const mes = hoje.substring(0, 7); // YYYY-MM
await calcularBancoHorasMensal(ctx, args.funcionarioId, mes);
// Verificar se estamos ajustando um mês passado
const hojeDate = new Date();
const mesAtual = `${hojeDate.getFullYear()}-${String(hojeDate.getMonth() + 1).padStart(2, '0')}`;
const estaAjustandoMesPassado = mes < mesAtual;
// Se estamos ajustando um mês passado, recalcular em cascata para atualizar meses seguintes
await calcularBancoHorasMensal(ctx, args.funcionarioId, mes, estaAjustandoMesPassado);
// Criar registro de homologação (mantido para compatibilidade) // Criar registro de homologação (mantido para compatibilidade)
const homologacaoId = await ctx.db.insert('homologacoesPonto', { const homologacaoId = await ctx.db.insert('homologacoesPonto', {
@@ -3872,7 +3951,14 @@ export const criarAjusteBancoHoras = mutation({
// Recalcular banco de horas mensal // Recalcular banco de horas mensal
const mes = args.dataAplicacao.substring(0, 7); const mes = args.dataAplicacao.substring(0, 7);
await calcularBancoHorasMensal(ctx, args.funcionarioId, mes);
// Verificar se estamos aplicando ajuste em um mês passado
const hoje = new Date();
const mesAtual = `${hoje.getFullYear()}-${String(hoje.getMonth() + 1).padStart(2, '0')}`;
const estaAplicandoEmMesPassado = mes < mesAtual;
// Se estamos aplicando em um mês passado, recalcular em cascata para atualizar meses seguintes
await calcularBancoHorasMensal(ctx, args.funcionarioId, mes, estaAplicandoEmMesPassado);
return { ajusteId, success: true }; return { ajusteId, success: true };
} }