diff --git a/client/src/app/quotes/list.tsx b/client/src/app/quotes/list.tsx index 59e6d41..282e8c0 100644 --- a/client/src/app/quotes/list.tsx +++ b/client/src/app/quotes/list.tsx @@ -31,7 +31,9 @@ export const QuotesList = () => { - Active + + + @@ -50,8 +52,8 @@ export const QuotesList = () => { - - + + diff --git a/client/src/locales/es.json b/client/src/locales/es.json index 0342087..54677d0 100644 --- a/client/src/locales/es.json +++ b/client/src/locales/es.json @@ -81,7 +81,8 @@ "subtitle": "", "tabs": { "all": "Todas", - "draft": "Borrador", + "draft": "Borradores", + "emitted": "Emitidas", "archived": "Archivadas" }, "columns": { diff --git a/server/src/contexts/sales/infrastructure/express/controllers/quotes/reportQuote/reporter/ReportQuote.reporter.ts b/server/src/contexts/sales/infrastructure/express/controllers/quotes/reportQuote/reporter/ReportQuote.reporter.ts index 0334899..26597ed 100644 --- a/server/src/contexts/sales/infrastructure/express/controllers/quotes/reportQuote/reporter/ReportQuote.reporter.ts +++ b/server/src/contexts/sales/infrastructure/express/controllers/quotes/reportQuote/reporter/ReportQuote.reporter.ts @@ -1,24 +1,52 @@ -import * as handlebars from "handlebars"; -import * as puppeteer from "puppeteer"; -import { Quote } from "../../../../../../domain"; +import { ICollection } from "@shared/contexts"; +import { Quote, QuoteItem } from "../../../../../../domain"; import { ISalesContext } from "../../../../../Sales.context"; +import { readFileSync } from "fs"; +import * as handlebars from "handlebars"; +import path from "path"; +import * as puppeteer from "puppeteer"; + export interface IReportQuoteReporter { - toPDF: (quote: Quote, context: ISalesContext) => any; + toHTML: (quote: Quote, context: ISalesContext) => string; + toPDF: (quote: Quote, context: ISalesContext) => Promise; } export const ReportQuotePresenter: IReportQuoteReporter = { - toPDF: async (quote: Quote, context: ISalesContext): Promise => { + toHTML: (quote: Quote, context: ISalesContext): string => { + const quote_dto = map(quote, context); + // Obtener y compilar la plantilla HTML const templateHtml = obtenerPlantillaHTML(); - const template = handlebars.compile(templateHtml); - const html = template(quote); + const template = handlebars.compile(templateHtml, {}); + const html = template(quote_dto); + + return html; + }, + + toPDF: async (quote: Quote, context: ISalesContext): Promise => { + const html = ReportQuotePresenter.toHTML(quote, context); // Generar el PDF con Puppeteer const browser = await puppeteer.launch(); const page = await browser.newPage(); - await page.setContent(html); - const pdfBuffer = await page.pdf({ format: "A4" }); + + await page.setContent(html, { waitUntil: "networkidle2" }); + const pdfBuffer = await page.pdf({ + //path: "quote.pdf", + format: "A4", + printBackground: false, + displayHeaderFooter: false, + //headerTemplate: `
Quote #${quote_dto.id}
`, + //footerTemplate: `
Page of
`, + margin: { + top: "0mm", + bottom: "0mm", + left: "0mm", + right: "0mm", + }, + }); + await browser.close(); return pdfBuffer; @@ -26,15 +54,64 @@ export const ReportQuotePresenter: IReportQuoteReporter = { }; const obtenerPlantillaHTML = (): string => { - // Implementar la lógica para obtener la plantilla HTML - return ` - - - Factura - - -

Factura:

-

Cliente:

- - `; + return readFileSync(path.join(__dirname, "./templates/quote.hbs")).toString(); }; + +const map = (quote: Quote, context: ISalesContext) => { + const { dealer } = context; + + return { + id: quote.id.toString(), + status: quote.status.toString(), + date: quote.date.toISO8601(), + reference: quote.reference.toString(), + customer_information: quote.customer.toString(), + lang_code: quote.language.toString(), + currency_code: quote.currency.toString(), + + payment_method: quote.paymentMethod.toString(), + validity: quote.validity.toString(), + notes: quote.notes.toString(), + + subtotal_price: quote.subtotalPrice.toFormat(), + + discount: quote.discount.toFormat(), + discount_price: quote.discountPrice.toFormat(), + + before_tax_price: quote.beforeTaxPrice.toFormat(), + + tax: quote.tax.toFormat(), + tax_price: quote.taxPrice.toFormat(), + + total_price: quote.totalPrice.toFormat(), + + items: quoteItemPresenter(quote.items, context), + + dealer: { + id: dealer?.id.toString(), + name: dealer?.name.toString(), + currency_code: dealer?.currency.code, + lang_code: dealer?.language.code, + contact_information: dealer?.additionalInfo.get("contact_information"), + default_payment_method: dealer?.additionalInfo.get("contact_information"), + default_notes: dealer?.additionalInfo.get("contact_information"), + default_legal_terms: dealer?.additionalInfo.get("contact_information"), + default_quote_validity: dealer?.additionalInfo.get("contact_information"), + }, + }; +}; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const quoteItemPresenter = (items: ICollection, context: ISalesContext): any[] => + items.totalCount > 0 + ? items.items.map((item: QuoteItem) => ({ + article_id: item.articleId.toString(), + description: item.description.toString(), + quantity: item.quantity.toFormat(), + unit_price: item.unitPrice.toFormat(), + subtotal_price: item.subtotalPrice.toFormat(), + discount: item.discount.toFormat(), + total_price: item.totalPrice.toFormat(), + })) + : []; +2; diff --git a/server/src/contexts/sales/infrastructure/express/controllers/quotes/reportQuote/reporter/templates/quote.hbs b/server/src/contexts/sales/infrastructure/express/controllers/quotes/reportQuote/reporter/templates/quote.hbs new file mode 100644 index 0000000..ff31b0d --- /dev/null +++ b/server/src/contexts/sales/infrastructure/express/controllers/quotes/reportQuote/reporter/templates/quote.hbs @@ -0,0 +1,129 @@ + + + + + + Presupuesto #{{id}} + + + + +
+
+
+

DISTRIBUIDOR OFICIAL

+
+
+ Logo distribuidor +
+
+ {{dealer.contact_information}} +
+
+
+
+ Uecko Logo +

Essential Furniture

+

PREMIO AMBARRO DEL AÑO 2021

+

LUXURY SPAIN

+

ELLE 2021

+
+
+ +
+
+

Presupuesto nº: {{id}}

+

Fecha: {{date}}

+

Validez: {{validity}}

+

Vendedor: {{dealer.name}}

+

Referencia cliente: {{reference}}

+
+
+

{{customer_information}}

+
+
+ +
+

Cotización

+ + + + + + + + + + + + + + {{#each items}} + + + + + + + + + {{/each}} + +
Cant.DescripciónPrec. UnitarioSubtotalDto (%)Importe total
{{quantity}}{{description}}{{unit_price}}{{subtotal_price}}{{discount}}{{total_price}}
+
+ +
+
+
+

Forma de pago: {{payment_method}}

+
+
+

Notas: {{notes}}

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Importe neto{{subtotal_price}}
% Descuento{{discount.amount}}{{discount_price}}
Base imponible{{before_tax_price}}
% IVA{{tax}}{{tax_price}}
Importe total{{total_price}}
+ +
+
+ +
+

Información básica sobre protección de datos

+

{{quote.default_legal_terms}}

+
+
+ + + \ No newline at end of file diff --git a/server/src/infrastructure/express/app.ts b/server/src/infrastructure/express/app.ts index 79f3e16..6271759 100644 --- a/server/src/infrastructure/express/app.ts +++ b/server/src/infrastructure/express/app.ts @@ -6,6 +6,7 @@ import responseTime from "response-time"; import { configurePassportAuth } from "@/contexts/auth"; import morgan from "morgan"; import passport from "passport"; +import path from "path"; import { v1Routes } from "./api/v1"; //const logger = initLogger(rTracer); @@ -56,6 +57,10 @@ configurePassportAuth(passport); // Express configuration app.set("port", process.env.PORT ?? 3000); +// Public assets +app.use("/assets", express.static(path.join(__dirname, "/public"))); + +// API app.use("/api/v1", v1Routes()); export { app }; diff --git a/shared/lib/contexts/common/domain/entities/MoneyValue.ts b/shared/lib/contexts/common/domain/entities/MoneyValue.ts index 87e9c2c..02f0c61 100644 --- a/shared/lib/contexts/common/domain/entities/MoneyValue.ts +++ b/shared/lib/contexts/common/domain/entities/MoneyValue.ts @@ -383,7 +383,7 @@ export class MoneyValue extends ValueObject implements IMoneyValue { } public toFormat(format?: string, roundingMode?: RoundingMode): string { - return this.props.toFormat(format, roundingMode); + return this._isNull ? "" : this.props.toFormat(format, roundingMode); } public toUnit(): number { diff --git a/shared/lib/contexts/common/domain/entities/Percentage.ts b/shared/lib/contexts/common/domain/entities/Percentage.ts index 7505e62..5784ec7 100644 --- a/shared/lib/contexts/common/domain/entities/Percentage.ts +++ b/shared/lib/contexts/common/domain/entities/Percentage.ts @@ -245,6 +245,14 @@ export class Percentage extends NullableValueObject { return this.isNull() ? 0 : Number(this.toString()); } + public toFormat(): string { + return this._isNull + ? "" + : Intl.NumberFormat("es-ES", { + maximumFractionDigits: 2, + }).format(this.toNumber()); + } + public toPrimitive(): NullOr { if (this.scale !== Percentage.DEFAULT_SCALE) { return this.convertScale(Percentage.DEFAULT_SCALE).toPrimitive(); diff --git a/shared/lib/contexts/common/domain/entities/Quantity.ts b/shared/lib/contexts/common/domain/entities/Quantity.ts index 0f87ac4..1317c74 100644 --- a/shared/lib/contexts/common/domain/entities/Quantity.ts +++ b/shared/lib/contexts/common/domain/entities/Quantity.ts @@ -222,6 +222,14 @@ export class Quantity extends NullableValueObject { return this.isNull() ? 0 : Number(this.toString()); } + public toFormat(): string { + return this._isNull + ? "" + : Intl.NumberFormat("es-ES", { + maximumFractionDigits: 2, + }).format(this.toNumber()); + } + public toPrimitive(): NullOr { if (this.scale !== Quantity.DEFAULT_SCALE) { return this.convertScale(Quantity.DEFAULT_SCALE).toPrimitive();