From 6c1870be8f7ad3417b6e841df6a4135dff7d9b0b Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Wed, 13 Nov 2024 18:15:13 +0200 Subject: [PATCH 01/64] fix: make manual entries adjust decimal credit/debit amounts --- .../webapp/src/containers/Accounting/MakeJournal/utils.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/webapp/src/containers/Accounting/MakeJournal/utils.tsx b/packages/webapp/src/containers/Accounting/MakeJournal/utils.tsx index ab7ff4c49..38fa852e5 100644 --- a/packages/webapp/src/containers/Accounting/MakeJournal/utils.tsx +++ b/packages/webapp/src/containers/Accounting/MakeJournal/utils.tsx @@ -4,7 +4,7 @@ import * as R from 'ramda'; import moment from 'moment'; import intl from 'react-intl-universal'; import { Intent } from '@blueprintjs/core'; -import { sumBy, setWith, toSafeInteger, get, first } from 'lodash'; +import { sumBy, setWith, get, first, toNumber } from 'lodash'; import { updateTableCell, repeatValue, @@ -91,8 +91,8 @@ export function transformToEditForm(manualJournal) { * Entries adjustment. */ function adjustmentEntries(entries) { - const credit = sumBy(entries, (e) => toSafeInteger(e.credit)); - const debit = sumBy(entries, (e) => toSafeInteger(e.debit)); + const credit = sumBy(entries, (e) => toNumber(e.credit)); + const debit = sumBy(entries, (e) => toNumber(e.debit)); return { debit: Math.max(credit - debit, 0), From 5d6f901d3393feb279097a7962f94f3be5f0dad3 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Wed, 13 Nov 2024 18:35:57 +0200 Subject: [PATCH 02/64] feat: allow quantity of entries accept decimal value (#753) --- .../src/api/controllers/Purchases/Bills.ts | 2 +- .../api/controllers/Purchases/VendorCredit.ts | 4 +- .../src/api/controllers/Sales/CreditNotes.ts | 7 ++- .../api/controllers/Sales/SalesEstimates.ts | 7 ++- .../api/controllers/Sales/SalesReceipts.ts | 7 ++- ...ge_quantity_in_items_entries_to_decimal.js | 39 +++++++++++++++++ .../DataTableCells/NumericInputCell.tsx | 43 +++++++++++-------- .../containers/Drawers/BillDrawer/utils.tsx | 6 +-- .../Drawers/CreditNoteDetailDrawer/utils.tsx | 5 +-- .../Drawers/InvoiceDetailDrawer/utils.tsx | 3 +- .../components.tsx | 2 +- .../Drawers/ReceiptDetailDrawer/utils.tsx | 5 +-- 12 files changed, 83 insertions(+), 47 deletions(-) create mode 100644 packages/server/src/database/migrations/20241113113437_change_quantity_in_items_entries_to_decimal.js diff --git a/packages/server/src/api/controllers/Purchases/Bills.ts b/packages/server/src/api/controllers/Purchases/Bills.ts index 7248c97df..94bfb814d 100644 --- a/packages/server/src/api/controllers/Purchases/Bills.ts +++ b/packages/server/src/api/controllers/Purchases/Bills.ts @@ -121,7 +121,7 @@ export default class BillsController extends BaseController { check('entries.*.index').exists().isNumeric().toInt(), check('entries.*.item_id').exists().isNumeric().toInt(), check('entries.*.rate').exists().isNumeric().toFloat(), - check('entries.*.quantity').exists().isNumeric().toInt(), + check('entries.*.quantity').exists().isNumeric().toFloat(), check('entries.*.discount') .optional({ nullable: true }) .isNumeric() diff --git a/packages/server/src/api/controllers/Purchases/VendorCredit.ts b/packages/server/src/api/controllers/Purchases/VendorCredit.ts index 26e3f85b9..e97fbf687 100644 --- a/packages/server/src/api/controllers/Purchases/VendorCredit.ts +++ b/packages/server/src/api/controllers/Purchases/VendorCredit.ts @@ -170,7 +170,7 @@ export default class VendorCreditController extends BaseController { check('entries.*.index').exists().isNumeric().toInt(), check('entries.*.item_id').exists().isNumeric().toInt(), check('entries.*.rate').exists().isNumeric().toFloat(), - check('entries.*.quantity').exists().isNumeric().toInt(), + check('entries.*.quantity').exists().isNumeric().toFloat(), check('entries.*.discount') .optional({ nullable: true }) .isNumeric() @@ -209,7 +209,7 @@ export default class VendorCreditController extends BaseController { check('entries.*.index').exists().isNumeric().toInt(), check('entries.*.item_id').exists().isNumeric().toInt(), check('entries.*.rate').exists().isNumeric().toFloat(), - check('entries.*.quantity').exists().isNumeric().toInt(), + check('entries.*.quantity').exists().isNumeric().toFloat(), check('entries.*.discount') .optional({ nullable: true }) .isNumeric() diff --git a/packages/server/src/api/controllers/Sales/CreditNotes.ts b/packages/server/src/api/controllers/Sales/CreditNotes.ts index 3037d33f1..a88b45939 100644 --- a/packages/server/src/api/controllers/Sales/CreditNotes.ts +++ b/packages/server/src/api/controllers/Sales/CreditNotes.ts @@ -233,7 +233,7 @@ export default class PaymentReceivesController extends BaseController { check('entries.*.index').exists().isNumeric().toInt(), check('entries.*.item_id').exists().isNumeric().toInt(), check('entries.*.rate').exists().isNumeric().toFloat(), - check('entries.*.quantity').exists().isNumeric().toInt(), + check('entries.*.quantity').exists().isNumeric().toFloat(), check('entries.*.discount') .optional({ nullable: true }) .isNumeric() @@ -755,9 +755,8 @@ export default class PaymentReceivesController extends BaseController { const { tenantId } = req; try { - const data = await this.getCreditNoteStateService.getCreditNoteState( - tenantId - ); + const data = + await this.getCreditNoteStateService.getCreditNoteState(tenantId); return res.status(200).send({ data }); } catch (error) { next(error); diff --git a/packages/server/src/api/controllers/Sales/SalesEstimates.ts b/packages/server/src/api/controllers/Sales/SalesEstimates.ts index 556e96fd6..5a2a80673 100644 --- a/packages/server/src/api/controllers/Sales/SalesEstimates.ts +++ b/packages/server/src/api/controllers/Sales/SalesEstimates.ts @@ -172,7 +172,7 @@ export default class SalesEstimatesController extends BaseController { check('entries').exists().isArray({ min: 1 }), check('entries.*.index').exists().isNumeric().toInt(), check('entries.*.item_id').exists().isNumeric().toInt(), - check('entries.*.quantity').exists().isNumeric().toInt(), + check('entries.*.quantity').exists().isNumeric().toFloat(), check('entries.*.rate').exists().isNumeric().toFloat(), check('entries.*.description').optional({ nullable: true }).trim(), check('entries.*.discount') @@ -562,9 +562,8 @@ export default class SalesEstimatesController extends BaseController { const { tenantId } = req; try { - const data = await this.saleEstimatesApplication.getSaleEstimateState( - tenantId - ); + const data = + await this.saleEstimatesApplication.getSaleEstimateState(tenantId); return res.status(200).send({ data }); } catch (error) { next(error); diff --git a/packages/server/src/api/controllers/Sales/SalesReceipts.ts b/packages/server/src/api/controllers/Sales/SalesReceipts.ts index caf93d02a..6b7d6ef7a 100644 --- a/packages/server/src/api/controllers/Sales/SalesReceipts.ts +++ b/packages/server/src/api/controllers/Sales/SalesReceipts.ts @@ -148,7 +148,7 @@ export default class SalesReceiptsController extends BaseController { check('entries.*.id').optional({ nullable: true }).isNumeric().toInt(), check('entries.*.index').exists().isNumeric().toInt(), check('entries.*.item_id').exists().isNumeric().toInt(), - check('entries.*.quantity').exists().isNumeric().toInt(), + check('entries.*.quantity').exists().isNumeric().toFloat(), check('entries.*.rate').exists().isNumeric().toFloat(), check('entries.*.discount') .optional({ nullable: true }) @@ -392,9 +392,8 @@ export default class SalesReceiptsController extends BaseController { // Retrieves receipt in pdf format. try { - const data = await this.saleReceiptsApplication.getSaleReceiptState( - tenantId - ); + const data = + await this.saleReceiptsApplication.getSaleReceiptState(tenantId); return res.status(200).send({ data }); } catch (error) { next(error); diff --git a/packages/server/src/database/migrations/20241113113437_change_quantity_in_items_entries_to_decimal.js b/packages/server/src/database/migrations/20241113113437_change_quantity_in_items_entries_to_decimal.js new file mode 100644 index 000000000..dcc711ffc --- /dev/null +++ b/packages/server/src/database/migrations/20241113113437_change_quantity_in_items_entries_to_decimal.js @@ -0,0 +1,39 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function (knex) { + return knex.schema + .table('items_entries', (table) => { + table.decimal('quantity', 13, 3).alter(); + }) + .table('inventory_transactions', (table) => { + table.decimal('quantity', 13, 3).alter(); + }) + .table('inventory_cost_lot_tracker', (table) => { + table.decimal('quantity', 13, 3).alter(); + }) + .table('items', (table) => { + table.decimal('quantityOnHand', 13, 3).alter(); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return knex.schema + .table('items_entries', (table) => { + table.integer('quantity').alter(); + }) + .table('inventory_transactions', (table) => { + table.integer('quantity').alter(); + }) + .table('inventory_cost_lot_tracker', (table) => { + table.integer('quantity').alter(); + }) + .table('items', (table) => { + table.integer('quantityOnHand').alter(); + }); +}; diff --git a/packages/webapp/src/components/DataTableCells/NumericInputCell.tsx b/packages/webapp/src/components/DataTableCells/NumericInputCell.tsx index 71e2d637d..b3432267c 100644 --- a/packages/webapp/src/components/DataTableCells/NumericInputCell.tsx +++ b/packages/webapp/src/components/DataTableCells/NumericInputCell.tsx @@ -1,8 +1,6 @@ -// @ts-nocheck import React, { useState, useEffect } from 'react'; import { FormGroup, NumericInput, Intent } from '@blueprintjs/core'; import classNames from 'classnames'; - import { CellType } from '@/constants'; import { CLASSES } from '@/constants/classes'; @@ -12,37 +10,44 @@ import { CLASSES } from '@/constants/classes'; export default function NumericInputCell({ row: { index }, column: { id }, - cell: { value: initialValue }, + cell: { value: controlledInputValue }, payload, -}) { - const [value, setValue] = useState(initialValue); +}: any) { + const [valueAsNumber, setValueAsNumber] = useState( + controlledInputValue || null, + ); + const handleInputValueChange = ( + valueAsNumber: number, + valueAsString: string, + ) => { + setValueAsNumber(valueAsNumber); + }; + const handleInputBlur = () => { + payload.updateData(index, id, valueAsNumber); + }; - const handleValueChange = (newValue) => { - setValue(newValue); - }; - const onBlur = () => { - payload.updateData(index, id, value); - }; useEffect(() => { - setValue(initialValue); - }, [initialValue]); + setValueAsNumber(controlledInputValue); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [controlledInputValue]); const error = payload.errors?.[index]?.[id]; return ( ); } -NumericInputCell.cellType = CellType.Field; \ No newline at end of file +NumericInputCell.cellType = CellType.Field; diff --git a/packages/webapp/src/containers/Drawers/BillDrawer/utils.tsx b/packages/webapp/src/containers/Drawers/BillDrawer/utils.tsx index 9b9691ddf..cc2d2c466 100644 --- a/packages/webapp/src/containers/Drawers/BillDrawer/utils.tsx +++ b/packages/webapp/src/containers/Drawers/BillDrawer/utils.tsx @@ -13,7 +13,6 @@ import { Tag, } from '@blueprintjs/core'; import { - FormatNumberCell, TextOverviewTooltipCell, FormattedMessage as T, Choose, @@ -51,9 +50,8 @@ export const useBillReadonlyEntriesTableColumns = () => { }, { Header: intl.get('quantity'), - accessor: 'quantity', - Cell: FormatNumberCell, - width: getColumnWidth(entries, 'quantity', { + accessor: 'quantity_formatted', + width: getColumnWidth(entries, 'quantity_formatted', { minWidth: 60, magicSpacing: 5, }), diff --git a/packages/webapp/src/containers/Drawers/CreditNoteDetailDrawer/utils.tsx b/packages/webapp/src/containers/Drawers/CreditNoteDetailDrawer/utils.tsx index 85e57c5ee..c4a5bcd74 100644 --- a/packages/webapp/src/containers/Drawers/CreditNoteDetailDrawer/utils.tsx +++ b/packages/webapp/src/containers/Drawers/CreditNoteDetailDrawer/utils.tsx @@ -48,9 +48,8 @@ export const useCreditNoteReadOnlyEntriesColumns = () => { }, { Header: intl.get('quantity'), - accessor: 'quantity', - Cell: FormatNumberCell, - width: getColumnWidth(entries, 'quantity', { + accessor: 'quantity_formatted', + width: getColumnWidth(entries, 'quantity_formatted', { minWidth: 60, magicSpacing: 5, }), diff --git a/packages/webapp/src/containers/Drawers/InvoiceDetailDrawer/utils.tsx b/packages/webapp/src/containers/Drawers/InvoiceDetailDrawer/utils.tsx index bb7f8ebc8..33396a2c5 100644 --- a/packages/webapp/src/containers/Drawers/InvoiceDetailDrawer/utils.tsx +++ b/packages/webapp/src/containers/Drawers/InvoiceDetailDrawer/utils.tsx @@ -54,11 +54,10 @@ export const useInvoiceReadonlyEntriesColumns = () => { { Header: intl.get('quantity'), accessor: 'quantity', - Cell: FormatNumberCell, align: 'right', disableSortBy: true, textOverview: true, - width: getColumnWidth(entries, 'quantity', { + width: getColumnWidth(entries, 'quantity_formatted', { minWidth: 60, magicSpacing: 5, }), diff --git a/packages/webapp/src/containers/Drawers/ItemDetailDrawer/ItemPaymentTransactions/EstimatePaymentTransactions/components.tsx b/packages/webapp/src/containers/Drawers/ItemDetailDrawer/ItemPaymentTransactions/EstimatePaymentTransactions/components.tsx index 08ddf152f..8055c4137 100644 --- a/packages/webapp/src/containers/Drawers/ItemDetailDrawer/ItemPaymentTransactions/EstimatePaymentTransactions/components.tsx +++ b/packages/webapp/src/containers/Drawers/ItemDetailDrawer/ItemPaymentTransactions/EstimatePaymentTransactions/components.tsx @@ -72,7 +72,7 @@ export const useEstimateTransactionsColumns = () => { { id: 'qunatity', Header: intl.get('item.drawer_quantity_sold'), - accessor: 'quantity', + accessor: 'quantity_formatted', align: 'right', width: 100, }, diff --git a/packages/webapp/src/containers/Drawers/ReceiptDetailDrawer/utils.tsx b/packages/webapp/src/containers/Drawers/ReceiptDetailDrawer/utils.tsx index 0241d3e76..87399cd85 100644 --- a/packages/webapp/src/containers/Drawers/ReceiptDetailDrawer/utils.tsx +++ b/packages/webapp/src/containers/Drawers/ReceiptDetailDrawer/utils.tsx @@ -31,9 +31,8 @@ export const useReceiptReadonlyEntriesTableColumns = () => { }, { Header: intl.get('quantity'), - accessor: 'quantity', - Cell: FormatNumberCell, - width: getColumnWidth(entries, 'quantity', { + accessor: 'quantity_formatted', + width: getColumnWidth(entries, 'quantity_formatted', { minWidth: 60, magicSpacing: 5, }), From 0e99ac88d3f97646a2f5efeeb6eb952a4af55a31 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sat, 16 Nov 2024 21:23:12 +0200 Subject: [PATCH 03/64] hotbug: upload attachments --- .../Attachments/UploadAttachmentsPopoverContent.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/webapp/src/containers/Attachments/UploadAttachmentsPopoverContent.tsx b/packages/webapp/src/containers/Attachments/UploadAttachmentsPopoverContent.tsx index f755b42b4..ddcdc794c 100644 --- a/packages/webapp/src/containers/Attachments/UploadAttachmentsPopoverContent.tsx +++ b/packages/webapp/src/containers/Attachments/UploadAttachmentsPopoverContent.tsx @@ -22,7 +22,7 @@ interface AttachmentFileCommon { size: number; mimeType: string; } -interface AttachmentFileLoaded extends AttachmentFileCommon {} +interface AttachmentFileLoaded extends AttachmentFileCommon { } interface AttachmentFileLoading extends AttachmentFileCommon { loading: boolean; } @@ -74,11 +74,11 @@ export function UploadAttachmentsPopoverContent({ }; // Uploads the attachments. const { mutateAsync: uploadAttachments } = useUploadAttachments({ - onSuccess: (data) => { + onSuccess: (data, formData) => { const newLocalFiles = stopLoadingAttachment( localFiles, - data.config.data.get('internalKey'), - data.data.data.key, + formData.get('internalKey'), + data.key, ); handleFilesChange(newLocalFiles); onUploadedChange && onUploadedChange(newLocalFiles); From 53ab40a075d703ce982032b1f35d76307c19688e Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sun, 17 Nov 2024 15:45:55 +0200 Subject: [PATCH 04/64] feat: estimate, receipt, credit note mail preview --- .../api/controllers/Sales/PaymentReceives.ts | 16 +- .../api/controllers/Sales/SalesEstimates.ts | 15 +- .../api/controllers/Sales/SalesReceipts.ts | 7 + .../Estimates/SaleEstimatesApplication.ts | 12 + .../Sales/Estimates/SaleEstimatesPdf.ts | 29 +- .../PaymentReceived/GetPaymentReceivedPdf.ts | 31 +- .../PaymentReceivedApplication.ts | 16 ++ .../Sales/Receipts/SaleReceiptApplication.ts | 13 + .../Sales/Receipts/SaleReceiptsPdfService.ts | 25 +- .../EstimateSendMail.schema.ts | 3 + .../EstimateSendMailBoot.tsx | 52 ++++ .../EstimateSendMailContent.tsx | 9 + .../EstimateSendMailDrawer.tsx | 42 +++ .../EstimateSendMailForm.tsx | 79 +++++ .../EstimateSendMailDrawer/_interfaces.ts | 1 + .../Estimates/EstimateSendMailDrawer/index.ts | 0 .../Sales/Estimates/EstimatesAlerts.tsx | 20 +- .../SendMailViewHeader.tsx} | 6 +- .../SendMailViewMessageField.tsx | 109 +++++++ .../SendMailViewPreview.tsx | 0 .../SendMailViewPreviewHeader.tsx | 76 +++++ .../SendMailViewPreviewPdfIframe.tsx | 27 ++ .../SendMailViewPreviewTabs.tsx | 47 +++ .../SendMailViewToAddressField.tsx | 209 ++++++++++++++ .../Estimates/SendMailViewDrawer/_types.ts | 1 + .../InvoiceSendMailContent.tsx | 4 +- .../InvoiceSendMailFields.tsx | 272 +----------------- .../InvoiceSendMailPreview.tsx | 75 ++--- .../InvoiceSendPdfPreviewConnected.tsx | 16 +- .../src/components/EstimatePaperTemplate.tsx | 247 ++++++++++++++++ .../PaymentReceivedPaperTemplate.tsx | 180 ++++++++++++ .../src/components/ReceiptPaperTemplate.tsx | 237 +++++++++++++++ shared/pdf-templates/src/index.ts | 7 + .../render-estimate-paper-template.tsx | 16 ++ .../renders/render-invoice-paper-template.tsx | 6 - ...render-payment-received-paper-template.tsx | 11 + .../renders/render-receipt-paper-template.tsx | 11 + 37 files changed, 1531 insertions(+), 396 deletions(-) create mode 100644 packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMail.schema.ts create mode 100644 packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailBoot.tsx create mode 100644 packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailContent.tsx create mode 100644 packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailDrawer.tsx create mode 100644 packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailForm.tsx create mode 100644 packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/_interfaces.ts create mode 100644 packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/index.ts rename packages/webapp/src/containers/Sales/{Invoices/InvoiceSendMailDrawer/InvoiceSendMailHeader.tsx => Estimates/SendMailViewDrawer/SendMailViewHeader.tsx} (91%) create mode 100644 packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewMessageField.tsx create mode 100644 packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewPreview.tsx create mode 100644 packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewPreviewHeader.tsx create mode 100644 packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewPreviewPdfIframe.tsx create mode 100644 packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewPreviewTabs.tsx create mode 100644 packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewToAddressField.tsx create mode 100644 packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/_types.ts create mode 100644 shared/pdf-templates/src/components/EstimatePaperTemplate.tsx create mode 100644 shared/pdf-templates/src/components/PaymentReceivedPaperTemplate.tsx create mode 100644 shared/pdf-templates/src/components/ReceiptPaperTemplate.tsx create mode 100644 shared/pdf-templates/src/renders/render-estimate-paper-template.tsx create mode 100644 shared/pdf-templates/src/renders/render-payment-received-paper-template.tsx create mode 100644 shared/pdf-templates/src/renders/render-receipt-paper-template.tsx diff --git a/packages/server/src/api/controllers/Sales/PaymentReceives.ts b/packages/server/src/api/controllers/Sales/PaymentReceives.ts index b0f17bfac..5979d30f0 100644 --- a/packages/server/src/api/controllers/Sales/PaymentReceives.ts +++ b/packages/server/src/api/controllers/Sales/PaymentReceives.ts @@ -411,9 +411,8 @@ export default class PaymentReceivesController extends BaseController { const { tenantId } = req; try { - const data = await this.paymentReceiveApplication.getPaymentReceivedState( - tenantId - ); + const data = + await this.paymentReceiveApplication.getPaymentReceivedState(tenantId); return res.status(200).send({ data }); } catch (error) { next(error); @@ -471,7 +470,7 @@ export default class PaymentReceivesController extends BaseController { ACCEPT_TYPE.APPLICATION_JSON, ACCEPT_TYPE.APPLICATION_PDF, ]); - // Response in pdf format. + // Responses pdf format. if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) { const [pdfContent, filename] = await this.paymentReceiveApplication.getPaymentReceivePdf( @@ -484,7 +483,14 @@ export default class PaymentReceivesController extends BaseController { 'Content-Disposition': `attachment; filename="${filename}"`, }); res.send(pdfContent); - // Response in json format. + // Responses html format. + } else if (ACCEPT_TYPE.APPLICATION_TEXT_HTML === acceptType) { + const htmlContent = this.paymentReceiveApplication.getPaymentReceivedHtml( + tenantId, + paymentReceiveId + ); + return res.status(200).send({ htmlContent }); + // Responses json format. } else { const paymentReceive = await this.paymentReceiveApplication.getPaymentReceive( diff --git a/packages/server/src/api/controllers/Sales/SalesEstimates.ts b/packages/server/src/api/controllers/Sales/SalesEstimates.ts index 5a2a80673..68f7e593e 100644 --- a/packages/server/src/api/controllers/Sales/SalesEstimates.ts +++ b/packages/server/src/api/controllers/Sales/SalesEstimates.ts @@ -13,11 +13,8 @@ import DynamicListingService from '@/services/DynamicListing/DynamicListService' import { ServiceError } from '@/exceptions'; import CheckPolicies from '@/api/middleware/CheckPolicies'; import { SaleEstimatesApplication } from '@/services/Sales/Estimates/SaleEstimatesApplication'; +import { ACCEPT_TYPE } from '@/interfaces/Http'; -const ACCEPT_TYPE = { - APPLICATION_PDF: 'application/pdf', - APPLICATION_JSON: 'application/json', -}; @Service() export default class SalesEstimatesController extends BaseController { @Inject() @@ -395,6 +392,7 @@ export default class SalesEstimatesController extends BaseController { const acceptType = accept.types([ ACCEPT_TYPE.APPLICATION_JSON, ACCEPT_TYPE.APPLICATION_PDF, + ACCEPT_TYPE.APPLICATION_TEXT_HTML, ]); // Retrieves estimate in pdf format. if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) { @@ -410,7 +408,14 @@ export default class SalesEstimatesController extends BaseController { }); res.send(pdfContent); // Retrieves estimates in json format. - } else { + } else if (ACCEPT_TYPE.APPLICATION_TEXT_HTML === acceptType) { + const htmlContent = + await this.saleEstimatesApplication.getSaleEstimateHtml( + tenantId, + estimateId + ); + return res.status(200).send({ htmlContent }); + } else if (ACCEPT_TYPE.APPLICATION_JSON) { const estimate = await this.saleEstimatesApplication.getSaleEstimate( tenantId, estimateId diff --git a/packages/server/src/api/controllers/Sales/SalesReceipts.ts b/packages/server/src/api/controllers/Sales/SalesReceipts.ts index 6b7d6ef7a..3b185634f 100644 --- a/packages/server/src/api/controllers/Sales/SalesReceipts.ts +++ b/packages/server/src/api/controllers/Sales/SalesReceipts.ts @@ -353,6 +353,7 @@ export default class SalesReceiptsController extends BaseController { const acceptType = accept.types([ ACCEPT_TYPE.APPLICATION_JSON, ACCEPT_TYPE.APPLICATION_PDF, + ACCEPT_TYPE.APPLICATION_TEXT_HTML, ]); // Retrieves receipt in pdf format. if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) { @@ -368,6 +369,12 @@ export default class SalesReceiptsController extends BaseController { }); res.send(pdfContent); // Retrieves receipt in json format. + } else if (ACCEPT_TYPE.APPLICATION_TEXT_HTML === acceptType) { + const htmlContent = await this.saleReceiptsApplication.getSaleReceiptHtml( + tenantId, + saleReceiptId + ); + res.send({ htmlContent }); } else { const saleReceipt = await this.saleReceiptsApplication.getSaleReceipt( tenantId, diff --git a/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts b/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts index f9972e282..233d40eac 100644 --- a/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts +++ b/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts @@ -220,6 +220,18 @@ export class SaleEstimatesApplication { ); } + /** + * Retrieve the HTML content of the given sale estimate. + * @param {number} tenantId + * @param {number} saleEstimateId + */ + public getSaleEstimateHtml(tenantId: number, saleEstimateId: number) { + return this.saleEstimatesPdfService.saleEstimateHtml( + tenantId, + saleEstimateId + ); + } + /** * Send the reminder mail of the given sale estimate. * @param {number} tenantId diff --git a/packages/server/src/services/Sales/Estimates/SaleEstimatesPdf.ts b/packages/server/src/services/Sales/Estimates/SaleEstimatesPdf.ts index f0750b43c..906da6de6 100644 --- a/packages/server/src/services/Sales/Estimates/SaleEstimatesPdf.ts +++ b/packages/server/src/services/Sales/Estimates/SaleEstimatesPdf.ts @@ -8,6 +8,7 @@ import { transformEstimateToPdfTemplate } from './utils'; import { EstimatePdfBrandingAttributes } from './constants'; import events from '@/subscribers/events'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import { renderEstimatePaperTemplateHtml } from '@bigcapital/pdf-templates'; @Service() export class SaleEstimatesPdf { @@ -29,6 +30,22 @@ export class SaleEstimatesPdf { @Inject() private eventPublisher: EventPublisher; + /** + * Retrieve sale estimate html content. + * @param {number} tenantId - + * @param {number} invoiceId - + */ + public async saleEstimateHtml( + tenantId: number, + estimateId: number + ): Promise { + const brandingAttributes = await this.getEstimateBrandingAttributes( + tenantId, + estimateId + ); + return renderEstimatePaperTemplateHtml({ ...brandingAttributes }); + } + /** * Retrieve sale invoice pdf content. * @param {number} tenantId - @@ -42,15 +59,9 @@ export class SaleEstimatesPdf { tenantId, saleEstimateId ); - const brandingAttributes = await this.getEstimateBrandingAttributes( - tenantId, - saleEstimateId - ); - const htmlContent = await this.templateInjectable.render( - tenantId, - 'modules/estimate-regular', - brandingAttributes - ); + // Retireves the sale estimate html. + const htmlContent = await this.saleEstimateHtml(tenantId, saleEstimateId); + const content = await this.chromiumlyTenancy.convertHtmlContent( tenantId, htmlContent diff --git a/packages/server/src/services/Sales/PaymentReceived/GetPaymentReceivedPdf.ts b/packages/server/src/services/Sales/PaymentReceived/GetPaymentReceivedPdf.ts index ed88b7ebf..36533961c 100644 --- a/packages/server/src/services/Sales/PaymentReceived/GetPaymentReceivedPdf.ts +++ b/packages/server/src/services/Sales/PaymentReceived/GetPaymentReceivedPdf.ts @@ -1,13 +1,13 @@ import { Inject, Service } from 'typedi'; +import { renderPaymentReceivedPaperTemplateHtml } from '@bigcapital/pdf-templates'; import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy'; -import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable'; import { GetPaymentReceived } from './GetPaymentReceived'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import { PaymentReceivedBrandingTemplate } from './PaymentReceivedBrandingTemplate'; import { transformPaymentReceivedToPdfTemplate } from './utils'; import { PaymentReceivedPdfTemplateAttributes } from '@/interfaces'; -import events from '@/subscribers/events'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import events from '@/subscribers/events'; @Service() export default class GetPaymentReceivedPdf { @@ -17,9 +17,6 @@ export default class GetPaymentReceivedPdf { @Inject() private chromiumlyTenancy: ChromiumlyTenancy; - @Inject() - private templateInjectable: TemplateInjectable; - @Inject() private getPaymentService: GetPaymentReceived; @@ -29,6 +26,23 @@ export default class GetPaymentReceivedPdf { @Inject() private eventPublisher: EventPublisher; + /** + * Retrieves payment received html content. + * @param {number} tenantId + * @param {number} paymentReceivedId + * @returns {Promise} + */ + public async getPaymentReceivedHtml( + tenantId: number, + paymentReceivedId: number + ): Promise { + const brandingAttributes = await this.getPaymentBrandingAttributes( + tenantId, + paymentReceivedId + ); + return renderPaymentReceivedPaperTemplateHtml(brandingAttributes); + } + /** * Retrieve sale invoice pdf content. * @param {number} tenantId - @@ -39,15 +53,10 @@ export default class GetPaymentReceivedPdf { tenantId: number, paymentReceivedId: number ): Promise<[Buffer, string]> { - const brandingAttributes = await this.getPaymentBrandingAttributes( + const htmlContent = await this.getPaymentReceivedHtml( tenantId, paymentReceivedId ); - const htmlContent = await this.templateInjectable.render( - tenantId, - 'modules/payment-receive-standard', - brandingAttributes - ); const filename = await this.getPaymentReceivedFilename( tenantId, paymentReceivedId diff --git a/packages/server/src/services/Sales/PaymentReceived/PaymentReceivedApplication.ts b/packages/server/src/services/Sales/PaymentReceived/PaymentReceivedApplication.ts index f88e784dd..95edd334b 100644 --- a/packages/server/src/services/Sales/PaymentReceived/PaymentReceivedApplication.ts +++ b/packages/server/src/services/Sales/PaymentReceived/PaymentReceivedApplication.ts @@ -228,6 +228,22 @@ export class PaymentReceivesApplication { ); }; + /** + * Retrieves the given payment receive html document. + * @param {number} tenantId + * @param {number} paymentReceiveId + * @returns {Promise} + */ + public getPaymentReceivedHtml = ( + tenantId: number, + paymentReceiveId: number + ) => { + return this.getPaymentReceivePdfService.getPaymentReceivedHtml( + tenantId, + paymentReceiveId + ); + }; + /** * Retrieves the create/edit initial state of the payment received. * @param {number} tenantId - The ID of the tenant. diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts index b68f195f0..5d8a134b8 100644 --- a/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts @@ -152,6 +152,19 @@ export class SaleReceiptApplication { ); } + /** + * Retrieves the given sale receipt html. + * @param {number} tenantId + * @param {number} saleReceiptId + * @returns {Promise} + */ + public getSaleReceiptHtml(tenantId: number, saleReceiptId: number) { + return this.getSaleReceiptPdfService.saleReceiptHtml( + tenantId, + saleReceiptId + ); + } + /** * Notify receipt customer by SMS of the given sale receipt. * @param {number} tenantId diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts index 9c9c6132b..3c4e42060 100644 --- a/packages/server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts @@ -8,6 +8,7 @@ import { transformReceiptToBrandingTemplateAttributes } from './utils'; import { ISaleReceiptBrandingTemplateAttributes } from '@/interfaces'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import events from '@/subscribers/events'; +import { renderReceiptPaperTemplateHtml } from '@bigcapital/pdf-templates'; @Service() export class SaleReceiptsPdf { @@ -29,6 +30,19 @@ export class SaleReceiptsPdf { @Inject() private eventPublisher: EventPublisher; + /** + * Retrieves sale receipt html content. + * @param {number} tennatId + * @param {number} saleReceiptId + */ + public async saleReceiptHtml(tennatId: number, saleReceiptId: number) { + const brandingAttributes = await this.getReceiptBrandingAttributes( + tennatId, + saleReceiptId + ); + return renderReceiptPaperTemplateHtml(brandingAttributes); + } + /** * Retrieves sale invoice pdf content. * @param {number} tenantId - @@ -41,16 +55,9 @@ export class SaleReceiptsPdf { ): Promise<[Buffer, string]> { const filename = await this.getSaleReceiptFilename(tenantId, saleReceiptId); - const brandingAttributes = await this.getReceiptBrandingAttributes( - tenantId, - saleReceiptId - ); // Converts the receipt template to html content. - const htmlContent = await this.templateInjectable.render( - tenantId, - 'modules/receipt-regular', - brandingAttributes - ); + const htmlContent = await this.saleReceiptHtml(tenantId, saleReceiptId); + // Renders the html content to pdf document. const content = await this.chromiumlyTenancy.convertHtmlContent( tenantId, diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMail.schema.ts b/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMail.schema.ts new file mode 100644 index 000000000..02134112c --- /dev/null +++ b/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMail.schema.ts @@ -0,0 +1,3 @@ +import * as Yup from 'yup'; + +export const EstimateSendMailSchema = Yup.object().shape({}); diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailBoot.tsx b/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailBoot.tsx new file mode 100644 index 000000000..5c1072ee5 --- /dev/null +++ b/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailBoot.tsx @@ -0,0 +1,52 @@ +import React, { createContext, useContext } from 'react'; +import { Spinner } from '@blueprintjs/core'; +import { useDrawerContext } from '@/components/Drawer/DrawerProvider'; + +interface EstimateSendMailBootValues { + estimateId: number; + + estimateMailState: GetSaleEstimateDefaultOptionsResponse | undefined; + isEstimateMailState: boolean; +} +interface EstimateSendMailBootProps { + children: React.ReactNode; +} + +const EstimateSendMailContentBootContext = + createContext({} as EstimateSendMailBootValues); + +export const EstimateSendMailBoot = ({ children }: EstimateSendMailBootProps) => { + const { + payload: { estimateId }, + } = useDrawerContext(); + + // Estimate mail options. + const { data: estimateMailState, isLoading: isEstimateMailState } = + useSaleEstimateMailState(estimateId); + + const isLoading = isEstimateMailState; + + if (isLoading) { + return ; + } + const value = { + estimateId, + + // # Estimate mail options + isEstimateMailState, + estimateMailState, + }; + + return ( + + {children} + + ); +}; +EstimateSendMailBoot.displayName = 'EstimateSendMailBoot'; + +export const useEstimateSendMailBoot = () => { + return useContext( + EstimateSendMailContentBootContext, + ); +}; diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailContent.tsx b/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailContent.tsx new file mode 100644 index 000000000..ae30ab402 --- /dev/null +++ b/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailContent.tsx @@ -0,0 +1,9 @@ + + +export function EstimateSendMailContent() { + + return ( + + + ); +} \ No newline at end of file diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailDrawer.tsx b/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailDrawer.tsx new file mode 100644 index 000000000..752d6b81f --- /dev/null +++ b/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailDrawer.tsx @@ -0,0 +1,42 @@ +// @ts-nocheck +import React from 'react'; +import * as R from 'ramda'; +import { Drawer, DrawerSuspense } from '@/components'; +import withDrawers from '@/containers/Drawer/withDrawers'; + +const EstimateSendMailDrawerProps = React.lazy(() => + import('./EstimateSendMailContent').then((module) => ({ + default: module.InvoiceSendMailContent, + })), +); + +interface EstimateSendMailDrawerProps { + name: string; + isOpen?: boolean; + payload?: any; +} + +function EstimateSendMailDrawerRoot({ + name, + + // #withDrawer + isOpen, + payload, +}: EstimateSendMailDrawerProps) { + return ( + + + + + + ); +} + +export const EstimateSendMailDrawer = R.compose(withDrawers())( + EstimateSendMailDrawerRoot, +); diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailForm.tsx b/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailForm.tsx new file mode 100644 index 000000000..545fc9405 --- /dev/null +++ b/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailForm.tsx @@ -0,0 +1,79 @@ +import { Form, Formik, FormikHelpers } from 'formik'; +import { css } from '@emotion/css'; +import { Intent } from '@blueprintjs/core'; +import { InvoiceSendMailFormValues } from './_types'; +import { InvoiceSendMailFormSchema } from './InvoiceSendMailForm.schema'; +import { useSendSaleInvoiceMail } from '@/hooks/query'; +import { AppToaster } from '@/components'; +import { useInvoiceSendMailBoot } from './InvoiceSendMailContentBoot'; +import { useDrawerActions } from '@/hooks/state'; +import { useDrawerContext } from '@/components/Drawer/DrawerProvider'; +import { transformToForm } from '@/utils'; +import { useEstimateSendMailBoot } from './EstimateSendMailBoot'; + +const initialValues: InvoiceSendMailFormValues = { + subject: '', + message: '', + to: [], + cc: [], + bcc: [], + attachPdf: true, +}; + +interface InvoiceSendMailFormProps { + children: React.ReactNode; +} + +export function EstimateSendMailForm({ children }: InvoiceSendMailFormProps) { + const { mutateAsync: sendInvoiceMail } = useSendSaleInvoiceMail(); + const { estimateId, estimateMailState } = useEstimateSendMailBoot(); + + const { name } = useDrawerContext(); + const { closeDrawer } = useDrawerActions(); + + const _initialValues: InvoiceSendMailFormValues = { + ...initialValues, + ...transformToForm(invoiceMailState, initialValues), + }; + const handleSubmit = ( + values: InvoiceSendMailFormValues, + { setSubmitting }: FormikHelpers, + ) => { + setSubmitting(true); + sendInvoiceMail({ id: invoiceId, values: { ...values } }) + .then(() => { + AppToaster.show({ + message: 'The invoice mail has been sent to the customer.', + intent: Intent.SUCCESS, + }); + setSubmitting(false); + closeDrawer(name); + }) + .catch((error) => { + setSubmitting(false); + AppToaster.show({ + message: 'Something went wrong!', + intent: Intent.SUCCESS, + }); + }); + }; + + return ( + +
+ {children} +
+
+ ); +} diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/_interfaces.ts b/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/_interfaces.ts new file mode 100644 index 000000000..b302170db --- /dev/null +++ b/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/_interfaces.ts @@ -0,0 +1 @@ +export interface EstimateSendMailFormValues {} diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/index.ts b/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimatesAlerts.tsx b/packages/webapp/src/containers/Sales/Estimates/EstimatesAlerts.tsx index d5931793c..730ad3043 100644 --- a/packages/webapp/src/containers/Sales/Estimates/EstimatesAlerts.tsx +++ b/packages/webapp/src/containers/Sales/Estimates/EstimatesAlerts.tsx @@ -18,20 +18,8 @@ const EstimateRejectAlert = React.lazy( * Estimates alert. */ export default [ - { - name: 'estimate-delete', - component: EstimateDeleteAlert, - }, - { - name: 'estimate-deliver', - component: EstimateDeliveredAlert, - }, - { - name: 'estimate-Approve', - component: EstimateApproveAlert, - }, - { - name: 'estimate-reject', - component: EstimateRejectAlert, - }, + { name: 'estimate-delete', component: EstimateDeleteAlert }, + { name: 'estimate-deliver', component: EstimateDeliveredAlert }, + { name: 'estimate-Approve', component: EstimateApproveAlert }, + { name: 'estimate-reject', component: EstimateRejectAlert }, ]; diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailHeader.tsx b/packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewHeader.tsx similarity index 91% rename from packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailHeader.tsx rename to packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewHeader.tsx index 25d3ab008..98a9c6584 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailHeader.tsx +++ b/packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewHeader.tsx @@ -4,16 +4,16 @@ import { Group, Icon } from '@/components'; import { useDrawerContext } from '@/components/Drawer/DrawerProvider'; import { useDrawerActions } from '@/hooks/state'; -interface ElementCustomizeHeaderProps { +interface SendMailViewHeaderProps { label?: string; children?: React.ReactNode; closeButton?: boolean; } -export function InvoiceSendMailHeader({ +export function SendMailViewHeader({ label, closeButton = true, -}: ElementCustomizeHeaderProps) { +}: SendMailViewHeaderProps) { const { name } = useDrawerContext(); const { closeDrawer } = useDrawerActions(); diff --git a/packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewMessageField.tsx b/packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewMessageField.tsx new file mode 100644 index 000000000..f5226d427 --- /dev/null +++ b/packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewMessageField.tsx @@ -0,0 +1,109 @@ +// @ts-nocheck +import { useFormikContext } from 'formik'; +import { Button, Icon, Position } from '@blueprintjs/core'; +import { SelectOptionProps } from '@blueprintjs-formik/select'; +import { FormGroupProps, TextAreaProps } from '@blueprintjs-formik/core'; +import { css } from '@emotion/css'; +import { FFormGroup, FSelect, FTextArea, Group, Stack } from '@/components'; +import { useCallback, useRef } from 'react'; +import { InvoiceSendMailFormValues } from '../../Invoices/InvoiceSendMailDrawer/_types'; + + +interface SendMailViewMessageFieldProps { + argsOptions?: Array; + formGroupProps?: Partial; + selectProps?: Partial; + textareaProps?: Partial; +} + +export function SendMailViewMessageField({ + argsOptions, + formGroupProps, + textareaProps, +}: SendMailViewMessageFieldProps) { + const textareaRef = useRef(null); + const { setFieldValue } = useFormikContext(); + + const handleTextareaChange = useCallback( + (item: SelectOptionProps) => { + const textarea = textareaRef.current; + if (!textarea) return; + + const { selectionStart, selectionEnd, value: text } = textarea; + const insertText = `{${item.value}}`; + const message = + text.substring(0, selectionStart) + + insertText + + text.substring(selectionEnd); + + setFieldValue('message', message); + + // Move the cursor to the end of the inserted text + setTimeout(() => { + textarea.selectionStart = textarea.selectionEnd = + selectionStart + insertText.length; + textarea.focus(); + }, 0); + }, + [setFieldValue], + ); + + const handleTagInputKeyDown = (e: React.KeyboardEvent) => { + // Prevent the form from submitting when the user presses the Enter key + if (e.key === 'Enter') { + e.preventDefault(); + } + }; + + return ( + + + + ( + + )} + fill={false} + fastField + /> + + + + + + ); +} diff --git a/packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewPreview.tsx b/packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewPreview.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewPreviewHeader.tsx b/packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewPreviewHeader.tsx new file mode 100644 index 000000000..c33c9fb68 --- /dev/null +++ b/packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewPreviewHeader.tsx @@ -0,0 +1,76 @@ +import { useMemo } from 'react'; +import { x } from '@xstyled/emotion'; +import { Box, Group, Stack } from '@/components'; + +interface SendViewPreviewHeaderProps { + companyName?: string; + customerName?: string; + subject: string; + from?: Array; + to?: Array; +} + +export function SendViewPreviewHeader({ + companyName, + subject, + customerName, + from, + to, +}: SendViewPreviewHeaderProps) { + const formatedFromAddresses = useMemo( + () => formatAddresses(from || []), + [from], + ); + const formattedToAddresses = useMemo(() => formatAddresses(to || []), [to]); + + return ( + + + + {subject} + + + + + + + A + + + + + {companyName} + {formatedFromAddresses} + + + + Send to: {customerName} {formattedToAddresses}; + + + + + + ); +} + +const formatAddresses = (addresses: Array) => + addresses?.map((email) => '<' + email + '>').join(' '); \ No newline at end of file diff --git a/packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewPreviewPdfIframe.tsx b/packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewPreviewPdfIframe.tsx new file mode 100644 index 000000000..441d46e10 --- /dev/null +++ b/packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewPreviewPdfIframe.tsx @@ -0,0 +1,27 @@ +import { css } from '@emotion/css'; +import clsx from 'classnames'; + +interface SendMailViewPreviewPdfIframeProps + extends React.IframeHTMLAttributes {} + +export const SendMailViewPreviewPdfIframe = ({ + ...props +}: SendMailViewPreviewPdfIframeProps) => { + return ( +