mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 22:30:31 +00:00
Merge pull request #723 from bigcapitalhq/invoice-mail-receipt
feat: wip Invoice mail receipt preview
This commit is contained in:
@@ -45,7 +45,6 @@ import ProjectBillableEntriesFormDialog from '@/containers/Projects/containers/P
|
||||
import TaxRateFormDialog from '@/containers/TaxRates/dialogs/TaxRateFormDialog/TaxRateFormDialog';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
import InvoiceExchangeRateChangeDialog from '@/containers/Sales/Invoices/InvoiceForm/Dialogs/InvoiceExchangeRateChangeDialog';
|
||||
import InvoiceMailDialog from '@/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialog';
|
||||
import EstimateMailDialog from '@/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialog';
|
||||
import ReceiptMailDialog from '@/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialog';
|
||||
import PaymentMailDialog from '@/containers/Sales/PaymentsReceived/PaymentMailDialog/PaymentMailDialog';
|
||||
@@ -144,7 +143,6 @@ export default function DialogsContainer() {
|
||||
<InvoiceExchangeRateChangeDialog
|
||||
dialogName={DialogsName.InvoiceExchangeRateChangeNotice}
|
||||
/>
|
||||
<InvoiceMailDialog dialogName={DialogsName.InvoiceMail} />
|
||||
<EstimateMailDialog dialogName={DialogsName.EstimateMail} />
|
||||
<ReceiptMailDialog dialogName={DialogsName.ReceiptMail} />
|
||||
<PaymentMailDialog dialogName={DialogsName.PaymentMail} />
|
||||
|
||||
@@ -31,6 +31,7 @@ import { PaymentReceivedCustomizeDrawer } from '@/containers/Sales/PaymentsRecei
|
||||
import { BrandingTemplatesDrawer } from '@/containers/BrandingTemplates/BrandingTemplatesDrawer';
|
||||
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
import { InvoiceSendMailDrawer } from '@/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailDrawer';
|
||||
|
||||
/**
|
||||
* Drawers container of the dashboard.
|
||||
@@ -79,6 +80,7 @@ export default function DrawersContainer() {
|
||||
name={DRAWERS.PAYMENT_RECEIVED_CUSTOMIZE}
|
||||
/>
|
||||
<BrandingTemplatesDrawer name={DRAWERS.BRANDING_TEMPLATES} />
|
||||
<InvoiceSendMailDrawer name={DRAWERS.INVOICE_SEND_MAIL} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,5 +33,6 @@ export enum DRAWERS {
|
||||
PAYMENT_RECEIVED_CUSTOMIZE = 'PAYMENT_RECEIVED_CUSTOMIZE',
|
||||
BRANDING_TEMPLATES = 'BRANDING_TEMPLATES',
|
||||
PAYMENT_INVOICE_PREVIEW = 'PAYMENT_INVOICE_PREVIEW',
|
||||
STRIPE_PAYMENT_INTEGRATION_EDIT = 'STRIPE_PAYMENT_INTEGRATION_EDIT'
|
||||
STRIPE_PAYMENT_INTEGRATION_EDIT = 'STRIPE_PAYMENT_INTEGRATION_EDIT',
|
||||
INVOICE_SEND_MAIL = 'INVOICE_SEND_MAIL'
|
||||
}
|
||||
|
||||
@@ -23,8 +23,6 @@ function BrandingTemplatesDrawerRoot({
|
||||
isOpen={isOpen}
|
||||
name={name}
|
||||
payload={payload}
|
||||
size={'600px'}
|
||||
style={{ borderLeftColor: '#cbcbcb' }}
|
||||
>
|
||||
<DrawerSuspense>
|
||||
<BrandingTemplatesContent />
|
||||
|
||||
@@ -37,8 +37,10 @@ export function ElementCustomizeFieldsMain() {
|
||||
<Stack spacing={0} className={styles.mainFields}>
|
||||
<ElementCustomizeHeader label={'Customize'} />
|
||||
|
||||
<Stack spacing={0} style={{ flex: '1 1 auto', overflow: 'auto' }}>
|
||||
<Box style={{ flex: '1 1' }}>{CustomizeTabPanel}</Box>
|
||||
<Stack spacing={0} flex="1 1 auto" overflow="auto">
|
||||
<Box flex={'1 1'} overflow="auto">
|
||||
{CustomizeTabPanel}
|
||||
</Box>
|
||||
<ElementCustomizeFooterActions />
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import { Box } from '@/components';
|
||||
import { Box, Stack } from '@/components';
|
||||
import { useElementCustomizeContext } from './ElementCustomizeProvider';
|
||||
|
||||
export function ElementCustomizePreviewContent() {
|
||||
const { PaperTemplate } = useElementCustomizeContext();
|
||||
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
padding: '28px 24px 40px',
|
||||
backgroundColor: '#F5F5F5',
|
||||
overflow: 'auto',
|
||||
flex: '1',
|
||||
}}
|
||||
>
|
||||
<Stack backgroundColor="#F5F5F5" overflow="auto" flex="1 1 0%" spacing={0}>
|
||||
{PaperTemplate}
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { InvoicePaymentPage, PaymentPageProps } from './PaymentPage';
|
||||
|
||||
export interface InvoicePaymentPagePreviewProps
|
||||
extends Partial<PaymentPageProps> { }
|
||||
|
||||
export function InvoicePaymentPagePreview(
|
||||
props: InvoicePaymentPagePreviewProps,
|
||||
) {
|
||||
return (
|
||||
<InvoicePaymentPage
|
||||
paidAmount={'$1,000.00'}
|
||||
dueDate={'20 Sep 2024'}
|
||||
total={'$1,000.00'}
|
||||
subtotal={'$1,000.00'}
|
||||
dueAmount={'$1,000.00'}
|
||||
customerName={'Ahmed Bouhuolia'}
|
||||
organizationName={'Bigcapital Technology, Inc.'}
|
||||
invoiceNumber={'INV-000001'}
|
||||
companyLogoUri={' '}
|
||||
organizationAddress={' '}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
260
packages/webapp/src/containers/PaymentPortal/PaymentPage.tsx
Normal file
260
packages/webapp/src/containers/PaymentPortal/PaymentPage.tsx
Normal file
@@ -0,0 +1,260 @@
|
||||
import { Text, Classes, Button, Intent, ButtonProps } from '@blueprintjs/core';
|
||||
import clsx from 'classnames';
|
||||
import { css } from '@emotion/css';
|
||||
import { lighten } from 'polished';
|
||||
import { Box, Group, Stack } from '@/components';
|
||||
import styles from './PaymentPortal.module.scss';
|
||||
|
||||
export interface PaymentPageProps {
|
||||
// # Company name
|
||||
companyLogoUri: string;
|
||||
organizationName: string;
|
||||
organizationAddress: string;
|
||||
|
||||
// # Colors
|
||||
primaryColor?: string;
|
||||
|
||||
// # Customer name
|
||||
customerName: string;
|
||||
customerAddress?: string;
|
||||
|
||||
// # Subtotal
|
||||
subtotal: string;
|
||||
subtotalLabel?: string;
|
||||
|
||||
// # Total
|
||||
total: string;
|
||||
totalLabel?: string;
|
||||
|
||||
// # Due date
|
||||
dueDate: string;
|
||||
|
||||
// # Paid amount
|
||||
paidAmount: string;
|
||||
paidAmountLabel?: string;
|
||||
|
||||
// # Due amount
|
||||
dueAmount: string;
|
||||
dueAmountLabel?: string;
|
||||
|
||||
// # Download invoice button
|
||||
downloadInvoiceBtnLabel?: string;
|
||||
downloadInvoiceButtonProps?: Partial<ButtonProps>;
|
||||
|
||||
// # View invoice button
|
||||
viewInvoiceLabel?: string;
|
||||
viewInvoiceButtonProps?: Partial<ButtonProps>;
|
||||
|
||||
// # Invoice number
|
||||
invoiceNumber: string;
|
||||
invoiceNumberLabel?: string;
|
||||
|
||||
// # Pay button
|
||||
showPayButton?: boolean;
|
||||
payButtonLabel?: string;
|
||||
payInvoiceButtonProps?: Partial<ButtonProps>;
|
||||
|
||||
// # Buy note
|
||||
buyNote?: string;
|
||||
|
||||
// # Copyright
|
||||
copyrightText?: string;
|
||||
|
||||
classNames?: Record<string, string>
|
||||
}
|
||||
|
||||
export function InvoicePaymentPage({
|
||||
// # Company
|
||||
companyLogoUri,
|
||||
organizationName,
|
||||
organizationAddress,
|
||||
|
||||
// # Colors
|
||||
primaryColor = 'rgb(0, 82, 204)',
|
||||
|
||||
// # Customer
|
||||
customerName,
|
||||
customerAddress,
|
||||
|
||||
// # Subtotal
|
||||
subtotal,
|
||||
subtotalLabel = 'Subtotal',
|
||||
|
||||
// # Total
|
||||
total,
|
||||
totalLabel = 'Total',
|
||||
|
||||
// # Due date
|
||||
dueDate,
|
||||
|
||||
// # Paid amount
|
||||
paidAmount,
|
||||
paidAmountLabel = 'Paid Amount (-)',
|
||||
|
||||
// # Invoice number
|
||||
invoiceNumber,
|
||||
invoiceNumberLabel = 'Invoice #',
|
||||
|
||||
// # Download invoice button
|
||||
downloadInvoiceBtnLabel = 'Download Invoice',
|
||||
downloadInvoiceButtonProps,
|
||||
|
||||
// # View invoice button
|
||||
viewInvoiceLabel = 'View Invoice',
|
||||
viewInvoiceButtonProps,
|
||||
|
||||
// # Due amount
|
||||
dueAmount,
|
||||
dueAmountLabel = 'Due Amount',
|
||||
|
||||
// # Pay button
|
||||
showPayButton = true,
|
||||
payButtonLabel = 'Pay {total}',
|
||||
payInvoiceButtonProps,
|
||||
|
||||
// # Buy note
|
||||
buyNote = 'By confirming your payment, you allow Bigcapital Technology, Inc. to charge you for this payment and save your payment information in accordance with their terms.',
|
||||
|
||||
// # Copyright
|
||||
copyrightText = `© 2024 Bigcapital Technology, Inc. <br /> All rights reserved.`,
|
||||
|
||||
classNames,
|
||||
}: PaymentPageProps) {
|
||||
return (
|
||||
<Box className={clsx(styles.root, classNames?.root)}>
|
||||
<Stack spacing={0} className={styles.body}>
|
||||
<Stack>
|
||||
<Group spacing={10}>
|
||||
{companyLogoUri && (
|
||||
<Box
|
||||
className={styles.companyLogoWrap}
|
||||
style={{
|
||||
backgroundImage: `url(${companyLogoUri})`,
|
||||
}}
|
||||
></Box>
|
||||
)}
|
||||
<Text>{organizationName}</Text>
|
||||
</Group>
|
||||
|
||||
<Stack spacing={6}>
|
||||
<h1 className={clsx(styles.bigTitle, classNames?.bigTitle)}>
|
||||
{organizationName} Sent an Invoice for {total}
|
||||
</h1>
|
||||
<Group spacing={10}>
|
||||
<Text className={clsx(Classes.TEXT_MUTED, styles.invoiceDueDate)}>
|
||||
Invoice due {dueDate}{' '}
|
||||
</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
<Stack className={styles.address} spacing={2}>
|
||||
<Box className={styles.customerName}>{customerName}</Box>
|
||||
|
||||
{customerAddress && (
|
||||
<Box dangerouslySetInnerHTML={{ __html: customerAddress }} />
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<h2 className={styles.invoiceNumber}>
|
||||
{invoiceNumberLabel} {invoiceNumber}
|
||||
</h2>
|
||||
|
||||
<Stack spacing={0} className={styles.totals}>
|
||||
<Group
|
||||
position={'apart'}
|
||||
className={clsx(styles.totalItem, styles.borderBottomGray)}
|
||||
>
|
||||
<Text>{subtotalLabel}</Text>
|
||||
<Text>{subtotal}</Text>
|
||||
</Group>
|
||||
|
||||
<Group position={'apart'} className={styles.totalItem}>
|
||||
<Text>{totalLabel}</Text>
|
||||
<Text style={{ fontWeight: 500 }}>{total}</Text>
|
||||
</Group>
|
||||
{/*
|
||||
{sharableLinkMeta?.taxes?.map((tax, key) => (
|
||||
<Group key={key} position={'apart'} className={styles.totalItem}>
|
||||
<Text>{tax?.name}</Text>
|
||||
<Text>{tax?.taxRateAmountFormatted}</Text>
|
||||
</Group>
|
||||
))} */}
|
||||
<Group
|
||||
position={'apart'}
|
||||
className={clsx(styles.totalItem, styles.borderBottomGray)}
|
||||
>
|
||||
<Text>{paidAmountLabel}</Text>
|
||||
<Text>{paidAmount}</Text>
|
||||
</Group>
|
||||
|
||||
<Group
|
||||
position={'apart'}
|
||||
className={clsx(styles.totalItem, styles.borderBottomDark)}
|
||||
>
|
||||
<Text>{dueAmountLabel}</Text>
|
||||
<Text style={{ fontWeight: 500 }}>{dueAmount}</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Stack spacing={8} className={styles.footerButtons}>
|
||||
<Button
|
||||
minimal
|
||||
className={clsx(styles.footerButton, styles.downloadInvoiceButton)}
|
||||
{...downloadInvoiceButtonProps}
|
||||
>
|
||||
{downloadInvoiceBtnLabel}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className={clsx(styles.footerButton, styles.viewInvoiceButton)}
|
||||
{...viewInvoiceButtonProps}
|
||||
>
|
||||
{viewInvoiceLabel}
|
||||
</Button>
|
||||
|
||||
{showPayButton && (
|
||||
<Button
|
||||
intent={Intent.PRIMARY}
|
||||
className={clsx(
|
||||
styles.footerButton,
|
||||
styles.buyButton,
|
||||
css`
|
||||
&.bp4-intent-primary {
|
||||
background-color: ${primaryColor};
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: ${lighten(0.1, primaryColor)};
|
||||
}
|
||||
}
|
||||
`,
|
||||
)}
|
||||
{...payInvoiceButtonProps}
|
||||
>
|
||||
{payButtonLabel.replace('{total}', total)}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{buyNote && (
|
||||
<Text className={clsx(Classes.TEXT_MUTED, styles.buyNote)}>
|
||||
{buyNote}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Stack spacing={18} className={styles.footer}>
|
||||
<Box dangerouslySetInnerHTML={{ __html: organizationAddress }}></Box>
|
||||
|
||||
{copyrightText && (
|
||||
<Stack
|
||||
spacing={0}
|
||||
className={styles.footerText}
|
||||
dangerouslySetInnerHTML={{ __html: copyrightText }}
|
||||
></Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
|
||||
width: 600px;
|
||||
margin: 40px auto;
|
||||
// margin: 40px auto;
|
||||
color: #222;
|
||||
background-color: #fff;
|
||||
}
|
||||
@@ -28,6 +28,7 @@
|
||||
font-weight: 500;
|
||||
color: #222;
|
||||
font-size: 26px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.invoiceDueDate{
|
||||
|
||||
@@ -29,7 +29,13 @@ export const useStripeIntegrationEditBoot = () => {
|
||||
return context;
|
||||
};
|
||||
|
||||
export const StripeIntegrationEditBoot: React.FC = ({ children }) => {
|
||||
interface StripeIntegrationEditBootProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const StripeIntegrationEditBoot: React.FC<
|
||||
StripeIntegrationEditBootProps
|
||||
> = ({ children }) => {
|
||||
const {
|
||||
payload: { stripePaymentMethodId },
|
||||
} = useDrawerContext();
|
||||
|
||||
@@ -19,7 +19,12 @@ function CreditNoteCustomizeDrawerRoot({
|
||||
payload,
|
||||
}) {
|
||||
return (
|
||||
<Drawer isOpen={isOpen} name={name} payload={payload} size={'100%'}>
|
||||
<Drawer
|
||||
isOpen={isOpen}
|
||||
name={name}
|
||||
payload={payload}
|
||||
size={'calc(100% - 10px)'}
|
||||
>
|
||||
<DrawerSuspense>
|
||||
<CreditNoteCustomizeDrawerBody />
|
||||
</DrawerSuspense>
|
||||
|
||||
@@ -20,7 +20,12 @@ function EstimateCustomizeDrawerRoot({
|
||||
payload,
|
||||
}) {
|
||||
return (
|
||||
<Drawer isOpen={isOpen} name={name} payload={payload} size={'100%'}>
|
||||
<Drawer
|
||||
isOpen={isOpen}
|
||||
name={name}
|
||||
payload={payload}
|
||||
size={'calc(100% - 10px)'}
|
||||
>
|
||||
<DrawerSuspense>
|
||||
<EstimateCustomizeDrawerBody />
|
||||
</DrawerSuspense>
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import React from 'react';
|
||||
import * as R from 'ramda';
|
||||
import { useFormikContext } from 'formik';
|
||||
import {
|
||||
InvoicePaperTemplate,
|
||||
InvoicePaperTemplateProps,
|
||||
} from './InvoicePaperTemplate';
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { Spinner, Tab } from '@blueprintjs/core';
|
||||
import {
|
||||
ElementCustomize,
|
||||
ElementCustomizeContent,
|
||||
@@ -16,9 +11,27 @@ import { InvoiceCustomizeSchema } from './InvoiceCustomizeForm.schema';
|
||||
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||
import { useDrawerActions } from '@/hooks/state';
|
||||
import { BrandingTemplateForm } from '@/containers/BrandingTemplates/BrandingTemplateForm';
|
||||
import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider';
|
||||
import { initialValues } from './constants';
|
||||
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
|
||||
import { InvoiceCustomizeTabs } from './InvoiceCustomizeTabs';
|
||||
|
||||
const InvoiceCustomizePaymentPreview = lazy(() =>
|
||||
import('./InvoiceCustomizePaymentPreview').then((module) => ({
|
||||
default: module.InvoiceCustomizePaymentPreview,
|
||||
})),
|
||||
);
|
||||
|
||||
const InvoiceCustomizeMailReceiptPreview = lazy(() =>
|
||||
import('./InvoiceCustomizeMailReceiptPreview').then((module) => ({
|
||||
default: module.InvoiceCustomizeMailReceiptPreview,
|
||||
})),
|
||||
);
|
||||
|
||||
const InvoiceCustomizePdfPreview = lazy(() =>
|
||||
import('./InvoiceCustomizePdfPreview').then((module) => ({
|
||||
default: module.InvoiceCustomizePdfPreview,
|
||||
})),
|
||||
);
|
||||
|
||||
/**
|
||||
* Invoice branding template customize.
|
||||
@@ -56,7 +69,39 @@ function InvoiceCustomizeFormContent() {
|
||||
return (
|
||||
<ElementCustomizeContent>
|
||||
<ElementCustomize.PaperTemplate>
|
||||
<InvoicePaperTemplateFormConnected />
|
||||
<InvoiceCustomizeTabs
|
||||
defaultSelectedTabId={'pdf-document'}
|
||||
id={'customize-preview-tabs'}
|
||||
renderActiveTabPanelOnly
|
||||
>
|
||||
<Tab
|
||||
id="pdf-document"
|
||||
title={'PDF document'}
|
||||
panel={
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<InvoiceCustomizePdfPreview />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Tab
|
||||
id={'payment-page'}
|
||||
title={'Payment page'}
|
||||
panel={
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<InvoiceCustomizePaymentPreview />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Tab
|
||||
id={'email-receipt'}
|
||||
title={'Email receipt'}
|
||||
panel={
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<InvoiceCustomizeMailReceiptPreview mx={'auto'} />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
</InvoiceCustomizeTabs>
|
||||
</ElementCustomize.PaperTemplate>
|
||||
|
||||
<ElementCustomize.FieldsTab id={'general'} label={'General'}>
|
||||
@@ -73,28 +118,3 @@ function InvoiceCustomizeFormContent() {
|
||||
</ElementCustomizeContent>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the `InvoicePaperTemplate` component props from the form and branding states.
|
||||
* @param Component
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const withInvoicePreviewTemplateProps = <P extends object>(
|
||||
Component: React.ComponentType<P>,
|
||||
) => {
|
||||
return (props: Omit<P, keyof InvoicePaperTemplateProps>) => {
|
||||
const { values } = useFormikContext<InvoiceCustomizeFormValues>();
|
||||
const { brandingState } = useElementCustomizeContext();
|
||||
|
||||
const mergedProps: InvoicePaperTemplateProps = {
|
||||
...brandingState,
|
||||
...values,
|
||||
};
|
||||
|
||||
return <Component {...(props as P)} {...mergedProps} />;
|
||||
};
|
||||
};
|
||||
|
||||
export const InvoicePaperTemplateFormConnected = R.compose(
|
||||
withInvoicePreviewTemplateProps,
|
||||
)(InvoicePaperTemplate);
|
||||
|
||||
@@ -17,7 +17,12 @@ function InvoiceCustomizeDrawerRoot({
|
||||
payload,
|
||||
}) {
|
||||
return (
|
||||
<Drawer isOpen={isOpen} name={name} size={'100%'} payload={payload}>
|
||||
<Drawer
|
||||
isOpen={isOpen}
|
||||
name={name}
|
||||
payload={payload}
|
||||
size={'calc(100% - 10px)'}
|
||||
>
|
||||
<DrawerSuspense>
|
||||
<InvoiceCustomize />
|
||||
</DrawerSuspense>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @ts-nocheck
|
||||
import { Classes, Text } from '@blueprintjs/core';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
FFormGroup,
|
||||
FieldRequiredHint,
|
||||
@@ -13,7 +14,6 @@ import { CreditCardIcon } from '@/icons/CreditCardIcon';
|
||||
import { Overlay } from './Overlay';
|
||||
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
|
||||
import { BrandingCompanyLogoUploadField } from '@/containers/ElementCustomize/components/BrandingCompanyLogoUploadField';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { MANAGE_LINK_URL } from './constants';
|
||||
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||
import { useDrawerActions } from '@/hooks/state';
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import * as R from 'ramda';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { InvoicePaymentPagePreviewProps } from '@/containers/PaymentPortal/InvoicePaymentPagePreview';
|
||||
import { InvoiceCustomizeFormValues } from './types';
|
||||
import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider';
|
||||
import { InvoiceMailReceiptPreview } from './InvoiceMailReceiptPreview';
|
||||
import { Box } from '@/components';
|
||||
|
||||
const withInvoiceMailReceiptPreviewConnected = <P extends Object>(
|
||||
Component: React.ComponentType<P>,
|
||||
) => {
|
||||
return (props: Omit<P, keyof InvoicePaymentPagePreviewProps>) => {
|
||||
const { values } = useFormikContext<InvoiceCustomizeFormValues>();
|
||||
const { brandingState } = useElementCustomizeContext();
|
||||
|
||||
const mergedBrandingState = {
|
||||
...brandingState,
|
||||
...values,
|
||||
};
|
||||
const mergedProps: InvoicePaymentPagePreviewProps = {
|
||||
companyLogoUri: mergedBrandingState?.companyLogoUri,
|
||||
primaryColor: mergedBrandingState?.primaryColor,
|
||||
// organizationAddress: mergedBrandingState,
|
||||
};
|
||||
return (
|
||||
<Box px={4} pt={8} pb={16}>
|
||||
<Component {...(props as P)} {...mergedProps} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export const InvoiceCustomizeMailReceiptPreview = R.compose(
|
||||
withInvoiceMailReceiptPreviewConnected,
|
||||
)(InvoiceMailReceiptPreview);
|
||||
@@ -0,0 +1,52 @@
|
||||
import * as R from 'ramda';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { css } from '@emotion/css';
|
||||
import {
|
||||
InvoicePaymentPagePreview,
|
||||
InvoicePaymentPagePreviewProps,
|
||||
} from '@/containers/PaymentPortal/InvoicePaymentPagePreview';
|
||||
import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider';
|
||||
import { InvoiceCustomizeFormValues } from './types';
|
||||
import { Box } from '@/components';
|
||||
|
||||
const withInvoicePaymentPreviewPageProps = <P extends Object>(
|
||||
Component: React.ComponentType<P>,
|
||||
) => {
|
||||
return (props: Omit<P, keyof InvoicePaymentPagePreviewProps>) => {
|
||||
const { values } = useFormikContext<InvoiceCustomizeFormValues>();
|
||||
const { brandingState } = useElementCustomizeContext();
|
||||
|
||||
const mergedBrandingState = {
|
||||
...brandingState,
|
||||
...values,
|
||||
};
|
||||
const mergedProps: InvoicePaymentPagePreviewProps = {
|
||||
companyLogoUri: mergedBrandingState?.companyLogoUri,
|
||||
primaryColor: mergedBrandingState?.primaryColor,
|
||||
};
|
||||
return (
|
||||
<Box px={4} pt={8} pb={16}>
|
||||
<Component
|
||||
{...(props as P)}
|
||||
{...mergedProps}
|
||||
classNames={{
|
||||
root: css`
|
||||
margin: 0 auto;
|
||||
border-radius: 5px !important;
|
||||
transform: scale(0.9);
|
||||
transform-origin: top;
|
||||
boxshadow: 0 10px 15px rgba(0, 0, 0, 0.05) !important;
|
||||
`,
|
||||
bigTitle: css`
|
||||
color: #333 !important;
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export const InvoiceCustomizePaymentPreview = R.compose(
|
||||
withInvoicePaymentPreviewPageProps,
|
||||
)(InvoicePaymentPagePreview);
|
||||
@@ -0,0 +1,37 @@
|
||||
import * as R from 'ramda';
|
||||
import { useFormikContext } from 'formik';
|
||||
import {
|
||||
InvoicePaperTemplate,
|
||||
InvoicePaperTemplateProps,
|
||||
} from './InvoicePaperTemplate';
|
||||
import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider';
|
||||
import { InvoiceCustomizeFormValues } from './types';
|
||||
import { Box } from '@/components';
|
||||
|
||||
/**
|
||||
* Injects the `InvoicePaperTemplate` component props from the form and branding states.
|
||||
* @param {React.ComponentType<P>} Component
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const withInvoicePreviewTemplateProps = <P extends object>(
|
||||
Component: React.ComponentType<P>,
|
||||
) => {
|
||||
return (props: Omit<P, keyof InvoicePaperTemplateProps>) => {
|
||||
const { values } = useFormikContext<InvoiceCustomizeFormValues>();
|
||||
const { brandingState } = useElementCustomizeContext();
|
||||
|
||||
const mergedProps: InvoicePaperTemplateProps = {
|
||||
...brandingState,
|
||||
...values,
|
||||
};
|
||||
return (
|
||||
<Box px={4} py={6}>
|
||||
<Component {...(props as P)} {...mergedProps} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export const InvoiceCustomizePdfPreview = R.compose(
|
||||
withInvoicePreviewTemplateProps,
|
||||
)(InvoicePaperTemplate);
|
||||
@@ -0,0 +1,37 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { Tabs, TabsProps } from '@blueprintjs/core';
|
||||
|
||||
interface InvoiceCustomizeTabsProps extends TabsProps {}
|
||||
|
||||
export function InvoiceCustomizeTabs(props: InvoiceCustomizeTabsProps) {
|
||||
return (
|
||||
<Tabs
|
||||
className={css`
|
||||
overflow: hidden;
|
||||
flex: 1 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.bp4-tab-list {
|
||||
padding: 0 20px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #dcdcdd;
|
||||
}
|
||||
.bp4-tab {
|
||||
line-height: 40px;
|
||||
}
|
||||
.bp4-tab:not([aria-selected='true']) {
|
||||
color: #5f6b7c;
|
||||
}
|
||||
.bp4-tab-indicator-wrapper .bp4-tab-indicator {
|
||||
height: 2px;
|
||||
}
|
||||
.bp4-tab-panel{
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
`}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
import { Button, Intent } from '@blueprintjs/core';
|
||||
import { css } from '@emotion/css';
|
||||
import { x } from '@xstyled/emotion';
|
||||
import { lighten } from 'polished';
|
||||
import { Group, Stack, StackProps } from '@/components';
|
||||
|
||||
export interface InvoiceMailReceiptProps extends StackProps {
|
||||
// # Company
|
||||
companyName: string;
|
||||
companyLogoUri?: string;
|
||||
|
||||
// # Colors
|
||||
primaryColor?: string;
|
||||
|
||||
// # Due date
|
||||
dueDate: string;
|
||||
dueDateLabel?: string;
|
||||
|
||||
// # Due amount
|
||||
dueAmountLabel?: string;
|
||||
dueAmount: string;
|
||||
|
||||
// # Total
|
||||
total: string;
|
||||
totalLabel?: string;
|
||||
|
||||
// # Invoice number
|
||||
invoiceNumber: string;
|
||||
invoiceNumberLabel?: string;
|
||||
|
||||
// # Mail message
|
||||
message: string;
|
||||
|
||||
// # Invoice items
|
||||
items?: Array<{ label: string; total: string; quantity: string | number }>;
|
||||
|
||||
// # View invoice button
|
||||
showViewInvoiceButton?: boolean;
|
||||
viewInvoiceButtonLabel?: string;
|
||||
viewInvoiceButtonOnClick?: () => void;
|
||||
}
|
||||
|
||||
export function InvoiceMailReceipt({
|
||||
// Company
|
||||
companyName,
|
||||
companyLogoUri,
|
||||
|
||||
// # Colors
|
||||
primaryColor = 'rgb(0, 82, 204)',
|
||||
|
||||
// Due date
|
||||
dueDate,
|
||||
dueDateLabel = 'Due',
|
||||
|
||||
// Due amount
|
||||
dueAmountLabel = 'Due Amount',
|
||||
dueAmount,
|
||||
|
||||
// Total
|
||||
total,
|
||||
totalLabel = 'Total',
|
||||
|
||||
// Invoice number
|
||||
invoiceNumber,
|
||||
invoiceNumberLabel = 'Invoice #',
|
||||
|
||||
// Invoice message
|
||||
message,
|
||||
|
||||
// Invoice items
|
||||
items,
|
||||
|
||||
// View invoice button
|
||||
showViewInvoiceButton = true,
|
||||
viewInvoiceButtonLabel = 'View Invoice',
|
||||
viewInvoiceButtonOnClick,
|
||||
...restProps
|
||||
}: InvoiceMailReceiptProps) {
|
||||
return (
|
||||
<Stack
|
||||
bg="white"
|
||||
w={'100%'}
|
||||
maxWidth={'500px'}
|
||||
p={'35px 25px'}
|
||||
borderRadius={'5px'}
|
||||
boxShadow={'0 10px 15px rgba(0, 0, 0, 0.05)'}
|
||||
color={'black'}
|
||||
{...restProps}
|
||||
>
|
||||
<Stack spacing={16} textAlign={'center'}>
|
||||
{companyLogoUri && (
|
||||
<x.div
|
||||
h="90px"
|
||||
w="90px"
|
||||
mx="auto"
|
||||
backgroundRepeat="no-repeat"
|
||||
backgroundPosition="center center"
|
||||
backgroundSize="contain"
|
||||
backgroundImage={`url("${companyLogoUri}")`}
|
||||
></x.div>
|
||||
)}
|
||||
<Stack spacing={8}>
|
||||
<x.h1 m={0} fontSize={'18px'} fontWeight={500} color="#404854">
|
||||
{companyName}
|
||||
</x.h1>
|
||||
|
||||
<x.h3 color="#383E47" fontWeight={500}>
|
||||
{total}
|
||||
</x.h3>
|
||||
|
||||
<x.span fontSize={'13px'} color="#404854">
|
||||
{invoiceNumberLabel} {invoiceNumber}
|
||||
</x.span>
|
||||
|
||||
<x.span fontSize={'13px'} color="#404854">
|
||||
{dueDateLabel} {dueDate}
|
||||
</x.span>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<x.p m={0} whiteSpace={'pre-line'} color="#252A31">
|
||||
{message}
|
||||
</x.p>
|
||||
|
||||
{showViewInvoiceButton && (
|
||||
<Button
|
||||
large
|
||||
intent={Intent.PRIMARY}
|
||||
className={css`
|
||||
&.bp4-intent-primary {
|
||||
background-color: ${primaryColor};
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: ${lighten(0.1, primaryColor)};
|
||||
}
|
||||
}
|
||||
&.bp4-large {
|
||||
min-height: 38px;
|
||||
}
|
||||
`}
|
||||
onClick={viewInvoiceButtonOnClick}
|
||||
>
|
||||
{viewInvoiceButtonLabel}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Stack spacing={0}>
|
||||
{items?.map((item, key) => (
|
||||
<Group
|
||||
key={key}
|
||||
h={'40px'}
|
||||
position={'apart'}
|
||||
borderBottomStyle="solid"
|
||||
borderBottomWidth={'1px'}
|
||||
borderBottomColor={'#D9D9D9'}
|
||||
borderTopStyle="solid"
|
||||
borderTopColor={'#D9D9D9'}
|
||||
borderTopWidth={'1px'}
|
||||
>
|
||||
<x.span>{item.label}</x.span>
|
||||
<x.span>
|
||||
{item.quantity} x {item.total}
|
||||
</x.span>
|
||||
</Group>
|
||||
))}
|
||||
|
||||
<Group
|
||||
h={'40px'}
|
||||
position={'apart'}
|
||||
borderBottomStyle="solid"
|
||||
borderBottomWidth={'1px'}
|
||||
borderColor={'#000'}
|
||||
>
|
||||
<x.span fontWeight={500}>{totalLabel}</x.span>
|
||||
<x.span fontWeight={600} fontSize={15}>
|
||||
{total}
|
||||
</x.span>
|
||||
</Group>
|
||||
|
||||
<Group
|
||||
h={'40px'}
|
||||
position={'apart'}
|
||||
borderBottomStyle="solid"
|
||||
borderBottomWidth={'1px'}
|
||||
borderBottomColor={'#000'}
|
||||
>
|
||||
<x.span fontWeight={500}>{dueAmountLabel}</x.span>
|
||||
<x.span fontWeight={600} fontSize={15}>
|
||||
{dueAmount}
|
||||
</x.span>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
InvoiceMailReceipt,
|
||||
InvoiceMailReceiptProps,
|
||||
} from './InvoiceMailReceipt';
|
||||
|
||||
export interface InvoiceMailReceiptPreviewProps
|
||||
extends Partial<InvoiceMailReceiptProps> {}
|
||||
|
||||
const receiptMessage = `Hi Ahmed,
|
||||
|
||||
Here’s invoice INV-0002 for AED 0.00
|
||||
|
||||
The amount outstanding of AED $100,00 is due on 2 October 2024
|
||||
|
||||
View your bill online From your online you can print a PDF or pay your outstanding bills,
|
||||
|
||||
If you have any questions, please let us know,
|
||||
|
||||
Thanks,
|
||||
Mohamed
|
||||
`;
|
||||
|
||||
export function InvoiceMailReceiptPreview(
|
||||
props: InvoiceMailReceiptPreviewProps,
|
||||
) {
|
||||
const propsWithDefaults = {
|
||||
message: receiptMessage,
|
||||
companyName: 'Bigcapital Technology, Inc.',
|
||||
total: '$1,000.00',
|
||||
invoiceNumber: 'INV-0001',
|
||||
dueDate: '2 Oct 2024',
|
||||
dueAmount: '$1,000.00',
|
||||
items: [{ label: 'Web development', total: '$1000.00', quantity: 1 }],
|
||||
companyLogoUri: ' ',
|
||||
...props,
|
||||
};
|
||||
return <InvoiceMailReceipt {...propsWithDefaults} />;
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
import { Classes, Text } from '@blueprintjs/core';
|
||||
import { PaperTemplate, PaperTemplateTotalBorder } from './PaperTemplate';
|
||||
import {
|
||||
PaperTemplate,
|
||||
PaperTemplateProps,
|
||||
PaperTemplateTotalBorder,
|
||||
} from './PaperTemplate';
|
||||
import { Box, Group, Stack } from '@/components';
|
||||
import {
|
||||
DefaultPdfTemplateTerms,
|
||||
@@ -23,7 +27,7 @@ interface PaperTax {
|
||||
amount: string;
|
||||
}
|
||||
|
||||
export interface InvoicePaperTemplateProps {
|
||||
export interface InvoicePaperTemplateProps extends PaperTemplateProps {
|
||||
primaryColor?: string;
|
||||
secondaryColor?: string;
|
||||
|
||||
@@ -177,9 +181,14 @@ export function InvoicePaperTemplate({
|
||||
statementLabel = 'Statement',
|
||||
showStatement = true,
|
||||
statement = DefaultPdfTemplateStatement,
|
||||
...props
|
||||
}: InvoicePaperTemplateProps) {
|
||||
return (
|
||||
<PaperTemplate primaryColor={primaryColor} secondaryColor={secondaryColor}>
|
||||
<PaperTemplate
|
||||
primaryColor={primaryColor}
|
||||
secondaryColor={secondaryColor}
|
||||
{...props}
|
||||
>
|
||||
<Stack spacing={24}>
|
||||
<Group align="start" spacing={10}>
|
||||
<Stack flex={1}>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import clsx from 'classnames';
|
||||
import { get, isFunction } from 'lodash';
|
||||
import { Box, Group, GroupProps } from '@/components';
|
||||
import { Box, BoxProps, Group, GroupProps } from '@/components';
|
||||
import styles from './InvoicePaperTemplate.module.scss';
|
||||
|
||||
export interface PaperTemplateProps {
|
||||
export interface PaperTemplateProps extends BoxProps {
|
||||
primaryColor?: string;
|
||||
secondaryColor?: string;
|
||||
children?: React.ReactNode;
|
||||
@@ -14,13 +14,13 @@ export function PaperTemplate({
|
||||
primaryColor,
|
||||
secondaryColor,
|
||||
children,
|
||||
...restProps
|
||||
}: PaperTemplateProps) {
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<Box {...restProps} className={clsx(styles.root, restProps?.className)}>
|
||||
<style>{`:root { --invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor}; }`}</style>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -118,9 +118,9 @@ PaperTemplate.TotalLine = ({
|
||||
);
|
||||
};
|
||||
|
||||
PaperTemplate.MutedText = () => { };
|
||||
PaperTemplate.MutedText = () => {};
|
||||
|
||||
PaperTemplate.Text = () => { };
|
||||
PaperTemplate.Text = () => {};
|
||||
|
||||
PaperTemplate.AddressesGroup = (props: GroupProps) => {
|
||||
return (
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Dialog, DialogSuspense } from '@/components';
|
||||
import withDialogRedux from '@/components/DialogReduxConnect';
|
||||
import { compose } from '@/utils';
|
||||
|
||||
const InvoiceFormMailDeliverDialogContent = React.lazy(
|
||||
() => import('./InvoiceFormMailDeliverDialogContent'),
|
||||
);
|
||||
|
||||
/**
|
||||
* Invoice mail dialog.
|
||||
*/
|
||||
function InvoiceFormMailDeliverDialog({
|
||||
dialogName,
|
||||
payload: { invoiceId = null },
|
||||
isOpen,
|
||||
}) {
|
||||
return (
|
||||
<Dialog
|
||||
name={dialogName}
|
||||
title={'Invoice Mail'}
|
||||
isOpen={isOpen}
|
||||
canEscapeJeyClose={false}
|
||||
isCloseButtonShown={false}
|
||||
autoFocus={true}
|
||||
style={{ width: 600 }}
|
||||
>
|
||||
<DialogSuspense>
|
||||
<InvoiceFormMailDeliverDialogContent
|
||||
dialogName={dialogName}
|
||||
invoiceId={invoiceId}
|
||||
/>
|
||||
</DialogSuspense>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(withDialogRedux())(InvoiceFormMailDeliverDialog);
|
||||
@@ -1,40 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import InvoiceMailDialogContent from '../../../InvoiceMailDialog/InvoiceMailDialogContent';
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
|
||||
interface InvoiceFormDeliverDialogContent {
|
||||
invoiceId: number;
|
||||
}
|
||||
|
||||
function InvoiceFormDeliverDialogContentRoot({
|
||||
invoiceId,
|
||||
|
||||
// #withDialogActions
|
||||
closeDialog,
|
||||
}: InvoiceFormDeliverDialogContent) {
|
||||
const history = useHistory();
|
||||
|
||||
const handleSubmit = () => {
|
||||
history.push('/invoices');
|
||||
closeDialog(DialogsName.InvoiceFormMailDeliver);
|
||||
};
|
||||
const handleCancel = () => {
|
||||
history.push('/invoices');
|
||||
closeDialog(DialogsName.InvoiceFormMailDeliver);
|
||||
};
|
||||
|
||||
return (
|
||||
<InvoiceMailDialogContent
|
||||
invoiceId={invoiceId}
|
||||
onFormSubmit={handleSubmit}
|
||||
onCancelClick={handleCancel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default R.compose(withDialogActions)(
|
||||
InvoiceFormDeliverDialogContentRoot,
|
||||
);
|
||||
@@ -2,7 +2,6 @@
|
||||
import { useFormikContext } from 'formik';
|
||||
import InvoiceNumberDialog from '@/containers/Dialogs/InvoiceNumberDialog';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
import InvoiceFormMailDeliverDialog from './Dialogs/InvoiceFormMailDeliverDialog/InvoiceFormMailDeliverDialog';
|
||||
|
||||
/**
|
||||
* Invoice form dialogs.
|
||||
@@ -28,9 +27,6 @@ export default function InvoiceFormDialogs() {
|
||||
dialogName={DialogsName.InvoiceNumberSettings}
|
||||
onConfirm={handleInvoiceNumberFormConfirm}
|
||||
/>
|
||||
<InvoiceFormMailDeliverDialog
|
||||
dialogName={DialogsName.InvoiceFormMailDeliver}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Dialog, DialogSuspense } from '@/components';
|
||||
import withDialogRedux from '@/components/DialogReduxConnect';
|
||||
import { compose } from '@/utils';
|
||||
|
||||
const InvoiceMailDialogBody = React.lazy(
|
||||
() => import('./InvoiceMailDialogBody'),
|
||||
);
|
||||
|
||||
/**
|
||||
* Invoice mail dialog.
|
||||
*/
|
||||
function InvoiceMailDialog({
|
||||
dialogName,
|
||||
payload: { invoiceId = null },
|
||||
isOpen,
|
||||
}) {
|
||||
return (
|
||||
<Dialog
|
||||
name={dialogName}
|
||||
title={'Invoice Mail'}
|
||||
isOpen={isOpen}
|
||||
canEscapeJeyClose={false}
|
||||
isCloseButtonShown={false}
|
||||
autoFocus={true}
|
||||
style={{ width: 600 }}
|
||||
>
|
||||
<DialogSuspense>
|
||||
<InvoiceMailDialogBody invoiceId={invoiceId} />
|
||||
</DialogSuspense>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
export default compose(withDialogRedux())(InvoiceMailDialog);
|
||||
@@ -1,36 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
import InvoiceMailDialogContent, {
|
||||
InvoiceMailDialogContentProps,
|
||||
} from './InvoiceMailDialogContent';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
|
||||
export interface InvoiceMailDialogBodyProps
|
||||
extends InvoiceMailDialogContentProps {}
|
||||
|
||||
function InvoiceMailDialogBodyRoot({
|
||||
invoiceId,
|
||||
onCancelClick,
|
||||
onFormSubmit,
|
||||
|
||||
// #withDialogActions
|
||||
closeDialog,
|
||||
}: InvoiceMailDialogBodyProps) {
|
||||
const handleCancelClick = () => {
|
||||
closeDialog(DialogsName.InvoiceMail);
|
||||
};
|
||||
const handleSubmitClick = () => {
|
||||
closeDialog(DialogsName.InvoiceMail);
|
||||
};
|
||||
|
||||
return (
|
||||
<InvoiceMailDialogContent
|
||||
invoiceId={invoiceId}
|
||||
onCancelClick={handleCancelClick}
|
||||
onFormSubmit={handleSubmitClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default R.compose(withDialogActions)(InvoiceMailDialogBodyRoot);
|
||||
@@ -1,48 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import React, { createContext } from 'react';
|
||||
import { useSaleInvoiceDefaultOptions } from '@/hooks/query';
|
||||
import { DialogContent } from '@/components';
|
||||
|
||||
interface InvoiceMailDialogBootValues {
|
||||
invoiceId: number;
|
||||
mailOptions: any;
|
||||
redirectToInvoicesList: boolean;
|
||||
}
|
||||
|
||||
const InvoiceMailDialagBoot = createContext<InvoiceMailDialogBootValues>();
|
||||
|
||||
interface InvoiceMailDialogBootProps {
|
||||
invoiceId: number;
|
||||
redirectToInvoicesList?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoice mail dialog boot provider.
|
||||
*/
|
||||
function InvoiceMailDialogBoot({
|
||||
invoiceId,
|
||||
redirectToInvoicesList,
|
||||
...props
|
||||
}: InvoiceMailDialogBootProps) {
|
||||
const { data: mailOptions, isLoading: isMailOptionsLoading } =
|
||||
useSaleInvoiceDefaultOptions(invoiceId);
|
||||
|
||||
const provider = {
|
||||
saleInvoiceId: invoiceId,
|
||||
mailOptions,
|
||||
isMailOptionsLoading,
|
||||
redirectToInvoicesList,
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogContent isLoading={isMailOptionsLoading}>
|
||||
<InvoiceMailDialagBoot.Provider value={provider} {...props} />
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
|
||||
const useInvoiceMailDialogBoot = () =>
|
||||
React.useContext<InvoiceMailDialogBootValues>(InvoiceMailDialagBoot);
|
||||
|
||||
export { InvoiceMailDialogBoot, useInvoiceMailDialogBoot };
|
||||
@@ -1,22 +0,0 @@
|
||||
import { InvoiceMailDialogBoot } from './InvoiceMailDialogBoot';
|
||||
import { InvoiceMailDialogForm } from './InvoiceMailDialogForm';
|
||||
|
||||
export interface InvoiceMailDialogContentProps {
|
||||
invoiceId: number;
|
||||
onFormSubmit?: () => void;
|
||||
onCancelClick?: () => void;
|
||||
}
|
||||
export default function InvoiceMailDialogContent({
|
||||
invoiceId,
|
||||
onFormSubmit,
|
||||
onCancelClick,
|
||||
}: InvoiceMailDialogContentProps) {
|
||||
return (
|
||||
<InvoiceMailDialogBoot invoiceId={invoiceId}>
|
||||
<InvoiceMailDialogForm
|
||||
onFormSubmit={onFormSubmit}
|
||||
onCancelClick={onCancelClick}
|
||||
/>
|
||||
</InvoiceMailDialogBoot>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import * as Yup from 'yup';
|
||||
|
||||
export const InvoiceMailFormSchema = Yup.object().shape({
|
||||
from: Yup.array().required().min(1).max(5).label('From address'),
|
||||
to: Yup.array().required().min(1).max(5).label('To address'),
|
||||
subject: Yup.string().required().label('Mail subject'),
|
||||
body: Yup.string().required().label('Mail body'),
|
||||
});
|
||||
@@ -1,69 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import { Formik } from 'formik';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { useInvoiceMailDialogBoot } from './InvoiceMailDialogBoot';
|
||||
import { AppToaster } from '@/components';
|
||||
import { useSendSaleInvoiceMail } from '@/hooks/query';
|
||||
import { InvoiceMailDialogFormContent } from './InvoiceMailDialogFormContent';
|
||||
import { InvoiceMailFormSchema } from './InvoiceMailDialogForm.schema';
|
||||
import {
|
||||
MailNotificationFormValues,
|
||||
initialMailNotificationValues,
|
||||
transformMailFormToRequest,
|
||||
transformMailFormToInitialValues,
|
||||
} from '@/containers/SendMailNotification/utils';
|
||||
|
||||
const initialFormValues = {
|
||||
...initialMailNotificationValues,
|
||||
attachInvoice: true,
|
||||
};
|
||||
|
||||
interface InvoiceMailFormValues extends MailNotificationFormValues {
|
||||
attachInvoice: boolean;
|
||||
}
|
||||
|
||||
export function InvoiceMailDialogForm({ onFormSubmit, onCancelClick }) {
|
||||
const { mailOptions, saleInvoiceId } = useInvoiceMailDialogBoot();
|
||||
const { mutateAsync: sendInvoiceMail } = useSendSaleInvoiceMail();
|
||||
|
||||
const initialValues = transformMailFormToInitialValues(
|
||||
mailOptions,
|
||||
initialFormValues,
|
||||
);
|
||||
// Handle the form submitting.
|
||||
const handleSubmit = (values: InvoiceMailFormValues, { setSubmitting }) => {
|
||||
const reqValues = transformMailFormToRequest(values);
|
||||
|
||||
setSubmitting(true);
|
||||
sendInvoiceMail([saleInvoiceId, reqValues])
|
||||
.then(() => {
|
||||
AppToaster.show({
|
||||
message: 'The mail notification has been sent successfully.',
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
setSubmitting(false);
|
||||
onFormSubmit && onFormSubmit(values);
|
||||
})
|
||||
.catch(() => {
|
||||
AppToaster.show({
|
||||
message: 'Something went wrong.',
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
setSubmitting(false);
|
||||
});
|
||||
};
|
||||
// Handle the close button click.
|
||||
const handleClose = () => {
|
||||
onCancelClick && onCancelClick();
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={InvoiceMailFormSchema}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<InvoiceMailDialogFormContent onClose={handleClose} />
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import { Form, useFormikContext } from 'formik';
|
||||
import { Button, Classes, Intent } from '@blueprintjs/core';
|
||||
import styled from 'styled-components';
|
||||
import { FFormGroup, FSwitch } from '@/components';
|
||||
import { MailNotificationForm } from '@/containers/SendMailNotification';
|
||||
import { saveInvoke } from '@/utils';
|
||||
import { useInvoiceMailDialogBoot } from './InvoiceMailDialogBoot';
|
||||
|
||||
interface SendMailNotificationFormProps {
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export function InvoiceMailDialogFormContent({
|
||||
onClose,
|
||||
}: SendMailNotificationFormProps) {
|
||||
const { isSubmitting } = useFormikContext();
|
||||
const { mailOptions } = useInvoiceMailDialogBoot();
|
||||
|
||||
const handleClose = () => {
|
||||
saveInvoke(onClose);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
<MailNotificationForm
|
||||
fromAddresses={mailOptions.from_addresses}
|
||||
toAddresses={mailOptions.to_addresses}
|
||||
/>
|
||||
<AttachFormGroup name={'attachInvoice'} inline>
|
||||
<FSwitch name={'attachInvoice'} label={'Attach Invoice'} />
|
||||
</AttachFormGroup>
|
||||
</div>
|
||||
|
||||
<div className={Classes.DIALOG_FOOTER}>
|
||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
onClick={handleClose}
|
||||
style={{ minWidth: '65px' }}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
intent={Intent.PRIMARY}
|
||||
loading={isSubmitting}
|
||||
style={{ minWidth: '75px' }}
|
||||
type="submit"
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
const AttachFormGroup = styled(FFormGroup)`
|
||||
background: #f8f9fb;
|
||||
margin-top: 0.6rem;
|
||||
padding: 4px 14px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #dcdcdd;
|
||||
`;
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './InvoiceMailDialog';
|
||||
export * from './InvoiceMailDialogContent';
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useMemo } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { Box, } from '@/components';
|
||||
import { InvoiceMailReceiptPreview } from '../InvoiceCustomize/InvoiceMailReceiptPreview';
|
||||
import { useInvoiceSendMailBoot } from './InvoiceSendMailContentBoot';
|
||||
import { InvoiceSendMailPreviewWithHeader } from './InvoiceSendMailHeaderPreview';
|
||||
import { useSendInvoiceMailMessage } from './_hooks';
|
||||
|
||||
export function InvoiceMailReceiptPreviewConneceted() {
|
||||
const mailMessage = useSendInvoiceMailMessage();
|
||||
const { invoiceMailState } = useInvoiceSendMailBoot();
|
||||
|
||||
const items = useMemo(
|
||||
() =>
|
||||
invoiceMailState?.entries?.map((entry: any) => ({
|
||||
quantity: entry.quantity,
|
||||
total: entry.totalFormatted,
|
||||
label: entry.name,
|
||||
})),
|
||||
[invoiceMailState?.entries],
|
||||
);
|
||||
|
||||
return (
|
||||
<InvoiceSendMailPreviewWithHeader>
|
||||
<Box px={4} pt={8} pb={16}>
|
||||
<InvoiceMailReceiptPreview
|
||||
companyName={invoiceMailState?.companyName}
|
||||
// companyLogoUri={invoiceMailState?.companyLogoUri}
|
||||
|
||||
primaryColor={invoiceMailState?.primaryColor}
|
||||
total={invoiceMailState?.totalFormatted}
|
||||
dueDate={invoiceMailState?.dueDateFormatted}
|
||||
dueAmount={invoiceMailState?.dueAmountFormatted}
|
||||
|
||||
invoiceNumber={invoiceMailState?.invoiceNo}
|
||||
items={items}
|
||||
message={mailMessage}
|
||||
className={css`
|
||||
margin: 0 auto;
|
||||
`}
|
||||
/>
|
||||
</Box>
|
||||
</InvoiceSendMailPreviewWithHeader>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Group, Stack } from '@/components';
|
||||
import { Classes } from '@blueprintjs/core';
|
||||
import { InvoiceSendMailBoot } from './InvoiceSendMailContentBoot';
|
||||
import { InvoiceSendMailForm } from './InvoiceSendMailForm';
|
||||
import { InvoiceSendMailHeader } from './InvoiceSendMailHeader';
|
||||
import { InvoiceSendMailPreview } from './InvoiceSendMailPreview';
|
||||
import { InvoiceSendMailFields } from './InvoiceSendMailFields';
|
||||
|
||||
export function InvoiceSendMailContent() {
|
||||
return (
|
||||
<Stack className={Classes.DRAWER_BODY}>
|
||||
<InvoiceSendMailBoot>
|
||||
<InvoiceSendMailForm>
|
||||
<Stack spacing={0} flex={1} overflow="hidden">
|
||||
<InvoiceSendMailHeader label={'Send Invoice Mail'} />
|
||||
|
||||
<Group flex={1} overflow="auto" spacing={0} alignItems={'stretch'}>
|
||||
<InvoiceSendMailFields />
|
||||
<InvoiceSendMailPreview />
|
||||
</Group>
|
||||
</Stack>
|
||||
</InvoiceSendMailForm>
|
||||
</InvoiceSendMailBoot>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// @ts-nocheck
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { Spinner } from '@blueprintjs/core';
|
||||
import {
|
||||
GetSaleInvoiceDefaultOptionsResponse,
|
||||
useSaleInvoiceMailState,
|
||||
} from '@/hooks/query';
|
||||
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||
|
||||
interface InvoiceSendMailBootValues {
|
||||
invoiceId: number;
|
||||
|
||||
invoiceMailState: GetSaleInvoiceDefaultOptionsResponse | undefined;
|
||||
isInvoiceMailState: boolean;
|
||||
}
|
||||
interface InvoiceSendMailBootProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const InvoiceSendMailContentBootContext =
|
||||
createContext<InvoiceSendMailBootValues>({} as InvoiceSendMailBootValues);
|
||||
|
||||
export const InvoiceSendMailBoot = ({ children }: InvoiceSendMailBootProps) => {
|
||||
const {
|
||||
payload: { invoiceId },
|
||||
} = useDrawerContext();
|
||||
|
||||
// Invoice mail options.
|
||||
const { data: invoiceMailState, isLoading: isInvoiceMailState } =
|
||||
useSaleInvoiceMailState(invoiceId);
|
||||
|
||||
const isLoading = isInvoiceMailState;
|
||||
|
||||
if (isLoading) {
|
||||
return <Spinner size={20} />;
|
||||
}
|
||||
const value = {
|
||||
invoiceId,
|
||||
|
||||
// # Invoice mail options
|
||||
isInvoiceMailState,
|
||||
invoiceMailState,
|
||||
};
|
||||
|
||||
return (
|
||||
<InvoiceSendMailContentBootContext.Provider value={value}>
|
||||
{children}
|
||||
</InvoiceSendMailContentBootContext.Provider>
|
||||
);
|
||||
};
|
||||
InvoiceSendMailBoot.displayName = 'InvoiceSendMailBoot';
|
||||
|
||||
export const useInvoiceSendMailBoot = () => {
|
||||
return useContext<InvoiceSendMailBootValues>(
|
||||
InvoiceSendMailContentBootContext,
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import * as R from 'ramda';
|
||||
import { Drawer, DrawerSuspense } from '@/components';
|
||||
import withDrawers from '@/containers/Drawer/withDrawers';
|
||||
import React from 'react';
|
||||
|
||||
const InvoiceSendMailContent = React.lazy(() =>
|
||||
import('./InvoiceSendMailContent').then((module) => ({
|
||||
default: module.InvoiceSendMailContent,
|
||||
})),
|
||||
);
|
||||
|
||||
interface InvoiceSendMailDrawerProps {
|
||||
name: string;
|
||||
isOpen?: boolean;
|
||||
payload?: any;
|
||||
}
|
||||
|
||||
function InvoiceSendMailDrawerRoot({
|
||||
name,
|
||||
|
||||
// #withDrawer
|
||||
isOpen,
|
||||
payload,
|
||||
}: InvoiceSendMailDrawerProps) {
|
||||
return (
|
||||
<Drawer
|
||||
isOpen={isOpen}
|
||||
name={name}
|
||||
payload={payload}
|
||||
size={'calc(100% - 10px)'}
|
||||
>
|
||||
<DrawerSuspense>
|
||||
<InvoiceSendMailContent />
|
||||
</DrawerSuspense>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export const InvoiceSendMailDrawer = R.compose(withDrawers())(
|
||||
InvoiceSendMailDrawerRoot,
|
||||
);
|
||||
@@ -0,0 +1,312 @@
|
||||
// @ts-nocheck
|
||||
import { Button, Intent, MenuItem, Position } from '@blueprintjs/core';
|
||||
import { useRef, useState, useMemo, useCallback } from 'react';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { SelectOptionProps } from '@blueprintjs-formik/select';
|
||||
import { css } from '@emotion/css';
|
||||
import {
|
||||
FCheckbox,
|
||||
FFormGroup,
|
||||
FInputGroup,
|
||||
FMultiSelect,
|
||||
FSelect,
|
||||
FTextArea,
|
||||
Group,
|
||||
Icon,
|
||||
Stack,
|
||||
} from '@/components';
|
||||
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||
import { useDrawerActions } from '@/hooks/state';
|
||||
import { useInvoiceMailItems, useSendInvoiceFormatArgsOptions } from './_hooks';
|
||||
|
||||
// Create new account renderer.
|
||||
const createNewItemRenderer = (query, active, handleClick) => {
|
||||
return (
|
||||
<MenuItem
|
||||
icon="add"
|
||||
text={'Now contact address'}
|
||||
active={active}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// Create new item from the given query string.
|
||||
const createNewItemFromQuery = (name) => ({ name });
|
||||
|
||||
const styleEmailButton = css`
|
||||
&.bp4-button.bp4-small {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
min-height: 26px;
|
||||
line-height: 26px;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
`;
|
||||
|
||||
const fieldsWrapStyle = css`
|
||||
> :not(:first-of-type) .bp4-input {
|
||||
border-top-color: transparent;
|
||||
border-top-right-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
> :not(:last-of-type) .bp4-input {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export function InvoiceSendMailFields() {
|
||||
const [showCCField, setShowCCField] = useState<boolean>(false);
|
||||
const [showBccField, setShowBccField] = useState<boolean>(false);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const { values, setFieldValue } = useFormikContext();
|
||||
const items = useInvoiceMailItems();
|
||||
const argsOptions = useSendInvoiceFormatArgsOptions();
|
||||
|
||||
const handleClickCcBtn = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
setShowCCField(true);
|
||||
};
|
||||
|
||||
const handleClickBccBtn = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
setShowBccField(true);
|
||||
};
|
||||
|
||||
const handleCreateToItemSelect = (value: SelectOptionProps) => {
|
||||
setFieldValue('to', [...values?.to, value?.name]);
|
||||
};
|
||||
|
||||
const handleCreateCcItemSelect = (value: SelectOptionProps) => {
|
||||
setFieldValue('cc', [...values?.cc, value?.name]);
|
||||
};
|
||||
const handleCreateBccItemSelect = (value: SelectOptionProps) => {
|
||||
setFieldValue('bcc', [...values?.bcc, value?.name]);
|
||||
};
|
||||
|
||||
const rightElementsToField = useMemo(() => (
|
||||
<Group
|
||||
spacing={0}
|
||||
paddingRight={'7px'}
|
||||
paddingTop={'7px'}
|
||||
fontWeight={500}
|
||||
color={'#000'}
|
||||
>
|
||||
<Button
|
||||
onClick={handleClickCcBtn}
|
||||
minimal
|
||||
small
|
||||
className={styleEmailButton}
|
||||
>
|
||||
CC
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={handleClickBccBtn}
|
||||
minimal
|
||||
small
|
||||
className={styleEmailButton}
|
||||
>
|
||||
BCC
|
||||
</Button>
|
||||
</Group>
|
||||
), []);
|
||||
|
||||
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]);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
bg="white"
|
||||
flex={'1'}
|
||||
maxHeight="100%"
|
||||
spacing={0}
|
||||
borderRight="1px solid #dcdcdd"
|
||||
>
|
||||
<Stack spacing={0} overflow="auto" flex="1" p={'30px'}>
|
||||
<FFormGroup label={'To'} name={'to'}>
|
||||
<Stack spacing={0} className={fieldsWrapStyle}>
|
||||
<FMultiSelect
|
||||
items={items}
|
||||
name={'to'}
|
||||
placeholder={'To'}
|
||||
popoverProps={{ minimal: true, fill: true }}
|
||||
tagInputProps={{
|
||||
tagProps: { round: true, minimal: true, large: true },
|
||||
rightElement: rightElementsToField,
|
||||
large: true,
|
||||
}}
|
||||
createNewItemRenderer={createNewItemRenderer}
|
||||
createNewItemFromQuery={createNewItemFromQuery}
|
||||
onCreateItemSelect={handleCreateToItemSelect}
|
||||
resetOnQuery
|
||||
resetOnSelect
|
||||
fill
|
||||
fastField
|
||||
/>
|
||||
{showCCField && (
|
||||
<FMultiSelect
|
||||
items={items}
|
||||
name={'cc'}
|
||||
placeholder={'Cc'}
|
||||
popoverProps={{ minimal: true, fill: true }}
|
||||
tagInputProps={{
|
||||
tagProps: { round: true, minimal: true, large: true },
|
||||
large: true,
|
||||
}}
|
||||
createNewItemRenderer={createNewItemRenderer}
|
||||
createNewItemFromQuery={createNewItemFromQuery}
|
||||
onCreateItemSelect={handleCreateCcItemSelect}
|
||||
resetOnQuery
|
||||
resetOnSelect
|
||||
fill
|
||||
fastField
|
||||
/>
|
||||
)}
|
||||
{showBccField && (
|
||||
<FMultiSelect
|
||||
items={items}
|
||||
name={'bcc'}
|
||||
placeholder={'Bcc'}
|
||||
popoverProps={{ minimal: true, fill: true }}
|
||||
tagInputProps={{
|
||||
tagProps: { round: true, minimal: true, large: true },
|
||||
large: true,
|
||||
}}
|
||||
createNewItemRenderer={createNewItemRenderer}
|
||||
createNewItemFromQuery={createNewItemFromQuery}
|
||||
onCreateItemSelect={handleCreateBccItemSelect}
|
||||
resetOnQuery
|
||||
resetOnSelect
|
||||
fill
|
||||
fastField
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</FFormGroup>
|
||||
|
||||
<FFormGroup label={'Submit'} name={'subject'}>
|
||||
<FInputGroup name={'subject'} large fastField />
|
||||
</FFormGroup>
|
||||
|
||||
<FFormGroup label={'Message'} name={'message'}>
|
||||
<Stack spacing={0}>
|
||||
<Group
|
||||
border={'1px solid #ced4da'}
|
||||
borderBottom={0}
|
||||
borderRadius={'3px 3px 0 0'}
|
||||
>
|
||||
<FSelect
|
||||
selectedItem={'customerName'}
|
||||
name={'item'}
|
||||
items={argsOptions}
|
||||
onItemChange={handleTextareaChange}
|
||||
popoverProps={{
|
||||
fill: false,
|
||||
position: Position.BOTTOM_LEFT,
|
||||
minimal: true,
|
||||
}}
|
||||
input={({ activeItem, text, label, value }) => (
|
||||
<Button
|
||||
minimal
|
||||
rightIcon={
|
||||
<Icon icon={'caret-down-16'} color={'#8F99A8'} />
|
||||
}
|
||||
>
|
||||
Insert Variable
|
||||
</Button>
|
||||
)}
|
||||
fill={false}
|
||||
fastField
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<FTextArea
|
||||
inputRef={textareaRef}
|
||||
name={'message'}
|
||||
large
|
||||
fill
|
||||
fastField
|
||||
className={css`
|
||||
resize: vertical;
|
||||
min-height: 300px;
|
||||
border-top-right-radius: 0px;
|
||||
border-top-left-radius: 0px;
|
||||
`}
|
||||
/>
|
||||
</Stack>
|
||||
</FFormGroup>
|
||||
|
||||
<Group>
|
||||
<FCheckbox name={'attachPdf'} label={'Attach PDF'} />
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
<InvoiceSendMailFooter />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function InvoiceSendMailFooter() {
|
||||
const { isSubmitting } = useFormikContext();
|
||||
const { name } = useDrawerContext();
|
||||
const { closeDrawer } = useDrawerActions();
|
||||
|
||||
const handleClose = () => {
|
||||
closeDrawer(name);
|
||||
};
|
||||
|
||||
return (
|
||||
<Group
|
||||
py={'12px'}
|
||||
px={'16px'}
|
||||
borderTop="1px solid #d8d8d9"
|
||||
position={'apart'}
|
||||
>
|
||||
<Group spacing={10} ml={'auto'}>
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
onClick={handleClose}
|
||||
style={{ minWidth: '65px' }}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
intent={Intent.PRIMARY}
|
||||
loading={isSubmitting}
|
||||
style={{ minWidth: '85px' }}
|
||||
type="submit"
|
||||
>
|
||||
Send Mail
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import * as Yup from 'yup';
|
||||
|
||||
export const InvoiceSendMailFormSchema = Yup.object().shape({
|
||||
subject: Yup.string().required('Subject is required'),
|
||||
message: Yup.string().required('Message is required'),
|
||||
to: Yup.array()
|
||||
.of(Yup.string().email('Invalid email'))
|
||||
.required('To address is required'),
|
||||
cc: Yup.array().of(Yup.string().email('Invalid email')),
|
||||
bcc: Yup.array().of(Yup.string().email('Invalid email')),
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
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';
|
||||
|
||||
const initialValues: InvoiceSendMailFormValues = {
|
||||
subject: '',
|
||||
message: '',
|
||||
to: [],
|
||||
cc: [],
|
||||
bcc: [],
|
||||
attachPdf: true,
|
||||
};
|
||||
|
||||
interface InvoiceSendMailFormProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function InvoiceSendMailForm({ children }: InvoiceSendMailFormProps) {
|
||||
const { mutateAsync: sendInvoiceMail } = useSendSaleInvoiceMail();
|
||||
const { invoiceId, invoiceMailState } = useInvoiceSendMailBoot();
|
||||
|
||||
const { name } = useDrawerContext();
|
||||
const { closeDrawer } = useDrawerActions();
|
||||
|
||||
const _initialValues: InvoiceSendMailFormValues = {
|
||||
...initialValues,
|
||||
...transformToForm(invoiceMailState, initialValues),
|
||||
};
|
||||
const handleSubmit = (
|
||||
values: InvoiceSendMailFormValues,
|
||||
{ setSubmitting }: FormikHelpers<InvoiceSendMailFormValues>,
|
||||
) => {
|
||||
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 (
|
||||
<Formik
|
||||
initialValues={_initialValues}
|
||||
validationSchema={InvoiceSendMailFormSchema}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Form
|
||||
className={css`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</Form>
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Button, Classes } from '@blueprintjs/core';
|
||||
import { x } from '@xstyled/emotion';
|
||||
import { Group, Icon } from '@/components';
|
||||
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||
import { useDrawerActions } from '@/hooks/state';
|
||||
|
||||
interface ElementCustomizeHeaderProps {
|
||||
label?: string;
|
||||
children?: React.ReactNode;
|
||||
closeButton?: boolean;
|
||||
}
|
||||
|
||||
export function InvoiceSendMailHeader({
|
||||
label,
|
||||
closeButton = true,
|
||||
}: ElementCustomizeHeaderProps) {
|
||||
const { name } = useDrawerContext();
|
||||
const { closeDrawer } = useDrawerActions();
|
||||
|
||||
const handleClose = () => {
|
||||
closeDrawer(name);
|
||||
};
|
||||
return (
|
||||
<Group
|
||||
p={'10px'}
|
||||
pl={'30px'}
|
||||
bg="white"
|
||||
alignItems={'center'}
|
||||
boxShadow={'0 1px 0 rgba(17, 20, 24, .15)'}
|
||||
zIndex={1}
|
||||
style={{ position: 'relative' }}
|
||||
>
|
||||
{label && (
|
||||
<x.h1 margin={0} fontSize={20} fontWeight={500} color={'#666'}>
|
||||
{label}
|
||||
</x.h1>
|
||||
)}
|
||||
{closeButton && (
|
||||
<Button
|
||||
aria-label="Close"
|
||||
className={Classes.DIALOG_CLOSE_BUTTON}
|
||||
icon={<Icon icon={'smallCross'} color={'#000'} />}
|
||||
minimal={true}
|
||||
onClick={handleClose}
|
||||
style={{ marginLeft: 'auto' }}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { x } from '@xstyled/emotion';
|
||||
import { Box, Group, Stack } from '@/components';
|
||||
import { useSendInvoiceMailForm, useSendInvoiceMailSubject } from './_hooks';
|
||||
import { useInvoiceSendMailBoot } from './InvoiceSendMailContentBoot';
|
||||
|
||||
export function InvoiceSendMailHeaderPreview() {
|
||||
const mailSubject = useSendInvoiceMailSubject();
|
||||
const { invoiceMailState } = useInvoiceSendMailBoot();
|
||||
const toAddresses = useMailHeaderToAddresses();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
bg={'white'}
|
||||
borderBottom={'1px solid #dcdcdd'}
|
||||
padding={'22px 30px'}
|
||||
spacing={8}
|
||||
position={'sticky'}
|
||||
top={0}
|
||||
zIndex={1}
|
||||
>
|
||||
<Box>
|
||||
<x.h2 fontWeight={600} fontSize={16}>
|
||||
{mailSubject}
|
||||
</x.h2>
|
||||
</Box>
|
||||
|
||||
<Group display="flex" gap={2}>
|
||||
<Group display="flex" alignItems="center" gap={15}>
|
||||
<x.abbr
|
||||
role="presentation"
|
||||
w={'40px'}
|
||||
h={'40px'}
|
||||
bg={'#daa3e4'}
|
||||
fill={'#daa3e4'}
|
||||
color={'#3f1946'}
|
||||
lineHeight={'40px'}
|
||||
textAlign={'center'}
|
||||
borderRadius={'40px'}
|
||||
fontSize={'14px'}
|
||||
>
|
||||
A
|
||||
</x.abbr>
|
||||
|
||||
<Stack spacing={2}>
|
||||
<Group spacing={2}>
|
||||
<Box fontWeight={600}>Ahmed </Box>
|
||||
<Box color={'#738091'}>
|
||||
<messaging-service@post.bigcapital.app>
|
||||
</Box>
|
||||
</Group>
|
||||
|
||||
<Box fontSize={'sm'} color={'#738091'}>
|
||||
Reply to: {invoiceMailState?.companyName} {toAddresses};
|
||||
</Box>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export function InvoiceSendMailPreviewWithHeader({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Box>
|
||||
<InvoiceSendMailHeaderPreview />
|
||||
<Box>{children}</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export const useMailHeaderToAddresses = () => {
|
||||
const {
|
||||
values: { to },
|
||||
} = useSendInvoiceMailForm();
|
||||
|
||||
return useMemo(() => to?.map((email) => '<' + email + '>').join(' '), [to]);
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { Tab, Tabs } from '@blueprintjs/core';
|
||||
import { Stack } from '@/components';
|
||||
|
||||
const InvoiceMailReceiptPreviewConneceted = lazy(() =>
|
||||
import('./InvoiceMailReceiptPreviewConnected.').then((module) => ({
|
||||
default: module.InvoiceMailReceiptPreviewConneceted,
|
||||
})),
|
||||
);
|
||||
const InvoiceSendPdfPreviewConnected = lazy(() =>
|
||||
import('./InvoiceSendPdfPreviewConnected').then((module) => ({
|
||||
default: module.InvoiceSendPdfPreviewConnected,
|
||||
})),
|
||||
);
|
||||
|
||||
export function InvoiceSendMailPreview() {
|
||||
return (
|
||||
<Stack bg="#F5F5F5" flex={'1'} maxHeight={'100%'} minWidth="850px">
|
||||
<Tabs
|
||||
id={'preview'}
|
||||
defaultSelectedTabId={'payment-page'}
|
||||
className={css`
|
||||
overflow: hidden;
|
||||
flex: 1 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.bp4-tab-list {
|
||||
padding: 0 20px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #dcdcdd;
|
||||
}
|
||||
.bp4-tab {
|
||||
line-height: 40px;
|
||||
}
|
||||
.bp4-tab:not([aria-selected='true']) {
|
||||
color: #5f6b7c;
|
||||
}
|
||||
.bp4-tab-indicator-wrapper .bp4-tab-indicator {
|
||||
height: 2px;
|
||||
}
|
||||
.bp4-tab-panel {
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Tab
|
||||
id={'payment-page'}
|
||||
title={'Payment page'}
|
||||
panel={
|
||||
<Suspense>
|
||||
<InvoiceMailReceiptPreviewConneceted />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Tab
|
||||
id="pdf-document"
|
||||
title={'PDF document'}
|
||||
panel={
|
||||
<Suspense>
|
||||
<InvoiceSendPdfPreviewConnected />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
</Tabs>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { Box } from '@/components';
|
||||
import { InvoicePaperTemplate } from '../InvoiceCustomize/InvoicePaperTemplate';
|
||||
import { InvoiceSendMailPreviewWithHeader } from './InvoiceSendMailHeaderPreview';
|
||||
|
||||
export function InvoiceSendPdfPreviewConnected() {
|
||||
return (
|
||||
<InvoiceSendMailPreviewWithHeader>
|
||||
<Box px={4} py={6}>
|
||||
<InvoicePaperTemplate
|
||||
className={css`
|
||||
margin: 0 auto;
|
||||
`}
|
||||
/>
|
||||
</Box>
|
||||
</InvoiceSendMailPreviewWithHeader>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { useFormikContext } from 'formik';
|
||||
import { useMemo } from 'react';
|
||||
import { SelectOptionProps } from '@blueprintjs-formik/select';
|
||||
import { chain, defaultTo, mapKeys, snakeCase, startCase } from 'lodash';
|
||||
import { InvoiceSendMailFormValues } from './_types';
|
||||
import { useInvoiceSendMailBoot } from './InvoiceSendMailContentBoot';
|
||||
|
||||
export const useSendInvoiceMailForm = () => {
|
||||
return useFormikContext<InvoiceSendMailFormValues>();
|
||||
};
|
||||
|
||||
export const useInvoiceMailItems = () => {
|
||||
const { values } = useFormikContext<InvoiceSendMailFormValues>();
|
||||
const cc = values?.cc || [];
|
||||
const bcc = values?.bcc || [];
|
||||
|
||||
return chain([...values?.to, ...cc, ...bcc])
|
||||
.filter((email) => !!email?.trim())
|
||||
.uniq()
|
||||
.map((email) => ({
|
||||
value: email,
|
||||
text: email,
|
||||
}))
|
||||
.value();
|
||||
};
|
||||
|
||||
export const useSendInvoiceMailFormatArgs = (): Record<string, string> => {
|
||||
const { invoiceMailState } = useInvoiceSendMailBoot();
|
||||
|
||||
return useMemo(() => {
|
||||
return mapKeys(invoiceMailState?.formatArgs, (_, key) =>
|
||||
startCase(snakeCase(key).replace('_', ' ')),
|
||||
);
|
||||
}, [invoiceMailState]);
|
||||
};
|
||||
|
||||
export const useSendInvoiceMailSubject = (): string => {
|
||||
const { values } = useFormikContext<InvoiceSendMailFormValues>();
|
||||
const formatArgs = useSendInvoiceMailFormatArgs();
|
||||
|
||||
return formatSmsMessage(values?.subject, formatArgs);
|
||||
};
|
||||
|
||||
export const useSendInvoiceFormatArgsOptions = (): Array<SelectOptionProps> => {
|
||||
const formatArgs = useSendInvoiceMailFormatArgs();
|
||||
|
||||
return Object.keys(formatArgs).map((key) => ({
|
||||
value: key,
|
||||
text: key,
|
||||
}));
|
||||
};
|
||||
|
||||
export const useSendInvoiceMailMessage = (): string => {
|
||||
const { values } = useFormikContext<InvoiceSendMailFormValues>();
|
||||
const formatArgs = useSendInvoiceMailFormatArgs();
|
||||
|
||||
return formatSmsMessage(values?.message, formatArgs);
|
||||
};
|
||||
|
||||
export const formatSmsMessage = (
|
||||
message: string,
|
||||
args: Record<string, any>,
|
||||
) => {
|
||||
let formattedMessage = message;
|
||||
|
||||
Object.keys(args).forEach((key) => {
|
||||
const variable = `{${key}}`;
|
||||
const value = defaultTo(args[key], '');
|
||||
|
||||
formattedMessage = formattedMessage.replace(variable, value);
|
||||
});
|
||||
return formattedMessage;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface InvoiceSendMailFormValues {
|
||||
subject: string;
|
||||
message: string;
|
||||
to: string[];
|
||||
cc: string[];
|
||||
bcc: string[];
|
||||
attachPdf: boolean;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './InvoiceSendMailDrawer';
|
||||
@@ -101,7 +101,7 @@ function InvoicesDataTable({
|
||||
|
||||
// Handle send mail invoice.
|
||||
const handleSendMailInvoice = ({ id }) => {
|
||||
openDialog(DialogsName.InvoiceMail, { invoiceId: id });
|
||||
openDrawer(DRAWERS.INVOICE_SEND_MAIL, { invoiceId: id });
|
||||
};
|
||||
|
||||
// Handle cell click.
|
||||
|
||||
@@ -19,7 +19,12 @@ function ReceiptCustomizeDrawerRoot({
|
||||
payload,
|
||||
}) {
|
||||
return (
|
||||
<Drawer isOpen={isOpen} name={name} size={'100%'} payload={payload}>
|
||||
<Drawer
|
||||
isOpen={isOpen}
|
||||
name={name}
|
||||
payload={payload}
|
||||
size={'calc(100% - 10px)'}
|
||||
>
|
||||
<DrawerSuspense>
|
||||
<ReceiptCustomizeDrawerBody />
|
||||
</DrawerSuspense>
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
// @ts-nocheck
|
||||
import { useQueryClient, useMutation, useQuery } from 'react-query';
|
||||
import {
|
||||
useQueryClient,
|
||||
useMutation,
|
||||
useQuery,
|
||||
UseMutationOptions,
|
||||
UseMutationResult,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from 'react-query';
|
||||
import { useRequestQuery } from '../useQueryRequest';
|
||||
import { transformPagination, transformToCamelCase } from '@/utils';
|
||||
import useApiRequest from '../useRequest';
|
||||
@@ -312,36 +320,114 @@ export function useInvoicePaymentTransactions(invoiceId, props) {
|
||||
);
|
||||
}
|
||||
|
||||
export function useSendSaleInvoiceMail(props) {
|
||||
// # Send sale invoice mail.
|
||||
// ------------------------------
|
||||
export interface SendSaleInvoiceMailValues {
|
||||
id: number;
|
||||
values: {
|
||||
subject: string;
|
||||
message: string;
|
||||
to: Array<string>;
|
||||
cc?: Array<string>;
|
||||
bcc?: Array<string>;
|
||||
attachInvoice?: boolean;
|
||||
};
|
||||
}
|
||||
export interface SendSaleInvoiceMailResponse { }
|
||||
/**
|
||||
* Sends the sale invoice mail.
|
||||
* @param {UseMutationOptions<SendSaleInvoiceMailValues, Error, SendSaleInvoiceMailResponse>}
|
||||
* @returns {UseMutationResult<SendSaleInvoiceMailResponse, Error, SendSaleInvoiceMailValues>}
|
||||
*/
|
||||
export function useSendSaleInvoiceMail(
|
||||
options?: UseMutationOptions<
|
||||
SendSaleInvoiceMailResponse,
|
||||
Error,
|
||||
SendSaleInvoiceMailValues
|
||||
>,
|
||||
): UseMutationResult<
|
||||
SendSaleInvoiceMailResponse,
|
||||
Error,
|
||||
SendSaleInvoiceMailValues
|
||||
> {
|
||||
const queryClient = useQueryClient();
|
||||
const apiRequest = useApiRequest();
|
||||
|
||||
return useMutation(
|
||||
([id, values]) => apiRequest.post(`sales/invoices/${id}/mail`, values),
|
||||
return useMutation<
|
||||
SendSaleInvoiceMailResponse,
|
||||
Error,
|
||||
SendSaleInvoiceMailValues
|
||||
>(
|
||||
(value) => apiRequest.post(`sales/invoices/${value.id}/mail`, value.values),
|
||||
{
|
||||
onSuccess: (res, [id, values]) => {
|
||||
// Common invalidate queries.
|
||||
onSuccess: (res) => {
|
||||
commonInvalidateQueries(queryClient);
|
||||
},
|
||||
...props,
|
||||
...options,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function useSaleInvoiceDefaultOptions(invoiceId, props) {
|
||||
return useRequestQuery(
|
||||
// # Get sale invoice default options.
|
||||
// --------------------------------------
|
||||
export interface GetSaleInvoiceDefaultOptionsResponse {
|
||||
companyName: string;
|
||||
|
||||
dueDate: string;
|
||||
dueDateFormatted: string;
|
||||
|
||||
dueAmount: number;
|
||||
dueAmountFormatted: string;
|
||||
|
||||
entries: Array<{
|
||||
quantity: number;
|
||||
quantityFormatted: string;
|
||||
rate: number;
|
||||
rateFormatted: string;
|
||||
total: number;
|
||||
totalFormatted: string;
|
||||
}>;
|
||||
formatArgs: Record<string, string>;
|
||||
|
||||
from: string[];
|
||||
to: string[];
|
||||
|
||||
invoiceDate: string;
|
||||
invoiceDateFormatted: string;
|
||||
|
||||
invoiceNo: string;
|
||||
|
||||
message: string;
|
||||
subject: string;
|
||||
|
||||
subtotal: number;
|
||||
subtotalFormatted: string;
|
||||
|
||||
total: number;
|
||||
totalFormatted: string;
|
||||
|
||||
attachInvoice: boolean;
|
||||
primaryColor: string;
|
||||
}
|
||||
|
||||
export function useSaleInvoiceMailState(
|
||||
invoiceId: number,
|
||||
options?: UseQueryOptions<GetSaleInvoiceDefaultOptionsResponse>,
|
||||
): UseQueryResult<GetSaleInvoiceDefaultOptionsResponse> {
|
||||
const apiRequest = useApiRequest();
|
||||
|
||||
return useQuery<GetSaleInvoiceDefaultOptionsResponse>(
|
||||
[t.SALE_INVOICE_DEFAULT_OPTIONS, invoiceId],
|
||||
{
|
||||
method: 'get',
|
||||
url: `sales/invoices/${invoiceId}/mail`,
|
||||
},
|
||||
{
|
||||
select: (res) => res.data.data,
|
||||
...props,
|
||||
},
|
||||
() =>
|
||||
apiRequest
|
||||
.get(`/sales/invoices/${invoiceId}/mail/state`)
|
||||
.then((res) => transformToCamelCase(res.data?.data)),
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
// # Get sale invoice state.
|
||||
// -------------------------------------
|
||||
export interface GetSaleInvoiceStateResponse {
|
||||
defaultTemplateId: number;
|
||||
}
|
||||
@@ -360,3 +446,68 @@ export function useGetSaleInvoiceState(
|
||||
{ ...options },
|
||||
);
|
||||
}
|
||||
|
||||
// # Get sale invoice branding template.
|
||||
// --------------------------------------
|
||||
export interface GetSaleInvoiceBrandingTemplateResponse {
|
||||
id: number;
|
||||
default: number;
|
||||
predefined: number;
|
||||
resource: string;
|
||||
resourceFormatted: string;
|
||||
templateName: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
createdAtFormatted: string;
|
||||
attributes: {
|
||||
billedToLabel?: string;
|
||||
companyLogoKey?: string | null;
|
||||
companyLogoUri?: string;
|
||||
dateIssueLabel?: string;
|
||||
discountLabel?: string;
|
||||
dueAmountLabel?: string;
|
||||
dueDateLabel?: string;
|
||||
invoiceNumberLabel?: string;
|
||||
itemDescriptionLabel?: string;
|
||||
itemNameLabel?: string;
|
||||
itemRateLabel?: string;
|
||||
itemTotalLabel?: string;
|
||||
paymentMadeLabel?: string;
|
||||
primaryColor?: string;
|
||||
secondaryColor?: string;
|
||||
showCompanyAddress?: boolean;
|
||||
showCompanyLogo?: boolean;
|
||||
showCustomerAddress?: boolean;
|
||||
showDateIssue?: boolean;
|
||||
showDiscount?: boolean;
|
||||
showDueAmount?: boolean;
|
||||
showDueDate?: boolean;
|
||||
showInvoiceNumber?: boolean;
|
||||
showPaymentMade?: boolean;
|
||||
showStatement?: boolean;
|
||||
showSubtotal?: boolean;
|
||||
showTaxes?: boolean;
|
||||
showTermsConditions?: boolean;
|
||||
showTotal?: boolean;
|
||||
statementLabel?: string;
|
||||
subtotalLabel?: string;
|
||||
termsConditionsLabel?: string;
|
||||
totalLabel?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function useGetSaleInvoiceBrandingTemplate(
|
||||
invoiceId: number,
|
||||
options?: UseQueryOptions<GetSaleInvoiceBrandingTemplateResponse, Error>,
|
||||
): UseQueryResult<GetSaleInvoiceBrandingTemplateResponse, Error> {
|
||||
const apiRequest = useApiRequest();
|
||||
|
||||
return useQuery<GetSaleInvoiceBrandingTemplateResponse, Error>(
|
||||
['SALE_INVOICE_BRANDING_TEMPLATE', invoiceId],
|
||||
() =>
|
||||
apiRequest
|
||||
.get(`/sales/invoices/${invoiceId}/template`)
|
||||
.then((res) => transformToCamelCase(res.data?.data)),
|
||||
{ ...options },
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user