From ef74e250f170b09831a85ec617b38a28fb70b62e Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Wed, 11 Sep 2024 16:49:44 +0200 Subject: [PATCH] feat: link pdf template to sales transactions --- .../src/api/controllers/Sales/CreditNotes.ts | 3 + .../api/controllers/Sales/PaymentReceives.ts | 3 + .../api/controllers/Sales/SalesEstimates.ts | 7 +- .../api/controllers/Sales/SalesInvoices.ts | 7 +- .../api/controllers/Sales/SalesReceipts.ts | 9 +- ...240911112147_create_pdf_templates_table.js | 68 +++++++-- .../services/PdfTemplate/CreatePdfTemplate.ts | 9 +- .../services/PdfTemplate/DeletePdfTemplate.ts | 6 + packages/server/src/subscribers/events.ts | 1 + .../InvoiceCustomize/InvoiceCustomize.tsx | 48 ++++++- .../Sales/Invoices/InvoiceCustomize/utils.ts | 24 ++++ .../webapp/src/hooks/query/pdf-templates.ts | 131 ++++++++++++++++++ 12 files changed, 294 insertions(+), 22 deletions(-) create mode 100644 packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/utils.ts create mode 100644 packages/webapp/src/hooks/query/pdf-templates.ts diff --git a/packages/server/src/api/controllers/Sales/CreditNotes.ts b/packages/server/src/api/controllers/Sales/CreditNotes.ts index 95d18b553..e26c26a0d 100644 --- a/packages/server/src/api/controllers/Sales/CreditNotes.ts +++ b/packages/server/src/api/controllers/Sales/CreditNotes.ts @@ -236,6 +236,9 @@ export default class PaymentReceivesController extends BaseController { check('attachments').isArray().optional(), check('attachments.*.key').exists().isString(), + + // Pdf template id. + check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(), ]; } diff --git a/packages/server/src/api/controllers/Sales/PaymentReceives.ts b/packages/server/src/api/controllers/Sales/PaymentReceives.ts index 10cbb0c28..2adeb1a84 100644 --- a/packages/server/src/api/controllers/Sales/PaymentReceives.ts +++ b/packages/server/src/api/controllers/Sales/PaymentReceives.ts @@ -167,6 +167,9 @@ export default class PaymentReceivesController extends BaseController { check('attachments').isArray().optional(), check('attachments.*.key').exists().isString(), + + // Pdf template id. + check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(), ]; } diff --git a/packages/server/src/api/controllers/Sales/SalesEstimates.ts b/packages/server/src/api/controllers/Sales/SalesEstimates.ts index b1808006f..c19632ce1 100644 --- a/packages/server/src/api/controllers/Sales/SalesEstimates.ts +++ b/packages/server/src/api/controllers/Sales/SalesEstimates.ts @@ -168,9 +168,7 @@ export default class SalesEstimatesController extends BaseController { check('entries.*.item_id').exists().isNumeric().toInt(), check('entries.*.quantity').exists().isNumeric().toInt(), check('entries.*.rate').exists().isNumeric().toFloat(), - check('entries.*.description') - .optional({ nullable: true }) - .trim(), + check('entries.*.description').optional({ nullable: true }).trim(), check('entries.*.discount') .optional({ nullable: true }) .isNumeric() @@ -186,6 +184,9 @@ export default class SalesEstimatesController extends BaseController { check('attachments').isArray().optional(), check('attachments.*.key').exists().isString(), + + // Pdf template id. + check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(), ]; } diff --git a/packages/server/src/api/controllers/Sales/SalesInvoices.ts b/packages/server/src/api/controllers/Sales/SalesInvoices.ts index 0b2bc948f..012b7f041 100644 --- a/packages/server/src/api/controllers/Sales/SalesInvoices.ts +++ b/packages/server/src/api/controllers/Sales/SalesInvoices.ts @@ -224,9 +224,7 @@ export default class SaleInvoicesController extends BaseController { .optional({ nullable: true }) .isNumeric() .toFloat(), - check('entries.*.description') - .optional({ nullable: true }) - .trim(), + check('entries.*.description').optional({ nullable: true }).trim(), check('entries.*.tax_code') .optional({ nullable: true }) .trim() @@ -257,6 +255,9 @@ export default class SaleInvoicesController extends BaseController { .optional({ nullable: true }) .isNumeric() .toFloat(), + + // Pdf template id. + check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(), ]; } diff --git a/packages/server/src/api/controllers/Sales/SalesReceipts.ts b/packages/server/src/api/controllers/Sales/SalesReceipts.ts index ba2376568..5330c5d26 100644 --- a/packages/server/src/api/controllers/Sales/SalesReceipts.ts +++ b/packages/server/src/api/controllers/Sales/SalesReceipts.ts @@ -148,17 +148,20 @@ export default class SalesReceiptsController extends BaseController { .optional({ nullable: true }) .isNumeric() .toInt(), - check('entries.*.description') - .optional({ nullable: true }) - .trim(), + check('entries.*.description').optional({ nullable: true }).trim(), check('entries.*.warehouse_id') .optional({ nullable: true }) .isNumeric() .toInt(), + check('receipt_message').optional().trim(), + check('statement').optional().trim(), check('attachments').isArray().optional(), check('attachments.*.key').exists().isString(), + + // Pdf template id. + check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(), ]; } diff --git a/packages/server/src/database/migrations/20240911112147_create_pdf_templates_table.js b/packages/server/src/database/migrations/20240911112147_create_pdf_templates_table.js index ce4c96f59..f48f28498 100644 --- a/packages/server/src/database/migrations/20240911112147_create_pdf_templates_table.js +++ b/packages/server/src/database/migrations/20240911112147_create_pdf_templates_table.js @@ -3,13 +3,49 @@ * @returns { Promise } */ exports.up = function (knex) { - return knex.schema.createTable('pdf_templates', (table) => { - table.increments('id').primary(); - table.text('resource'); - table.text('template_name'); - table.json('attributes'); - table.timestamps(); - }); + return knex.schema + .createTable('pdf_templates', (table) => { + table.increments('id').primary(); + table.text('resource'); + table.text('template_name'); + table.json('attributes'); + table.timestamps(); + }) + .table('sales_invoices', (table) => { + table + .integer('pdf_template_id') + .unsigned() + .references('id') + .inTable('pdf_templates'); + }) + .table('sales_estimates', (table) => { + table + .integer('pdf_template_id') + .unsigned() + .references('id') + .inTable('pdf_templates'); + }) + .table('sales_receipts', (table) => { + table + .integer('pdf_template_id') + .unsigned() + .references('id') + .inTable('pdf_templates'); + }) + .table('credit_notes', (table) => { + table + .integer('pdf_template_id') + .unsigned() + .references('id') + .inTable('pdf_templates'); + }) + .table('payment_receives', (table) => { + table + .integer('pdf_template_id') + .unsigned() + .references('id') + .inTable('pdf_templates'); + }); }; /** @@ -17,5 +53,21 @@ exports.up = function (knex) { * @returns { Promise } */ exports.down = function (knex) { - return knex.schema.dropTableIfExists('pdf_templates'); + return knex.schema + .table('payment_receives', (table) => { + table.dropColumn('pdf_template_id'); + }) + .table('credit_notes', (table) => { + table.dropColumn('pdf_template_id'); + }) + .table('sales_receipts', (table) => { + table.dropColumn('pdf_template_id'); + }) + .table('sales_estimates', (table) => { + table.dropColumn('pdf_template_id'); + }) + .table('sales_invoices', (table) => { + table.dropColumn('pdf_template_id'); + }) + .dropTableIfExists('pdf_templates'); }; diff --git a/packages/server/src/services/PdfTemplate/CreatePdfTemplate.ts b/packages/server/src/services/PdfTemplate/CreatePdfTemplate.ts index 251e67980..feab657c6 100644 --- a/packages/server/src/services/PdfTemplate/CreatePdfTemplate.ts +++ b/packages/server/src/services/PdfTemplate/CreatePdfTemplate.ts @@ -31,13 +31,18 @@ export class CreatePdfTemplate { const attributes = invoiceTemplateDTO; return this.uow.withTransaction(tenantId, async (trx) => { + // Triggers `onPdfTemplateCreating` event. + await this.eventPublisher.emitAsync(events.pdfTemplate.onCreating, { + tenantId, + }); + await PdfTemplate.query(trx).insert({ templateName, resource, attributes, }); - - await this.eventPublisher.emitAsync(events.pdfTemplate.onInvoiceCreated, { + // Triggers `onPdfTemplateCreated` event. + await this.eventPublisher.emitAsync(events.pdfTemplate.onCreated, { tenantId, }); }); diff --git a/packages/server/src/services/PdfTemplate/DeletePdfTemplate.ts b/packages/server/src/services/PdfTemplate/DeletePdfTemplate.ts index eaf4391a5..e15bea1de 100644 --- a/packages/server/src/services/PdfTemplate/DeletePdfTemplate.ts +++ b/packages/server/src/services/PdfTemplate/DeletePdfTemplate.ts @@ -24,8 +24,14 @@ export class DeletePdfTemplate { const { PdfTemplate } = this.tenancy.models(tenantId); return this.uow.withTransaction(tenantId, async (trx) => { + // Triggers `onPdfTemplateDeleting` event. + await this.eventPublisher.emitAsync(events.pdfTemplate.onDeleting, { + tenantId, + templateId, + }); await PdfTemplate.query(trx).deleteById(templateId); + // Triggers `onPdfTemplateDeleted` event. await this.eventPublisher.emitAsync(events.pdfTemplate.onDeleted, { tenantId, templateId, diff --git a/packages/server/src/subscribers/events.ts b/packages/server/src/subscribers/events.ts index 2a70570cb..ba2cf7103 100644 --- a/packages/server/src/subscribers/events.ts +++ b/packages/server/src/subscribers/events.ts @@ -692,6 +692,7 @@ export default { onEditing: 'onPdfTemplateEditing', onEdited: 'onPdfTemplatedEdited', + onDeleting: 'onPdfTemplateDeleting', onDeleted: 'onPdfTemplateDeleted', onInvoiceCreated: 'onInvoicePdfTemplateCreated', diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceCustomize.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceCustomize.tsx index 090cadc88..c42dc2e44 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceCustomize.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceCustomize.tsx @@ -1,7 +1,7 @@ import React from 'react'; import * as R from 'ramda'; -import { Box } from '@/components'; -import { Classes } from '@blueprintjs/core'; +import { AppToaster, Box } from '@/components'; +import { Classes, Intent } from '@blueprintjs/core'; import { useFormikContext } from 'formik'; import { InvoicePaperTemplate, @@ -12,9 +12,51 @@ import { InvoiceCustomizeGeneralField } from './InvoiceCustomizeGeneralFields'; import { InvoiceCustomizeContentFields } from './InvoiceCutomizeContentFields'; import { InvoiceCustomizeValues } from './types'; import { initialValues } from './constants'; +import { + useCreatePdfTemplate, + useEditPdfTemplate, +} from '@/hooks/query/pdf-templates'; +import { transformToEditRequest, transformToNewRequest } from './utils'; export default function InvoiceCustomizeContent() { - const handleFormSubmit = (values: InvoiceCustomizeValues) => {}; + const { mutateAsync: createPdfTemplate } = useCreatePdfTemplate(); + const { mutateAsync: editPdfTemplate } = useEditPdfTemplate(); + + const templateId: number = 1; + + const handleFormSubmit = (values: InvoiceCustomizeValues) => { + const handleSuccess = (message: string) => { + AppToaster.show({ + intent: Intent.SUCCESS, + message, + }); + }; + const handleError = (message: string) => { + AppToaster.show({ + intent: Intent.DANGER, + message, + }); + }; + if (templateId) { + const reqValues = transformToEditRequest(values, templateId); + + // Edit existing template + editPdfTemplate({ ...reqValues }) + .then(() => handleSuccess('PDF template updated successfully!')) + .catch(() => + handleError('An error occurred while updating the PDF template.'), + ); + } else { + const reqValues = transformToNewRequest(values); + + // Create new template + createPdfTemplate(reqValues) + .then(() => handleSuccess('PDF template created successfully!')) + .catch(() => + handleError('An error occurred while creating the PDF template.'), + ); + } + }; return ( diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/utils.ts b/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/utils.ts new file mode 100644 index 000000000..a063b9ee2 --- /dev/null +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/utils.ts @@ -0,0 +1,24 @@ +import { omit } from 'lodash'; +import { InvoiceCustomizeValues } from './types'; +import { CreatePdfTemplateValues, EditPdfTemplateValues } from '@/hooks/query/pdf-templates'; + +export const transformToEditRequest = ( + values: InvoiceCustomizeValues, + templateId: number, +): EditPdfTemplateValues => { + return { + templateId, + templateName: 'Template Name', + attributes: omit(values, ['templateName']), + }; +}; + +export const transformToNewRequest = ( + values: InvoiceCustomizeValues, +): CreatePdfTemplateValues => { + return { + resource: 'SaleInvoice', + templateName: 'Template Name', + attributes: omit(values, ['templateName']), + }; +}; diff --git a/packages/webapp/src/hooks/query/pdf-templates.ts b/packages/webapp/src/hooks/query/pdf-templates.ts new file mode 100644 index 000000000..2045c0fb3 --- /dev/null +++ b/packages/webapp/src/hooks/query/pdf-templates.ts @@ -0,0 +1,131 @@ +import { + useMutation, + useQuery, + UseMutationOptions, + UseQueryOptions, + UseMutationResult, + UseQueryResult, +} from 'react-query'; +import useApiRequest from '../useRequest'; + +export interface CreatePdfTemplateValues { + templateName: string; + resource: string; + attributes: Record; +} + +export interface CreatePdfTemplateResponse {} + +export interface EditPdfTemplateValues { + templateId: string | number; + templateName: string; + attributes: Record; +} + +export interface EditPdfTemplateResponse {} + +export interface DeletePdfTemplateValues { + templateId: number | string; +} + +export interface DeletePdfTemplateResponse {} + +export interface GetPdfTemplateValues {} + +export interface GetPdfTemplateResponse {} + +export interface GetPdfTemplatesValues {} + +export interface GetPdfTemplatesResponse {} + +// Hook for creating a PDF template +export const useCreatePdfTemplate = ( + options?: UseMutationOptions< + CreatePdfTemplateResponse, + Error, + CreatePdfTemplateValues + >, +): UseMutationResult< + CreatePdfTemplateResponse, + Error, + CreatePdfTemplateValues +> => { + const apiRequest = useApiRequest(); + return useMutation( + (values) => + apiRequest.post('/pdf-templates', values).then((res) => res.data), + options, + ); +}; + +// Hook for editing a PDF template +export const useEditPdfTemplate = ( + options?: UseMutationOptions< + EditPdfTemplateResponse, + Error, + { templateId: number; values: EditPdfTemplateValues } + >, +): UseMutationResult< + EditPdfTemplateResponse, + Error, + { templateId: number; values: EditPdfTemplateValues } +> => { + const apiRequest = useApiRequest(); + return useMutation< + EditPdfTemplateResponse, + Error, + { templateId: number; values: EditPdfTemplateValues } + >( + ({ templateId, values }) => + apiRequest + .put(`/pdf-templates/${templateId}`, values) + .then((res) => res.data), + options, + ); +}; + +// Hook for deleting a PDF template +export const useDeletePdfTemplate = ( + options?: UseMutationOptions< + DeletePdfTemplateResponse, + Error, + { templateId: number } + >, +): UseMutationResult< + DeletePdfTemplateResponse, + Error, + { templateId: number } +> => { + const apiRequest = useApiRequest(); + return useMutation( + ({ templateId }) => + apiRequest.delete(`/pdf-templates/${templateId}`).then((res) => res.data), + options, + ); +}; + +// Hook for getting a single PDF template +export const useGetPdfTemplate = ( + templateId: number, + options?: UseQueryOptions, +): UseQueryResult => { + const apiRequest = useApiRequest(); + return useQuery( + ['pdfTemplate', templateId], + () => + apiRequest.get(`/pdf-templates/${templateId}`).then((res) => res.data), + options, + ); +}; + +// Hook for getting multiple PDF templates +export const useGetPdfTemplates = ( + options?: UseQueryOptions, +): UseQueryResult => { + const apiRequest = useApiRequest(); + return useQuery( + 'pdfTemplates', + () => apiRequest.get('/pdf-templates').then((res) => res.data), + options, + ); +};