refactor: update fichaPontoPDF and processamento to enhance legend styling and accumulate saldo for all days, improving report accuracy

This commit is contained in:
2025-12-15 11:50:51 -03:00
parent a951f61676
commit 60b53dac74
6 changed files with 124 additions and 36 deletions

View File

@@ -372,29 +372,59 @@ export function adicionarLegenda(doc: jsPDF, yPosition: number): number {
doc.text('LEGENDA', 15, yPosition); doc.text('LEGENDA', 15, yPosition);
yPosition += 10; yPosition += 10;
const legendaData: Array<[string, string]> = [ // Corpo da legenda com cores aplicadas e siglas intuitivas
['Cor de Fundo - Branco', 'Dia normal'], const legendaData: Array<
['Cor de Fundo - Azul Claro', 'Dia com atestado médico'], [
['Cor de Fundo - Amarelo Claro', 'Dia com ausência aprovada'], string | { content: string; styles?: { fillColor?: number[]; textColor?: number[]; fontStyle?: string } },
['Cor de Fundo - Verde Claro', 'Dia abonado'], string
['Cor de Fundo - Cinza Claro', 'Dia não computado (dispensa/férias)'], ]
['Cor de Fundo - Laranja Claro', 'Dia com inconsistência'], > = [
['Texto Verde', 'Saldo positivo / Registro marcado'], [
['Texto Vermelho', 'Saldo negativo / Registro não marcado'], { content: 'Fundo Branco (DN)', styles: { fillColor: [255, 255, 255] } },
['✓', 'Registro marcado'], 'Dia normal'
['✗', 'Registro não marcado'], ],
['⚠', 'Inconsistência detectada'], [
['🏥', 'Atestado médico'], { content: 'Fundo Azul Claro (AT)', styles: { fillColor: [230, 240, 255] } },
['🚫', 'Ausência'], 'Dia com atestado médico'
['📋', 'Licença'], ],
['✅', 'Abonado'], [
['⏸', 'Não computado'] { content: 'Fundo Amarelo Claro (AUS)', styles: { fillColor: [255, 255, 230] } },
'Dia com ausência aprovada'
],
[
{ content: 'Fundo Verde Claro (ABO)', styles: { fillColor: [230, 255, 230] } },
'Dia abonado'
],
[
{ content: 'Fundo Cinza Claro (NC)', styles: { fillColor: [240, 240, 240] } },
'Dia não computado (dispensa/férias)'
],
[
{ content: 'Fundo Laranja Claro (INC)', styles: { fillColor: [255, 240, 230] } },
'Dia com inconsistência'
],
[
{ content: 'Texto Verde', styles: { textColor: [0, 128, 0], fontStyle: 'bold' } },
'Saldo positivo / Registro marcado'
],
[
{ content: 'Texto Vermelho', styles: { textColor: [200, 0, 0], fontStyle: 'bold' } },
'Saldo negativo / Registro não marcado'
],
['RM', 'Registro marcado'],
['RNM', 'Registro não marcado'],
['INC', 'Inconsistência detectada'],
['AT', 'Atestado médico'],
['AUS', 'Ausência'],
['LIC', 'Licença'],
['ABO', 'Abonado'],
['NC', 'Não computado']
]; ];
autoTable(doc, { autoTable(doc, {
startY: yPosition, startY: yPosition,
head: [['Símbolo/Cor', 'Significado']], head: [['Símbolo/Cor', 'Significado']],
body: legendaData, body: legendaData as unknown as Array<Array<string | { content: string; styles?: { fillColor?: number[]; textColor?: number[]; fontStyle?: string } }>>,
theme: 'striped', theme: 'striped',
headStyles: { headStyles: {
fillColor: [60, 60, 60], fillColor: [60, 60, 60],
@@ -410,7 +440,25 @@ export function adicionarLegenda(doc: jsPDF, yPosition: number): number {
1: { cellWidth: 110 } 1: { cellWidth: 110 }
}, },
margin: { left: 15, right: 15 }, margin: { left: 15, right: 15 },
styles: { cellPadding: 3 } styles: { cellPadding: 3 },
didParseCell: (data) => {
// aplicar estilos de cor/texto definidos nas células da primeira coluna
if (data.section === 'body' && data.column.index === 0) {
const raw = data.row.raw?.[0];
if (raw && typeof raw === 'object' && 'styles' in raw && raw.styles) {
const styles = raw.styles as { fillColor?: number[]; textColor?: number[]; fontStyle?: string };
if (styles.fillColor) {
data.cell.styles.fillColor = styles.fillColor;
}
if (styles.textColor) {
data.cell.styles.textColor = styles.textColor;
}
if (styles.fontStyle) {
data.cell.styles.fontStyle = styles.fontStyle;
}
}
}
}
}); });
const finalYLegenda = (doc as JsPDFWithAutoTable).lastAutoTable?.finalY ?? yPosition + 10; const finalYLegenda = (doc as JsPDFWithAutoTable).lastAutoTable?.finalY ?? yPosition + 10;
@@ -446,3 +494,4 @@ export function adicionarRodape(doc: jsPDF): void {

View File

@@ -628,25 +628,34 @@ export async function processarDadosFichaPonto(
} }
// Calcular saldo acumulado para cada dia // Calcular saldo acumulado para cada dia
// Agora consideramos todos os dias que possuem saldo diário, inclusive
// atestados, ausências e dias não computados, para que o resumo do período
// reflita qualquer trabalho realizado e a carga horária esperada.
let saldoAcumulado = 0; let saldoAcumulado = 0;
for (const dia of diasProcessados) { for (const dia of diasProcessados) {
if (dia.computado && dia.saldoDiario) { if (dia.saldoDiario) {
saldoAcumulado += dia.saldoDiario.diferencaMinutos; saldoAcumulado += dia.saldoDiario.diferencaMinutos;
} }
dia.saldoAcumulado = saldoAcumulado; dia.saldoAcumulado = saldoAcumulado;
} }
// Calcular resumo com formatações // Calcular resumo com formatações
const totalHorasTrabalhadas = diasProcessados // Total de horas trabalhadas e esperadas passa a considerar todos os dias,
.filter((d) => d.computado) // não apenas os marcados como "computados", para que trechos trabalhados
.reduce((acc, d) => acc + (d.saldoDiario?.trabalhadoMinutos || 0), 0); // em dias de ausência/dispensa também apareçam no resumo.
const totalHorasEsperadas = diasProcessados const totalHorasTrabalhadas = diasProcessados.reduce(
.filter((d) => d.computado) (acc, d) => acc + (d.saldoDiario?.trabalhadoMinutos || 0),
.reduce((acc, d) => acc + (d.saldoDiario?.esperadoMinutos || 0), 0); 0
const diferencaTotal = diasProcessados );
.filter((d) => d.computado) const totalHorasEsperadas = diasProcessados.reduce(
.reduce((acc, d) => acc + (d.saldoDiario?.diferencaMinutos || 0), 0); (acc, d) => acc + (d.saldoDiario?.esperadoMinutos || 0),
0
);
const diferencaTotal = diasProcessados.reduce(
(acc, d) => acc + (d.saldoDiario?.diferencaMinutos || 0),
0
);
const saldoPeriodo = diferencaTotal; const saldoPeriodo = diferencaTotal;
const saldoFinal = const saldoFinal =
diasProcessados.length > 0 ? diasProcessados[diasProcessados.length - 1]!.saldoAcumulado : 0; diasProcessados.length > 0 ? diasProcessados[diasProcessados.length - 1]!.saldoAcumulado : 0;

View File

@@ -85,3 +85,4 @@

View File

@@ -85,3 +85,4 @@

View File

@@ -1,6 +1,18 @@
import { v } from 'convex/values'; import { v } from 'convex/values';
import { mutation } from './_generated/server'; import { mutation, type MutationCtx } from './_generated/server';
import { authComponent, updatePassword } from './auth'; import { authComponent, updatePassword } from './auth';
import type { GenericCtx } from '@convex-dev/better-auth';
import type { DataModel } from './_generated/dataModel';
/**
* Helper para converter MutationCtx para GenericCtx do better-auth
* Os tipos são estruturalmente compatíveis, apenas há diferença nas definições de tipo
*/
function toGenericCtx(ctx: MutationCtx): GenericCtx<DataModel> {
// Os tipos são estruturalmente idênticos, apenas há diferença nas definições de tipo
// entre a versão do Convex usada pelo projeto e a usada pelo @convex-dev/better-auth
return ctx as unknown as GenericCtx<DataModel>;
}
/** /**
* Alterar senha do usuário autenticado * Alterar senha do usuário autenticado
@@ -19,7 +31,7 @@ export const alterarSenha = mutation({
handler: async (ctx, args) => { handler: async (ctx, args) => {
try { try {
// Verificar se o usuário está autenticado // Verificar se o usuário está autenticado
const authUser = await authComponent.safeGetAuthUser(ctx); const authUser = await authComponent.safeGetAuthUser(toGenericCtx(ctx));
if (!authUser) { if (!authUser) {
return { return {
sucesso: false as const, sucesso: false as const,

View File

@@ -3,7 +3,7 @@ import { convex } from '@convex-dev/better-auth/plugins';
import { betterAuth } from 'better-auth'; import { betterAuth } from 'better-auth';
import { components } from './_generated/api'; import { components } from './_generated/api';
import type { DataModel } from './_generated/dataModel'; import type { DataModel } from './_generated/dataModel';
import { type MutationCtx, type QueryCtx, query } from './_generated/server'; import { type MutationCtx, type QueryCtx, type ActionCtx, query } from './_generated/server';
// Usar SITE_URL se disponível, caso contrário usar CONVEX_SITE_URL ou um valor padrão // Usar SITE_URL se disponível, caso contrário usar CONVEX_SITE_URL ou um valor padrão
const siteUrl = process.env.SITE_URL || process.env.CONVEX_SITE_URL || 'http://localhost:5173'; const siteUrl = process.env.SITE_URL || process.env.CONVEX_SITE_URL || 'http://localhost:5173';
@@ -14,6 +14,22 @@ console.log('siteUrl:', siteUrl);
// as well as helper methods for general use. // as well as helper methods for general use.
export const authComponent = createClient<DataModel>(components.betterAuth); export const authComponent = createClient<DataModel>(components.betterAuth);
/**
* Helper type para converter contextos do Convex para GenericCtx do better-auth
* Isso resolve incompatibilidade de tipos entre versões do Convex sem usar 'any'
*/
type ConvexCtx = QueryCtx | MutationCtx | ActionCtx;
/**
* Função helper para converter contexto do Convex para GenericCtx do better-auth
* Os tipos são estruturalmente compatíveis, apenas há diferença nas definições de tipo
*/
function toGenericCtx(ctx: ConvexCtx): GenericCtx<DataModel> {
// Os tipos são estruturalmente idênticos, apenas há diferença nas definições de tipo
// entre a versão do Convex usada pelo projeto e a usada pelo @convex-dev/better-auth
return ctx as unknown as GenericCtx<DataModel>;
}
export const createAuth = ( export const createAuth = (
ctx: GenericCtx<DataModel>, ctx: GenericCtx<DataModel>,
{ optionsOnly } = { optionsOnly: false } { optionsOnly } = { optionsOnly: false }
@@ -45,7 +61,7 @@ export const getCurrentUser = query({
args: {}, args: {},
handler: async (ctx) => { handler: async (ctx) => {
try { try {
const authUser = await authComponent.safeGetAuthUser(ctx); const authUser = await authComponent.safeGetAuthUser(toGenericCtx(ctx));
if (!authUser) { if (!authUser) {
return; return;
} }
@@ -83,7 +99,7 @@ export const getCurrentUser = query({
}); });
export const getCurrentUserFunction = async (ctx: QueryCtx | MutationCtx) => { export const getCurrentUserFunction = async (ctx: QueryCtx | MutationCtx) => {
const authUser = await authComponent.safeGetAuthUser(ctx); const authUser = await authComponent.safeGetAuthUser(toGenericCtx(ctx));
if (!authUser) { if (!authUser) {
return; return;
} }
@@ -102,7 +118,7 @@ export const createAuthUser = async (
ctx: MutationCtx, ctx: MutationCtx,
args: { nome: string; email: string; password: string } args: { nome: string; email: string; password: string }
) => { ) => {
const { auth, headers } = await authComponent.getAuth(createAuth, ctx); const { auth, headers } = await authComponent.getAuth(createAuth, toGenericCtx(ctx));
const result = await auth.api.signUpEmail({ const result = await auth.api.signUpEmail({
headers, headers,
@@ -120,7 +136,7 @@ export const updatePassword = async (
ctx: MutationCtx, ctx: MutationCtx,
args: { newPassword: string; currentPassword: string } args: { newPassword: string; currentPassword: string }
) => { ) => {
const { auth, headers } = await authComponent.getAuth(createAuth, ctx); const { auth, headers } = await authComponent.getAuth(createAuth, toGenericCtx(ctx));
await auth.api.changePassword({ await auth.api.changePassword({
headers, headers,