refactor: update fichaPontoPDF and processamento to enhance legend styling and accumulate saldo for all days, improving report accuracy
This commit is contained in:
@@ -372,29 +372,59 @@ export function adicionarLegenda(doc: jsPDF, yPosition: number): number {
|
||||
doc.text('LEGENDA', 15, yPosition);
|
||||
yPosition += 10;
|
||||
|
||||
const legendaData: Array<[string, string]> = [
|
||||
['Cor de Fundo - Branco', 'Dia normal'],
|
||||
['Cor de Fundo - Azul Claro', 'Dia com atestado médico'],
|
||||
['Cor de Fundo - Amarelo Claro', 'Dia com ausência aprovada'],
|
||||
['Cor de Fundo - Verde Claro', 'Dia abonado'],
|
||||
['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'],
|
||||
['✓', 'Registro marcado'],
|
||||
['✗', 'Registro não marcado'],
|
||||
['⚠', 'Inconsistência detectada'],
|
||||
['🏥', 'Atestado médico'],
|
||||
['🚫', 'Ausência'],
|
||||
['📋', 'Licença'],
|
||||
['✅', 'Abonado'],
|
||||
['⏸', 'Não computado']
|
||||
// Corpo da legenda com cores aplicadas e siglas intuitivas
|
||||
const legendaData: Array<
|
||||
[
|
||||
string | { content: string; styles?: { fillColor?: number[]; textColor?: number[]; fontStyle?: string } },
|
||||
string
|
||||
]
|
||||
> = [
|
||||
[
|
||||
{ content: 'Fundo Branco (DN)', styles: { fillColor: [255, 255, 255] } },
|
||||
'Dia normal'
|
||||
],
|
||||
[
|
||||
{ content: 'Fundo Azul Claro (AT)', styles: { fillColor: [230, 240, 255] } },
|
||||
'Dia com atestado médico'
|
||||
],
|
||||
[
|
||||
{ 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, {
|
||||
startY: yPosition,
|
||||
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',
|
||||
headStyles: {
|
||||
fillColor: [60, 60, 60],
|
||||
@@ -410,7 +440,25 @@ export function adicionarLegenda(doc: jsPDF, yPosition: number): number {
|
||||
1: { cellWidth: 110 }
|
||||
},
|
||||
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;
|
||||
@@ -446,3 +494,4 @@ export function adicionarRodape(doc: jsPDF): void {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -628,25 +628,34 @@ export async function processarDadosFichaPonto(
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
for (const dia of diasProcessados) {
|
||||
if (dia.computado && dia.saldoDiario) {
|
||||
if (dia.saldoDiario) {
|
||||
saldoAcumulado += dia.saldoDiario.diferencaMinutos;
|
||||
}
|
||||
dia.saldoAcumulado = saldoAcumulado;
|
||||
}
|
||||
|
||||
// Calcular resumo com formatações
|
||||
const totalHorasTrabalhadas = diasProcessados
|
||||
.filter((d) => d.computado)
|
||||
.reduce((acc, d) => acc + (d.saldoDiario?.trabalhadoMinutos || 0), 0);
|
||||
const totalHorasEsperadas = diasProcessados
|
||||
.filter((d) => d.computado)
|
||||
.reduce((acc, d) => acc + (d.saldoDiario?.esperadoMinutos || 0), 0);
|
||||
const diferencaTotal = diasProcessados
|
||||
.filter((d) => d.computado)
|
||||
.reduce((acc, d) => acc + (d.saldoDiario?.diferencaMinutos || 0), 0);
|
||||
// Total de horas trabalhadas e esperadas passa a considerar todos os dias,
|
||||
// não apenas os marcados como "computados", para que trechos trabalhados
|
||||
// em dias de ausência/dispensa também apareçam no resumo.
|
||||
const totalHorasTrabalhadas = diasProcessados.reduce(
|
||||
(acc, d) => acc + (d.saldoDiario?.trabalhadoMinutos || 0),
|
||||
0
|
||||
);
|
||||
const totalHorasEsperadas = diasProcessados.reduce(
|
||||
(acc, d) => acc + (d.saldoDiario?.esperadoMinutos || 0),
|
||||
0
|
||||
);
|
||||
const diferencaTotal = diasProcessados.reduce(
|
||||
(acc, d) => acc + (d.saldoDiario?.diferencaMinutos || 0),
|
||||
0
|
||||
);
|
||||
const saldoPeriodo = diferencaTotal;
|
||||
const saldoFinal =
|
||||
diasProcessados.length > 0 ? diasProcessados[diasProcessados.length - 1]!.saldoAcumulado : 0;
|
||||
|
||||
@@ -85,3 +85,4 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -85,3 +85,4 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
import { v } from 'convex/values';
|
||||
import { mutation } from './_generated/server';
|
||||
import { mutation, type MutationCtx } from './_generated/server';
|
||||
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
|
||||
@@ -19,7 +31,7 @@ export const alterarSenha = mutation({
|
||||
handler: async (ctx, args) => {
|
||||
try {
|
||||
// Verificar se o usuário está autenticado
|
||||
const authUser = await authComponent.safeGetAuthUser(ctx);
|
||||
const authUser = await authComponent.safeGetAuthUser(toGenericCtx(ctx));
|
||||
if (!authUser) {
|
||||
return {
|
||||
sucesso: false as const,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { convex } from '@convex-dev/better-auth/plugins';
|
||||
import { betterAuth } from 'better-auth';
|
||||
import { components } from './_generated/api';
|
||||
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
|
||||
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.
|
||||
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 = (
|
||||
ctx: GenericCtx<DataModel>,
|
||||
{ optionsOnly } = { optionsOnly: false }
|
||||
@@ -45,7 +61,7 @@ export const getCurrentUser = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
try {
|
||||
const authUser = await authComponent.safeGetAuthUser(ctx);
|
||||
const authUser = await authComponent.safeGetAuthUser(toGenericCtx(ctx));
|
||||
if (!authUser) {
|
||||
return;
|
||||
}
|
||||
@@ -83,7 +99,7 @@ export const getCurrentUser = query({
|
||||
});
|
||||
|
||||
export const getCurrentUserFunction = async (ctx: QueryCtx | MutationCtx) => {
|
||||
const authUser = await authComponent.safeGetAuthUser(ctx);
|
||||
const authUser = await authComponent.safeGetAuthUser(toGenericCtx(ctx));
|
||||
if (!authUser) {
|
||||
return;
|
||||
}
|
||||
@@ -102,7 +118,7 @@ export const createAuthUser = async (
|
||||
ctx: MutationCtx,
|
||||
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({
|
||||
headers,
|
||||
@@ -120,7 +136,7 @@ export const updatePassword = async (
|
||||
ctx: MutationCtx,
|
||||
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({
|
||||
headers,
|
||||
|
||||
Reference in New Issue
Block a user