1465 lines
32 KiB
JavaScript
1465 lines
32 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const PDFDocument = require('pdfkit');
|
|
|
|
const CORES = {
|
|
azul: '#175CD3',
|
|
azulEscuro: '#1849A9',
|
|
azulClaro: '#EFF8FF',
|
|
texto: '#344054',
|
|
textoForte: '#101828',
|
|
textoSuave: '#667085',
|
|
borda: '#D0D5DD',
|
|
bordaClara: '#EAECF0',
|
|
fundo: '#F8FAFC',
|
|
sucesso: '#027A48',
|
|
sucessoFundo: '#ECFDF3',
|
|
alerta: '#B54708',
|
|
alertaFundo: '#FFFAEB',
|
|
erro: '#B42318',
|
|
erroFundo: '#FEF3F2',
|
|
branco: '#FFFFFF'
|
|
};
|
|
|
|
const MARGEM = 42;
|
|
const TOPO_CONTEUDO = 102;
|
|
const RODAPE_Y = 806;
|
|
const LARGURA_A4 = 595.28;
|
|
const ALTURA_A4 = 841.89;
|
|
const LARGURA_CONTEUDO = LARGURA_A4 - (MARGEM * 2);
|
|
|
|
const STATUS_OS_LABEL = {
|
|
RASCUNHO: 'Rascunho',
|
|
ABERTA: 'Aberta',
|
|
EM_DIAGNOSTICO: 'Em diagnóstico',
|
|
AGUARDANDO_APROVACAO: 'Aguardando aprovação',
|
|
AGUARDANDO_PECA: 'Aguardando peça',
|
|
EM_EXECUCAO: 'Em execução',
|
|
EM_TESTES: 'Em testes',
|
|
FINALIZADA: 'Finalizada',
|
|
AGUARDANDO_RETIRADA: 'Aguardando retirada',
|
|
RETIRADA: 'Retirada',
|
|
CANCELADA: 'Cancelada'
|
|
};
|
|
|
|
const STATUS_PAGAMENTO_LABEL = {
|
|
NAO_APLICAVEL: 'Não aplicável',
|
|
PENDENTE: 'Pendente',
|
|
PARCIAL: 'Parcial',
|
|
RECEBIDO: 'Recebido',
|
|
CORTESIA: 'Cortesia'
|
|
};
|
|
|
|
const TIPO_OS_LABEL = {
|
|
CORRETIVA: 'Corretiva',
|
|
PREVENTIVA: 'Preventiva',
|
|
REFORMA: 'Reforma',
|
|
DESENVOLVIMENTO: 'Desenvolvimento',
|
|
FABRICACAO: 'Fabricação',
|
|
GARANTIA: 'Garantia',
|
|
CORTESIA: 'Cortesia',
|
|
INTERNA: 'Interna',
|
|
OUTRO: 'Outro'
|
|
};
|
|
|
|
const TIPO_ODOMETRO_LABEL = {
|
|
CADASTRO_INICIAL: 'Cadastro inicial',
|
|
ENTRADA_MANUTENCAO: 'Entrada na manutenção',
|
|
SAIDA_MANUTENCAO: 'Saída da manutenção',
|
|
ATUALIZACAO_MANUAL: 'Atualização manual',
|
|
TELEMETRIA: 'Telemetria'
|
|
};
|
|
|
|
function empresaConfig() {
|
|
return {
|
|
nome: process.env.ORION_PDF_EMPRESA_NOME || 'Zendion INC',
|
|
documento: process.env.ORION_PDF_EMPRESA_DOCUMENTO || '',
|
|
endereco: process.env.ORION_PDF_EMPRESA_ENDERECO || 'Rua Custódio Pereira, 690, Leme, SP',
|
|
contato: process.env.ORION_PDF_EMPRESA_CONTATO || '+55 (19) 99871-7560',
|
|
site: process.env.ORION_PDF_EMPRESA_SITE || 'https://zendioninc.com.br',
|
|
logo: process.env.ORION_PDF_LOGO || ''
|
|
};
|
|
}
|
|
|
|
function texto(valor, fallback = '-') {
|
|
if (valor === null || valor === undefined || valor === '') {
|
|
return fallback;
|
|
}
|
|
|
|
return String(valor);
|
|
}
|
|
|
|
function numero(valor) {
|
|
const convertido = Number(valor);
|
|
return Number.isFinite(convertido) ? convertido : 0;
|
|
}
|
|
|
|
function moeda(valor) {
|
|
return numero(valor).toLocaleString('pt-BR', {
|
|
style: 'currency',
|
|
currency: 'BRL'
|
|
});
|
|
}
|
|
|
|
function decimal(valor, casas = 2) {
|
|
return numero(valor).toLocaleString('pt-BR', {
|
|
minimumFractionDigits: casas,
|
|
maximumFractionDigits: casas
|
|
});
|
|
}
|
|
|
|
function dataBR(valor) {
|
|
if (!valor) {
|
|
return '-';
|
|
}
|
|
|
|
const data = valor instanceof Date ? valor : new Date(valor);
|
|
|
|
if (Number.isNaN(data.getTime())) {
|
|
return texto(valor);
|
|
}
|
|
|
|
return data.toLocaleDateString('pt-BR');
|
|
}
|
|
|
|
function dataHoraBR(valor) {
|
|
if (!valor) {
|
|
return '-';
|
|
}
|
|
|
|
const data = valor instanceof Date ? valor : new Date(valor);
|
|
|
|
if (Number.isNaN(data.getTime())) {
|
|
return texto(valor);
|
|
}
|
|
|
|
return data.toLocaleString('pt-BR', {
|
|
dateStyle: 'short',
|
|
timeStyle: 'short'
|
|
});
|
|
}
|
|
|
|
function segundosParaTempo(valor) {
|
|
let total = Math.max(0, Math.floor(numero(valor)));
|
|
const horas = Math.floor(total / 3600);
|
|
total %= 3600;
|
|
const minutos = Math.floor(total / 60);
|
|
const segundos = total % 60;
|
|
|
|
return [
|
|
String(horas).padStart(2, '0'),
|
|
String(minutos).padStart(2, '0'),
|
|
String(segundos).padStart(2, '0')
|
|
].join(':');
|
|
}
|
|
|
|
function nomeArquivoSeguro(valor) {
|
|
return texto(valor, 'documento')
|
|
.normalize('NFD')
|
|
.replace(/[\u0300-\u036f]/g, '')
|
|
.replace(/[^a-zA-Z0-9._-]+/g, '_')
|
|
.replace(/_+/g, '_')
|
|
.replace(/^_|_$/g, '');
|
|
}
|
|
|
|
function statusCor(status) {
|
|
if (status === 'RECEBIDO' || status === 'RETIRADA' || status === 'FINALIZADA') {
|
|
return {
|
|
fundo: CORES.sucessoFundo,
|
|
texto: CORES.sucesso
|
|
};
|
|
}
|
|
|
|
if (
|
|
status === 'PENDENTE' ||
|
|
status === 'CANCELADA'
|
|
) {
|
|
return {
|
|
fundo: CORES.erroFundo,
|
|
texto: CORES.erro
|
|
};
|
|
}
|
|
|
|
if (
|
|
status === 'PARCIAL' ||
|
|
status === 'AGUARDANDO_PECA' ||
|
|
status === 'AGUARDANDO_APROVACAO' ||
|
|
status === 'AGUARDANDO_RETIRADA'
|
|
) {
|
|
return {
|
|
fundo: CORES.alertaFundo,
|
|
texto: CORES.alerta
|
|
};
|
|
}
|
|
|
|
return {
|
|
fundo: CORES.azulClaro,
|
|
texto: CORES.azulEscuro
|
|
};
|
|
}
|
|
|
|
function configurarRespostaPdf(res, nomeArquivo) {
|
|
res.status(200);
|
|
res.setHeader('Content-Type', 'application/pdf');
|
|
res.setHeader(
|
|
'Content-Disposition',
|
|
`attachment; filename="${nomeArquivoSeguro(nomeArquivo)}"`
|
|
);
|
|
res.setHeader('Cache-Control', 'no-store');
|
|
}
|
|
|
|
function criarDocumento(res, nomeArquivo, titulo, subtitulo) {
|
|
configurarRespostaPdf(res, nomeArquivo);
|
|
|
|
const doc = new PDFDocument({
|
|
size: 'A4',
|
|
margins: {
|
|
top: TOPO_CONTEUDO,
|
|
right: MARGEM,
|
|
bottom: 58,
|
|
left: MARGEM
|
|
},
|
|
bufferPages: true,
|
|
info: {
|
|
Title: titulo,
|
|
Subject: subtitulo,
|
|
Author: empresaConfig().nome,
|
|
Creator: 'Oriontard'
|
|
}
|
|
});
|
|
|
|
doc.pipe(res);
|
|
|
|
const contexto = {
|
|
titulo,
|
|
subtitulo,
|
|
empresa: empresaConfig()
|
|
};
|
|
|
|
doc.on('pageAdded', () => {
|
|
desenharCabecalho(doc, contexto);
|
|
});
|
|
|
|
desenharCabecalho(doc, contexto);
|
|
|
|
return doc;
|
|
}
|
|
|
|
function desenharCabecalho(doc, contexto) {
|
|
const empresa = contexto.empresa;
|
|
const y = 28;
|
|
|
|
doc.save();
|
|
|
|
if (empresa.logo && fs.existsSync(empresa.logo)) {
|
|
try {
|
|
doc.image(empresa.logo, MARGEM, y, {
|
|
fit: [52, 40],
|
|
align: 'left',
|
|
valign: 'center'
|
|
});
|
|
} catch (error) {
|
|
desenharMarcaPadrao(doc, y);
|
|
}
|
|
} else {
|
|
desenharMarcaPadrao(doc, y);
|
|
}
|
|
|
|
doc
|
|
.font('Helvetica-Bold')
|
|
.fontSize(14)
|
|
.fillColor(CORES.textoForte)
|
|
.text(empresa.nome, MARGEM + 64, y + 1, {
|
|
width: 220
|
|
});
|
|
|
|
const linhaEmpresa = [
|
|
empresa.documento,
|
|
empresa.contato,
|
|
empresa.site
|
|
].filter(Boolean).join(' · ');
|
|
|
|
if (linhaEmpresa) {
|
|
doc
|
|
.font('Helvetica')
|
|
.fontSize(7.5)
|
|
.fillColor(CORES.textoSuave)
|
|
.text(linhaEmpresa, MARGEM + 64, y + 20, {
|
|
width: 250
|
|
});
|
|
}
|
|
|
|
doc
|
|
.font('Helvetica-Bold')
|
|
.fontSize(11)
|
|
.fillColor(CORES.azulEscuro)
|
|
.text(contexto.titulo, 320, y + 1, {
|
|
width: LARGURA_A4 - MARGEM - 320,
|
|
align: 'right'
|
|
});
|
|
|
|
doc
|
|
.font('Helvetica')
|
|
.fontSize(7.5)
|
|
.fillColor(CORES.textoSuave)
|
|
.text(contexto.subtitulo, 310, y + 20, {
|
|
width: LARGURA_A4 - MARGEM - 310,
|
|
align: 'right'
|
|
});
|
|
|
|
doc
|
|
.moveTo(MARGEM, 78)
|
|
.lineTo(LARGURA_A4 - MARGEM, 78)
|
|
.lineWidth(1)
|
|
.strokeColor(CORES.bordaClara)
|
|
.stroke();
|
|
|
|
doc.restore();
|
|
doc.x = MARGEM;
|
|
doc.y = TOPO_CONTEUDO;
|
|
}
|
|
|
|
function desenharMarcaPadrao(doc, y) {
|
|
doc
|
|
.roundedRect(MARGEM, y, 48, 40, 8)
|
|
.fill(CORES.azulEscuro);
|
|
|
|
doc
|
|
.font('Helvetica-Bold')
|
|
.fontSize(19)
|
|
.fillColor(CORES.branco)
|
|
.text('Z', MARGEM, y + 9, {
|
|
width: 48,
|
|
align: 'center'
|
|
});
|
|
}
|
|
|
|
function garantirEspaco(doc, alturaNecessaria) {
|
|
if (doc.y + alturaNecessaria > RODAPE_Y - 18) {
|
|
doc.addPage();
|
|
}
|
|
}
|
|
|
|
function tituloSecao(doc, titulo, subtitulo = '') {
|
|
garantirEspaco(doc, subtitulo ? 50 : 34);
|
|
|
|
doc
|
|
.font('Helvetica-Bold')
|
|
.fontSize(11)
|
|
.fillColor(CORES.textoForte)
|
|
.text(titulo, MARGEM, doc.y, {
|
|
width: LARGURA_CONTEUDO
|
|
});
|
|
|
|
doc.y += 4;
|
|
|
|
doc
|
|
.moveTo(MARGEM, doc.y)
|
|
.lineTo(MARGEM + LARGURA_CONTEUDO, doc.y)
|
|
.lineWidth(1.4)
|
|
.strokeColor(CORES.azul)
|
|
.stroke();
|
|
|
|
doc.y += 8;
|
|
|
|
if (subtitulo) {
|
|
doc
|
|
.font('Helvetica')
|
|
.fontSize(8)
|
|
.fillColor(CORES.textoSuave)
|
|
.text(subtitulo, {
|
|
width: LARGURA_CONTEUDO
|
|
});
|
|
|
|
doc.y += 7;
|
|
}
|
|
}
|
|
|
|
function badge(doc, x, y, label, status, largura = 112) {
|
|
const cor = statusCor(status);
|
|
const cursorX = doc.x;
|
|
const cursorY = doc.y;
|
|
|
|
doc
|
|
.roundedRect(x, y, largura, 22, 11)
|
|
.fill(cor.fundo);
|
|
|
|
doc
|
|
.font('Helvetica-Bold')
|
|
.fontSize(7.5)
|
|
.fillColor(cor.texto)
|
|
.text(label, x + 8, y + 7, {
|
|
width: largura - 16,
|
|
height: 10,
|
|
align: 'center',
|
|
ellipsis: true
|
|
});
|
|
|
|
// Desenhos absolutos não podem alterar o fluxo vertical do documento.
|
|
doc.x = cursorX;
|
|
doc.y = cursorY;
|
|
}
|
|
|
|
function cardInfo(doc, x, y, largura, rotulo, valor, opcoes = {}) {
|
|
const altura = opcoes.altura || 48;
|
|
const cursorX = doc.x;
|
|
const cursorY = doc.y;
|
|
|
|
doc
|
|
.roundedRect(x, y, largura, altura, 7)
|
|
.lineWidth(0.8)
|
|
.fillAndStroke(CORES.fundo, CORES.bordaClara);
|
|
|
|
doc
|
|
.font('Helvetica-Bold')
|
|
.fontSize(6.5)
|
|
.fillColor(CORES.textoSuave)
|
|
.text(rotulo.toUpperCase(), x + 10, y + 9, {
|
|
width: largura - 20,
|
|
height: 9,
|
|
ellipsis: true
|
|
});
|
|
|
|
doc
|
|
.font(opcoes.negrito === false ? 'Helvetica' : 'Helvetica-Bold')
|
|
.fontSize(opcoes.fonte || 9.2)
|
|
.fillColor(CORES.textoForte)
|
|
.text(texto(valor), x + 10, y + 23, {
|
|
width: largura - 20,
|
|
height: altura - 28,
|
|
ellipsis: true
|
|
});
|
|
|
|
// doc.text() move doc.y mesmo quando usamos coordenadas absolutas.
|
|
// Restaurar o cursor mantém todos os cards da mesma linha alinhados.
|
|
doc.x = cursorX;
|
|
doc.y = cursorY;
|
|
}
|
|
|
|
function blocoTexto(doc, rotulo, conteudo) {
|
|
const valor = texto(conteudo, '');
|
|
if (!valor) {
|
|
return;
|
|
}
|
|
|
|
const alturaTexto = doc
|
|
.font('Helvetica')
|
|
.fontSize(8.5)
|
|
.heightOfString(valor, {
|
|
width: LARGURA_CONTEUDO - 22,
|
|
lineGap: 2
|
|
});
|
|
|
|
const altura = Math.max(52, alturaTexto + 31);
|
|
garantirEspaco(doc, altura + 8);
|
|
|
|
const y = doc.y;
|
|
|
|
doc
|
|
.roundedRect(MARGEM, y, LARGURA_CONTEUDO, altura, 7)
|
|
.lineWidth(0.8)
|
|
.fillAndStroke(CORES.fundo, CORES.bordaClara);
|
|
|
|
doc
|
|
.font('Helvetica-Bold')
|
|
.fontSize(6.5)
|
|
.fillColor(CORES.textoSuave)
|
|
.text(rotulo.toUpperCase(), MARGEM + 11, y + 9, {
|
|
width: LARGURA_CONTEUDO - 22
|
|
});
|
|
|
|
doc
|
|
.font('Helvetica')
|
|
.fontSize(8.5)
|
|
.fillColor(CORES.texto)
|
|
.text(valor, MARGEM + 11, y + 23, {
|
|
width: LARGURA_CONTEUDO - 22,
|
|
lineGap: 2
|
|
});
|
|
|
|
doc.y = y + altura + 8;
|
|
}
|
|
|
|
function nomesTecnicos(atividade) {
|
|
const nomes = (atividade.tecnicos || [])
|
|
.map(item => item.tecnico_nome)
|
|
.filter(Boolean);
|
|
|
|
return nomes.length ? nomes.join(', ') : '-';
|
|
}
|
|
|
|
function desenharMateriaisAtividade(doc, materiais, interno) {
|
|
if (!materiais || !materiais.length) {
|
|
return;
|
|
}
|
|
|
|
garantirEspaco(doc, 46);
|
|
|
|
const x = MARGEM + 12;
|
|
const largura = LARGURA_CONTEUDO - 24;
|
|
const colunas = interno
|
|
? [230, 48, 55, 78, 78]
|
|
: [270, 55, 70, 90];
|
|
|
|
const cabecalhos = interno
|
|
? ['Material / insumo', 'Qtd.', 'Un.', 'Custo', 'Cobrado']
|
|
: ['Material / insumo', 'Qtd.', 'Un.', 'Valor'];
|
|
|
|
function cabecalhoTabela() {
|
|
garantirEspaco(doc, 30);
|
|
|
|
const y = doc.y;
|
|
doc
|
|
.rect(x, y, largura, 22)
|
|
.fill(CORES.azulClaro);
|
|
|
|
let cx = x;
|
|
|
|
cabecalhos.forEach((cabecalho, indice) => {
|
|
doc
|
|
.font('Helvetica-Bold')
|
|
.fontSize(6.5)
|
|
.fillColor(CORES.azulEscuro)
|
|
.text(cabecalho, cx + 5, y + 7, {
|
|
width: colunas[indice] - 10,
|
|
align: indice === 0 ? 'left' : 'right'
|
|
});
|
|
|
|
cx += colunas[indice];
|
|
});
|
|
|
|
doc.y = y + 22;
|
|
}
|
|
|
|
cabecalhoTabela();
|
|
|
|
materiais.forEach((material, indice) => {
|
|
garantirEspaco(doc, 28);
|
|
|
|
if (doc.y < TOPO_CONTEUDO + 5) {
|
|
cabecalhoTabela();
|
|
}
|
|
|
|
const y = doc.y;
|
|
const alturaDescricao = doc
|
|
.font('Helvetica')
|
|
.fontSize(7)
|
|
.heightOfString(texto(material.descricao_snapshot), {
|
|
width: colunas[0] - 10
|
|
});
|
|
|
|
const altura = Math.max(24, alturaDescricao + 12);
|
|
|
|
if (doc.y + altura > RODAPE_Y - 18) {
|
|
doc.addPage();
|
|
cabecalhoTabela();
|
|
}
|
|
|
|
if (indice % 2 === 1) {
|
|
doc
|
|
.rect(x, doc.y, largura, altura)
|
|
.fill(CORES.fundo);
|
|
}
|
|
|
|
let cx = x;
|
|
const valores = interno
|
|
? [
|
|
texto(material.descricao_snapshot),
|
|
decimal(material.quantidade, 3),
|
|
texto(material.unidade_snapshot),
|
|
moeda(material.valor_custo_total),
|
|
material.cobravel ? moeda(material.valor_cobrado_total) : 'Não cobrado'
|
|
]
|
|
: [
|
|
texto(material.descricao_snapshot),
|
|
decimal(material.quantidade, 3),
|
|
texto(material.unidade_snapshot),
|
|
material.cobravel ? moeda(material.valor_cobrado_total) : 'Não cobrado'
|
|
];
|
|
|
|
valores.forEach((valor, coluna) => {
|
|
doc
|
|
.font(coluna === 0 ? 'Helvetica' : 'Helvetica')
|
|
.fontSize(7)
|
|
.fillColor(CORES.texto)
|
|
.text(valor, cx + 5, doc.y + 6, {
|
|
width: colunas[coluna] - 10,
|
|
height: altura - 8,
|
|
align: coluna === 0 ? 'left' : 'right',
|
|
ellipsis: true
|
|
});
|
|
|
|
cx += colunas[coluna];
|
|
});
|
|
|
|
doc
|
|
.moveTo(x, doc.y + altura)
|
|
.lineTo(x + largura, doc.y + altura)
|
|
.lineWidth(0.5)
|
|
.strokeColor(CORES.bordaClara)
|
|
.stroke();
|
|
|
|
doc.y += altura;
|
|
});
|
|
|
|
doc.y += 6;
|
|
}
|
|
|
|
function desenharAtividade(doc, atividade, indice, interno) {
|
|
garantirEspaco(doc, 108);
|
|
|
|
const yInicial = doc.y;
|
|
|
|
doc
|
|
.roundedRect(MARGEM, yInicial, LARGURA_CONTEUDO, 30, 7)
|
|
.fill(CORES.azulClaro);
|
|
|
|
doc
|
|
.font('Helvetica-Bold')
|
|
.fontSize(8.5)
|
|
.fillColor(CORES.azulEscuro)
|
|
.text(
|
|
`${String(indice + 1).padStart(2, '0')} · ${dataBR(atividade.data_atividade)}`,
|
|
MARGEM + 11,
|
|
yInicial + 10,
|
|
{ width: 135 }
|
|
);
|
|
|
|
const horasTotais =
|
|
numero(atividade.horas_expediente) +
|
|
numero(atividade.horas_extras);
|
|
|
|
doc
|
|
.font('Helvetica')
|
|
.fontSize(7.2)
|
|
.fillColor(CORES.textoSuave)
|
|
.text(
|
|
`Técnicos: ${nomesTecnicos(atividade)}`,
|
|
MARGEM + 145,
|
|
yInicial + 10,
|
|
{ width: 235, ellipsis: true }
|
|
);
|
|
|
|
doc
|
|
.font('Helvetica-Bold')
|
|
.fontSize(7.2)
|
|
.fillColor(CORES.azulEscuro)
|
|
.text(
|
|
`${decimal(horasTotais)} h`,
|
|
MARGEM + 395,
|
|
yInicial + 10,
|
|
{ width: 62, align: 'right' }
|
|
);
|
|
|
|
doc
|
|
.font('Helvetica-Bold')
|
|
.fontSize(7.2)
|
|
.fillColor(CORES.azulEscuro)
|
|
.text(
|
|
atividade.cobravel ? moeda(atividade.valor_mao_obra) : 'Não cobrada',
|
|
MARGEM + 462,
|
|
yInicial + 10,
|
|
{ width: 38, align: 'right' }
|
|
);
|
|
|
|
doc.y = yInicial + 38;
|
|
|
|
const descricao = texto(atividade.descricao, '');
|
|
doc
|
|
.font('Helvetica')
|
|
.fontSize(8.5)
|
|
.fillColor(CORES.texto)
|
|
.text(descricao, MARGEM + 8, doc.y, {
|
|
width: LARGURA_CONTEUDO - 16,
|
|
lineGap: 2
|
|
});
|
|
|
|
doc.y += 7;
|
|
|
|
const observacao = interno
|
|
? atividade.observacoes_internas
|
|
: atividade.observacoes_cliente;
|
|
|
|
if (observacao) {
|
|
doc
|
|
.font('Helvetica-Oblique')
|
|
.fontSize(7.5)
|
|
.fillColor(CORES.textoSuave)
|
|
.text(`Observação: ${observacao}`, MARGEM + 8, doc.y, {
|
|
width: LARGURA_CONTEUDO - 16
|
|
});
|
|
|
|
doc.y += 7;
|
|
}
|
|
|
|
desenharMateriaisAtividade(doc, atividade.materiais || [], interno);
|
|
|
|
doc
|
|
.moveTo(MARGEM, doc.y)
|
|
.lineTo(MARGEM + LARGURA_CONTEUDO, doc.y)
|
|
.lineWidth(0.7)
|
|
.strokeColor(CORES.bordaClara)
|
|
.stroke();
|
|
|
|
doc.y += 12;
|
|
}
|
|
|
|
function desenharResumoFinanceiro(doc, ordem, interno) {
|
|
// Mantém o título e os primeiros cards juntos na mesma página.
|
|
garantirEspaco(doc, interno ? 178 : 154);
|
|
|
|
tituloSecao(
|
|
doc,
|
|
interno ? 'Resumo financeiro e custos' : 'Resumo financeiro',
|
|
interno
|
|
? 'Valores cobrados do cliente e custos internos registrados.'
|
|
: 'Valores consolidados desta ordem de serviço.'
|
|
);
|
|
|
|
const y = doc.y;
|
|
const largura = interno ? 120 : 150;
|
|
const gap = 10;
|
|
const itens = interno
|
|
? [
|
|
['Mão de obra', moeda(ordem.valor_mao_obra)],
|
|
['Insumos cobrados', moeda(ordem.valor_insumos)],
|
|
['Custo de insumos', moeda(ordem.custo_insumos)],
|
|
['Desconto', moeda(ordem.valor_desconto)]
|
|
]
|
|
: [
|
|
['Mão de obra', moeda(ordem.valor_mao_obra)],
|
|
['Materiais e insumos', moeda(ordem.valor_insumos)],
|
|
['Desconto', moeda(ordem.valor_desconto)]
|
|
];
|
|
|
|
itens.forEach((item, indice) => {
|
|
cardInfo(
|
|
doc,
|
|
MARGEM + ((largura + gap) * indice),
|
|
y,
|
|
largura,
|
|
item[0],
|
|
item[1],
|
|
{ altura: 48, fonte: 10 }
|
|
);
|
|
});
|
|
|
|
doc.y = y + 59;
|
|
|
|
const totalLargura = 160;
|
|
cardInfo(
|
|
doc,
|
|
MARGEM,
|
|
doc.y,
|
|
totalLargura,
|
|
'Valor total',
|
|
moeda(ordem.valor_total),
|
|
{ altura: 51, fonte: 12 }
|
|
);
|
|
|
|
cardInfo(
|
|
doc,
|
|
MARGEM + totalLargura + gap,
|
|
doc.y,
|
|
totalLargura,
|
|
'Valor pago',
|
|
moeda(ordem.valor_pago),
|
|
{ altura: 51, fonte: 12 }
|
|
);
|
|
|
|
cardInfo(
|
|
doc,
|
|
MARGEM + ((totalLargura + gap) * 2),
|
|
doc.y,
|
|
totalLargura,
|
|
'Valor pendente',
|
|
moeda(ordem.valor_pendente),
|
|
{ altura: 51, fonte: 12 }
|
|
);
|
|
|
|
doc.y += 62;
|
|
|
|
const labelPagamento =
|
|
STATUS_PAGAMENTO_LABEL[ordem.status_pagamento] ||
|
|
texto(ordem.status_pagamento);
|
|
|
|
badge(
|
|
doc,
|
|
MARGEM,
|
|
doc.y,
|
|
`Pagamento: ${labelPagamento}`,
|
|
ordem.status_pagamento,
|
|
155
|
|
);
|
|
|
|
if (ordem.tipo === 'GARANTIA') {
|
|
badge(doc, MARGEM + 165, doc.y, 'Serviço em garantia', 'PARCIAL', 135);
|
|
} else if (ordem.tipo === 'CORTESIA') {
|
|
badge(doc, MARGEM + 165, doc.y, 'Serviço em cortesia', 'PARCIAL', 135);
|
|
}
|
|
|
|
doc.y += 34;
|
|
}
|
|
|
|
function desenharOdometros(doc, odometros) {
|
|
if (!odometros || !odometros.length) {
|
|
return;
|
|
}
|
|
|
|
// Evita deixar o título da seção sozinho no fim da página.
|
|
garantirEspaco(doc, 118);
|
|
|
|
tituloSecao(
|
|
doc,
|
|
'Leituras de odômetro',
|
|
'Leituras vinculadas à entrada, aos testes ou à saída da manutenção.'
|
|
);
|
|
|
|
odometros.forEach(leitura => {
|
|
garantirEspaco(doc, 76);
|
|
|
|
const y = doc.y;
|
|
|
|
doc
|
|
.font('Helvetica-Bold')
|
|
.fontSize(8)
|
|
.fillColor(CORES.textoForte)
|
|
.text(
|
|
TIPO_ODOMETRO_LABEL[leitura.tipo_leitura] ||
|
|
texto(leitura.tipo_leitura),
|
|
MARGEM,
|
|
y,
|
|
{ width: 230 }
|
|
);
|
|
|
|
doc
|
|
.font('Helvetica')
|
|
.fontSize(7)
|
|
.fillColor(CORES.textoSuave)
|
|
.text(dataHoraBR(leitura.leitura_em), MARGEM + 250, y, {
|
|
width: 260,
|
|
align: 'right'
|
|
});
|
|
|
|
const valores = [
|
|
['Total', segundosParaTempo(leitura.odometro_total_segundos)],
|
|
['Conectado', segundosParaTempo(leitura.odometro_conectado_segundos)],
|
|
['Movimento', segundosParaTempo(leitura.odometro_movimento_segundos)],
|
|
['Parcial', segundosParaTempo(leitura.odometro_parcial_segundos)]
|
|
];
|
|
|
|
valores.forEach((item, indice) => {
|
|
cardInfo(
|
|
doc,
|
|
MARGEM + (indice * 126),
|
|
y + 18,
|
|
116,
|
|
item[0],
|
|
item[1],
|
|
{ altura: 44, fonte: 9 }
|
|
);
|
|
});
|
|
|
|
doc.y = y + 72;
|
|
});
|
|
}
|
|
|
|
function desenharAssinaturas(doc, ordem) {
|
|
garantirEspaco(doc, 112);
|
|
|
|
doc.y += 10;
|
|
|
|
const y = doc.y + 44;
|
|
const largura = 220;
|
|
|
|
doc
|
|
.moveTo(MARGEM, y)
|
|
.lineTo(MARGEM + largura, y)
|
|
.lineWidth(0.7)
|
|
.strokeColor(CORES.textoSuave)
|
|
.stroke();
|
|
|
|
doc
|
|
.moveTo(LARGURA_A4 - MARGEM - largura, y)
|
|
.lineTo(LARGURA_A4 - MARGEM, y)
|
|
.stroke();
|
|
|
|
doc
|
|
.font('Helvetica')
|
|
.fontSize(7)
|
|
.fillColor(CORES.textoSuave)
|
|
.text(
|
|
'Responsável técnico',
|
|
MARGEM,
|
|
y + 6,
|
|
{ width: largura, align: 'center' }
|
|
);
|
|
|
|
doc
|
|
.text(
|
|
'Cliente / responsável pela retirada',
|
|
LARGURA_A4 - MARGEM - largura,
|
|
y + 6,
|
|
{ width: largura, align: 'center' }
|
|
);
|
|
|
|
if (ordem.tecnico_responsavel_nome) {
|
|
doc
|
|
.font('Helvetica-Bold')
|
|
.fontSize(7.5)
|
|
.fillColor(CORES.texto)
|
|
.text(
|
|
ordem.tecnico_responsavel_nome,
|
|
MARGEM,
|
|
y + 19,
|
|
{ width: largura, align: 'center' }
|
|
);
|
|
}
|
|
|
|
if (ordem.responsavel_retirada) {
|
|
doc
|
|
.font('Helvetica-Bold')
|
|
.fontSize(7.5)
|
|
.fillColor(CORES.texto)
|
|
.text(
|
|
ordem.responsavel_retirada,
|
|
LARGURA_A4 - MARGEM - largura,
|
|
y + 19,
|
|
{ width: largura, align: 'center' }
|
|
);
|
|
}
|
|
|
|
doc.y = y + 42;
|
|
}
|
|
|
|
function adicionarRodapes(doc, identificador) {
|
|
const paginas = doc.bufferedPageRange();
|
|
const ultimaPaginaOriginal = paginas.start + paginas.count - 1;
|
|
|
|
for (let i = paginas.start; i <= ultimaPaginaOriginal; i += 1) {
|
|
doc.switchToPage(i);
|
|
|
|
const cursorX = doc.x;
|
|
const cursorY = doc.y;
|
|
const margemInferiorOriginal = doc.page.margins.bottom;
|
|
|
|
// O rodapé fica fisicamente dentro do A4, mas abaixo da margem de fluxo.
|
|
// Sem isto, o PDFKit cria uma página nova para cada doc.text() do rodapé.
|
|
doc.page.margins.bottom = 0;
|
|
|
|
doc
|
|
.moveTo(MARGEM, RODAPE_Y)
|
|
.lineTo(LARGURA_A4 - MARGEM, RODAPE_Y)
|
|
.lineWidth(0.6)
|
|
.strokeColor(CORES.bordaClara)
|
|
.stroke();
|
|
|
|
doc
|
|
.font('Helvetica')
|
|
.fontSize(6.5)
|
|
.fillColor(CORES.textoSuave)
|
|
.text(
|
|
`${identificador} · Gerado em ${dataHoraBR(new Date())}`,
|
|
MARGEM,
|
|
RODAPE_Y + 8,
|
|
{
|
|
width: 350,
|
|
height: 9,
|
|
lineBreak: false,
|
|
ellipsis: true
|
|
}
|
|
);
|
|
|
|
doc
|
|
.text(
|
|
`Página ${i - paginas.start + 1} de ${paginas.count}`,
|
|
420,
|
|
RODAPE_Y + 8,
|
|
{
|
|
width: LARGURA_A4 - MARGEM - 420,
|
|
height: 9,
|
|
align: 'right',
|
|
lineBreak: false
|
|
}
|
|
);
|
|
|
|
doc.page.margins.bottom = margemInferiorOriginal;
|
|
doc.x = cursorX;
|
|
doc.y = cursorY;
|
|
}
|
|
|
|
// Termina posicionado na última página real, sem criar páginas vazias.
|
|
doc.switchToPage(ultimaPaginaOriginal);
|
|
}
|
|
|
|
function gerarOrdemServicoPdf(res, ordem, opcoes = {}) {
|
|
const interno = opcoes.interno === true;
|
|
const equipamento = ordem.equipamento_nome_snapshot ||
|
|
ordem.modelo_snapshot ||
|
|
'Equipamento';
|
|
|
|
const nomeArquivo =
|
|
`OS_${ordem.id}_${equipamento}` +
|
|
(ordem.numero_serie_snapshot ? `_NS_${ordem.numero_serie_snapshot}` : '') +
|
|
(interno ? '_INTERNA' : '_CLIENTE') +
|
|
'.pdf';
|
|
|
|
const doc = criarDocumento(
|
|
res,
|
|
nomeArquivo,
|
|
`Ordem de Serviço #${ordem.id}`,
|
|
interno
|
|
? 'Relatório técnico interno de manutenção'
|
|
: 'Relatório técnico de manutenção'
|
|
);
|
|
|
|
const y = doc.y;
|
|
|
|
doc
|
|
.font('Helvetica-Bold')
|
|
.fontSize(18)
|
|
.fillColor(CORES.textoForte)
|
|
.text(`Ordem de Serviço #${ordem.id}`, MARGEM, y, {
|
|
width: 260
|
|
});
|
|
|
|
doc
|
|
.font('Helvetica')
|
|
.fontSize(8)
|
|
.fillColor(CORES.textoSuave)
|
|
.text(
|
|
`${TIPO_OS_LABEL[ordem.tipo] || texto(ordem.tipo)} · ` +
|
|
`${texto(ordem.prioridade, 'Normal')}`,
|
|
MARGEM,
|
|
y + 23,
|
|
{ width: 250 }
|
|
);
|
|
|
|
badge(
|
|
doc,
|
|
340,
|
|
y,
|
|
STATUS_OS_LABEL[ordem.status] || texto(ordem.status),
|
|
ordem.status,
|
|
150
|
|
);
|
|
|
|
doc.y = y + 46;
|
|
|
|
const cardW = (LARGURA_CONTEUDO - 20) / 3;
|
|
|
|
cardInfo(
|
|
doc,
|
|
MARGEM,
|
|
doc.y,
|
|
cardW,
|
|
'Equipamento',
|
|
equipamento,
|
|
{ altura: 53 }
|
|
);
|
|
|
|
cardInfo(
|
|
doc,
|
|
MARGEM + cardW + 10,
|
|
doc.y,
|
|
cardW,
|
|
'Número de série',
|
|
ordem.numero_serie_snapshot || '-',
|
|
{ altura: 53 }
|
|
);
|
|
|
|
cardInfo(
|
|
doc,
|
|
MARGEM + ((cardW + 10) * 2),
|
|
doc.y,
|
|
cardW,
|
|
'Cliente',
|
|
ordem.cliente_nome_snapshot || '-',
|
|
{ altura: 53 }
|
|
);
|
|
|
|
doc.y += 64;
|
|
|
|
cardInfo(
|
|
doc,
|
|
MARGEM,
|
|
doc.y,
|
|
cardW,
|
|
'Entrada',
|
|
dataHoraBR(ordem.data_entrada),
|
|
{ altura: 48 }
|
|
);
|
|
|
|
cardInfo(
|
|
doc,
|
|
MARGEM + cardW + 10,
|
|
doc.y,
|
|
cardW,
|
|
'Finalização',
|
|
dataHoraBR(ordem.data_finalizacao),
|
|
{ altura: 48 }
|
|
);
|
|
|
|
cardInfo(
|
|
doc,
|
|
MARGEM + ((cardW + 10) * 2),
|
|
doc.y,
|
|
cardW,
|
|
'Retirada',
|
|
dataHoraBR(ordem.data_retirada),
|
|
{ altura: 48 }
|
|
);
|
|
|
|
doc.y += 60;
|
|
|
|
blocoTexto(doc, 'Problema relatado', ordem.problema_relatado);
|
|
blocoTexto(doc, 'Diagnóstico técnico', ordem.diagnostico);
|
|
blocoTexto(doc, 'Solução executada', ordem.solucao_resumo);
|
|
|
|
if (!interno && ordem.observacoes_cliente) {
|
|
blocoTexto(doc, 'Observações ao cliente', ordem.observacoes_cliente);
|
|
}
|
|
|
|
if (interno && ordem.observacoes_internas) {
|
|
blocoTexto(doc, 'Observações internas', ordem.observacoes_internas);
|
|
}
|
|
|
|
tituloSecao(
|
|
doc,
|
|
'Atividades realizadas',
|
|
'Histórico cronológico das etapas executadas nesta manutenção.'
|
|
);
|
|
|
|
if (!ordem.atividades || !ordem.atividades.length) {
|
|
blocoTexto(doc, 'Atividades', 'Nenhuma atividade registrada.');
|
|
} else {
|
|
ordem.atividades.forEach((atividade, indice) => {
|
|
desenharAtividade(doc, atividade, indice, interno);
|
|
});
|
|
}
|
|
|
|
desenharOdometros(doc, ordem.odometros_da_os || []);
|
|
desenharResumoFinanceiro(doc, ordem, interno);
|
|
|
|
if (ordem.garantia_dias || ordem.garantia_ate) {
|
|
blocoTexto(
|
|
doc,
|
|
'Garantia do serviço',
|
|
[
|
|
ordem.garantia_dias
|
|
? `${ordem.garantia_dias} dia(s)`
|
|
: null,
|
|
ordem.garantia_ate
|
|
? `válida até ${dataBR(ordem.garantia_ate)}`
|
|
: null
|
|
].filter(Boolean).join(', ')
|
|
);
|
|
}
|
|
|
|
desenharAssinaturas(doc, ordem);
|
|
adicionarRodapes(doc, `OS #${ordem.id}`);
|
|
doc.end();
|
|
}
|
|
|
|
function cabecalhoTabelaRelatorio(doc, colunas, y) {
|
|
doc
|
|
.rect(MARGEM, y, LARGURA_CONTEUDO, 24)
|
|
.fill(CORES.azulEscuro);
|
|
|
|
let x = MARGEM;
|
|
|
|
colunas.forEach(coluna => {
|
|
doc
|
|
.font('Helvetica-Bold')
|
|
.fontSize(6.2)
|
|
.fillColor(CORES.branco)
|
|
.text(coluna.titulo, x + 4, y + 8, {
|
|
width: coluna.largura - 8,
|
|
align: coluna.alinhamento || 'left'
|
|
});
|
|
|
|
x += coluna.largura;
|
|
});
|
|
|
|
doc.y = y + 24;
|
|
}
|
|
|
|
function linhaTabelaRelatorio(doc, colunas, valores, indice) {
|
|
const alturas = valores.map((valor, coluna) => {
|
|
return doc
|
|
.font('Helvetica')
|
|
.fontSize(6.5)
|
|
.heightOfString(texto(valor), {
|
|
width: colunas[coluna].largura - 8
|
|
});
|
|
});
|
|
|
|
const altura = Math.max(27, Math.min(44, Math.max(...alturas) + 10));
|
|
|
|
if (doc.y + altura > RODAPE_Y - 18) {
|
|
doc.addPage();
|
|
cabecalhoTabelaRelatorio(doc, colunas, doc.y);
|
|
}
|
|
|
|
const y = doc.y;
|
|
|
|
if (indice % 2 === 1) {
|
|
doc
|
|
.rect(MARGEM, y, LARGURA_CONTEUDO, altura)
|
|
.fill(CORES.fundo);
|
|
}
|
|
|
|
let x = MARGEM;
|
|
|
|
valores.forEach((valor, coluna) => {
|
|
doc
|
|
.font('Helvetica')
|
|
.fontSize(6.5)
|
|
.fillColor(CORES.texto)
|
|
.text(texto(valor), x + 4, y + 6, {
|
|
width: colunas[coluna].largura - 8,
|
|
height: altura - 8,
|
|
ellipsis: true,
|
|
align: colunas[coluna].alinhamento || 'left'
|
|
});
|
|
|
|
x += colunas[coluna].largura;
|
|
});
|
|
|
|
doc
|
|
.moveTo(MARGEM, y + altura)
|
|
.lineTo(MARGEM + LARGURA_CONTEUDO, y + altura)
|
|
.lineWidth(0.45)
|
|
.strokeColor(CORES.bordaClara)
|
|
.stroke();
|
|
|
|
doc.y = y + altura;
|
|
}
|
|
|
|
function gerarRelatorioOrdensServicoPdf(res, ordens, filtros = {}) {
|
|
const inicio = filtros.dataInicio;
|
|
const fim = filtros.dataFim;
|
|
const periodo = `${dataBR(inicio)} a ${dataBR(fim)}`;
|
|
|
|
const nomeArquivo = `Relatorio_OS_${nomeArquivoSeguro(
|
|
dataBR(inicio)
|
|
)}_${nomeArquivoSeguro(dataBR(fim))}.pdf`;
|
|
|
|
const doc = criarDocumento(
|
|
res,
|
|
nomeArquivo,
|
|
'Relatório de Serviços e Cobranças',
|
|
`Período: ${periodo}`
|
|
);
|
|
|
|
const totalHoras = ordens.reduce(
|
|
(soma, ordem) =>
|
|
soma +
|
|
numero(ordem.total_horas_expediente) +
|
|
numero(ordem.total_horas_extras),
|
|
0
|
|
);
|
|
|
|
const totalServicos = ordens.reduce(
|
|
(soma, ordem) => soma + numero(ordem.valor_mao_obra),
|
|
0
|
|
);
|
|
|
|
const totalInsumos = ordens.reduce(
|
|
(soma, ordem) => soma + numero(ordem.valor_insumos),
|
|
0
|
|
);
|
|
|
|
const totalGeral = ordens.reduce(
|
|
(soma, ordem) => soma + numero(ordem.valor_total),
|
|
0
|
|
);
|
|
|
|
const totalPago = ordens.reduce(
|
|
(soma, ordem) => soma + numero(ordem.valor_pago),
|
|
0
|
|
);
|
|
|
|
const totalPendente = ordens.reduce(
|
|
(soma, ordem) => soma + numero(ordem.valor_pendente),
|
|
0
|
|
);
|
|
|
|
doc
|
|
.font('Helvetica-Bold')
|
|
.fontSize(18)
|
|
.fillColor(CORES.textoForte)
|
|
.text('Relatório de Serviços e Cobranças');
|
|
|
|
doc
|
|
.font('Helvetica')
|
|
.fontSize(8)
|
|
.fillColor(CORES.textoSuave)
|
|
.text(
|
|
`Período: ${periodo} · Critério: ${texto(filtros.criterioLabel)}`,
|
|
{ width: LARGURA_CONTEUDO }
|
|
);
|
|
|
|
doc.y += 16;
|
|
|
|
const resumo = [
|
|
['Ordens', ordens.length],
|
|
['Horas', `${decimal(totalHoras)} h`],
|
|
['Mão de obra', moeda(totalServicos)],
|
|
['Insumos', moeda(totalInsumos)]
|
|
];
|
|
|
|
const cardW = (LARGURA_CONTEUDO - 30) / 4;
|
|
|
|
resumo.forEach((item, indice) => {
|
|
cardInfo(
|
|
doc,
|
|
MARGEM + ((cardW + 10) * indice),
|
|
doc.y,
|
|
cardW,
|
|
item[0],
|
|
item[1],
|
|
{ altura: 50, fonte: 10 }
|
|
);
|
|
});
|
|
|
|
doc.y += 62;
|
|
|
|
const resumoFinanceiro = [
|
|
['Total faturado', moeda(totalGeral)],
|
|
['Total recebido', moeda(totalPago)],
|
|
['Total pendente', moeda(totalPendente)]
|
|
];
|
|
|
|
const cardFinanceiroW = (LARGURA_CONTEUDO - 20) / 3;
|
|
|
|
resumoFinanceiro.forEach((item, indice) => {
|
|
cardInfo(
|
|
doc,
|
|
MARGEM + ((cardFinanceiroW + 10) * indice),
|
|
doc.y,
|
|
cardFinanceiroW,
|
|
item[0],
|
|
item[1],
|
|
{ altura: 53, fonte: 11.5 }
|
|
);
|
|
});
|
|
|
|
doc.y += 72;
|
|
|
|
tituloSecao(
|
|
doc,
|
|
'Ordens incluídas',
|
|
filtros.clienteNome
|
|
? `Cliente: ${filtros.clienteNome}`
|
|
: 'Relação consolidada das ordens que atendem aos filtros informados.'
|
|
);
|
|
|
|
if (!ordens.length) {
|
|
blocoTexto(doc, 'Resultado', 'Nenhuma ordem de serviço encontrada para o período.');
|
|
} else {
|
|
const colunas = [
|
|
{ titulo: 'OS', largura: 31, alinhamento: 'center' },
|
|
{ titulo: 'Equipamento', largura: 104 },
|
|
{ titulo: 'Finalização', largura: 58, alinhamento: 'center' },
|
|
{ titulo: 'Horas', largura: 45, alinhamento: 'right' },
|
|
{ titulo: 'Mão de obra', largura: 65, alinhamento: 'right' },
|
|
{ titulo: 'Insumos', largura: 58, alinhamento: 'right' },
|
|
{ titulo: 'Total', largura: 67, alinhamento: 'right' },
|
|
{ titulo: 'Pendente', largura: 83, alinhamento: 'right' }
|
|
];
|
|
|
|
cabecalhoTabelaRelatorio(doc, colunas, doc.y);
|
|
|
|
ordens.forEach((ordem, indice) => {
|
|
linhaTabelaRelatorio(
|
|
doc,
|
|
colunas,
|
|
[
|
|
`#${ordem.id}`,
|
|
`${texto(ordem.equipamento_nome_snapshot)}${
|
|
ordem.numero_serie_snapshot
|
|
? ` · NS ${ordem.numero_serie_snapshot}`
|
|
: ''
|
|
}`,
|
|
dataBR(
|
|
ordem.data_finalizacao ||
|
|
ordem.data_retirada ||
|
|
ordem.data_entrada
|
|
),
|
|
decimal(
|
|
numero(ordem.total_horas_expediente) +
|
|
numero(ordem.total_horas_extras)
|
|
),
|
|
moeda(ordem.valor_mao_obra),
|
|
moeda(ordem.valor_insumos),
|
|
moeda(ordem.valor_total),
|
|
moeda(ordem.valor_pendente)
|
|
],
|
|
indice
|
|
);
|
|
});
|
|
|
|
doc.y += 12;
|
|
}
|
|
|
|
tituloSecao(doc, 'Consolidação financeira');
|
|
|
|
garantirEspaco(doc, 98);
|
|
|
|
const linhas = [
|
|
['Mão de obra', totalServicos],
|
|
['Materiais e insumos', totalInsumos],
|
|
['Valor total das OS', totalGeral],
|
|
['Valor recebido', totalPago],
|
|
['Valor pendente', totalPendente]
|
|
];
|
|
|
|
linhas.forEach((linha, indice) => {
|
|
const y = doc.y;
|
|
|
|
if (indice % 2 === 0) {
|
|
doc
|
|
.rect(MARGEM, y, LARGURA_CONTEUDO, 24)
|
|
.fill(CORES.fundo);
|
|
}
|
|
|
|
doc
|
|
.font(indice >= 2 ? 'Helvetica-Bold' : 'Helvetica')
|
|
.fontSize(8)
|
|
.fillColor(CORES.texto)
|
|
.text(linha[0], MARGEM + 8, y + 8, {
|
|
width: 300
|
|
});
|
|
|
|
doc
|
|
.font('Helvetica-Bold')
|
|
.text(moeda(linha[1]), MARGEM + 320, y + 8, {
|
|
width: LARGURA_CONTEUDO - 328,
|
|
align: 'right'
|
|
});
|
|
|
|
doc.y = y + 24;
|
|
});
|
|
|
|
doc.y += 18;
|
|
|
|
blocoTexto(
|
|
doc,
|
|
'Observação',
|
|
'Este relatório consolida os valores registrados nas ordens de serviço. ' +
|
|
'Pagamentos cancelados não compõem o valor recebido.'
|
|
);
|
|
|
|
adicionarRodapes(doc, 'Relatório de serviços');
|
|
doc.end();
|
|
}
|
|
|
|
module.exports = {
|
|
gerarOrdemServicoPdf,
|
|
gerarRelatorioOrdensServicoPdf,
|
|
nomeArquivoSeguro
|
|
};
|