mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 21:00:31 +00:00
feat: sharable payment link dialog
This commit is contained in:
@@ -103,6 +103,13 @@ function InvoiceDetailActionsBar({
|
||||
openDialog(DialogsName.InvoiceMail, { invoiceId });
|
||||
};
|
||||
|
||||
const handleShareButtonClick = () => {
|
||||
openDialog(DialogsName.SharePaymentLink, {
|
||||
transactionId: invoiceId,
|
||||
transactionType: 'SaleInvoice',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<DrawerActionsBar>
|
||||
<NavbarGroup>
|
||||
@@ -150,6 +157,11 @@ function InvoiceDetailActionsBar({
|
||||
onClick={handleDeleteInvoice}
|
||||
/>
|
||||
</Can>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
text={'Share'}
|
||||
onClick={handleShareButtonClick}
|
||||
/>
|
||||
<Can I={SaleInvoiceAction.Writeoff} a={AbilitySubject.Invoice}>
|
||||
<NavbarDivider />
|
||||
<BadDebtMenuItem
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { DialogBody } from '@blueprintjs/core';
|
||||
import { SharePaymentLinkForm } from './SharePaymentLinkForm';
|
||||
import { SharePaymentLinkFormContent } from './SharePaymentLinkFormContent';
|
||||
import { SharePaymentLinkProvider } from './SharePaymentLinkProvider';
|
||||
|
||||
export function SharePaymentLinkContent() {
|
||||
return (
|
||||
<DialogBody>
|
||||
<SharePaymentLinkProvider>
|
||||
<SharePaymentLinkForm>
|
||||
<SharePaymentLinkFormContent />
|
||||
</SharePaymentLinkForm>
|
||||
</SharePaymentLinkProvider>
|
||||
</DialogBody>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Dialog, DialogSuspense } from '@/components';
|
||||
import withDialogRedux from '@/components/DialogReduxConnect';
|
||||
import { compose } from '@/utils';
|
||||
|
||||
const SharePaymentLinkContent = React.lazy(() =>
|
||||
import('./SharePaymentLinkContent').then((module) => ({
|
||||
default: module.SharePaymentLinkContent,
|
||||
})),
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function SharePaymentLinkDialogRoot({ dialogName, payload, isOpen }) {
|
||||
return (
|
||||
<Dialog
|
||||
name={dialogName}
|
||||
isOpen={isOpen}
|
||||
payload={payload}
|
||||
title={'Share Link'}
|
||||
canEscapeJeyClose={true}
|
||||
autoFocus={true}
|
||||
style={{ width: 400 }}
|
||||
>
|
||||
<DialogSuspense>
|
||||
<SharePaymentLinkContent />
|
||||
</DialogSuspense>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export const SharePaymentLinkDialog = compose(withDialogRedux())(
|
||||
SharePaymentLinkDialogRoot,
|
||||
);
|
||||
|
||||
SharePaymentLinkDialog.displayName = 'SharePaymentLinkDialog';
|
||||
@@ -0,0 +1,15 @@
|
||||
import * as Yup from 'yup';
|
||||
|
||||
export const SharePaymentLinkFormSchema = Yup.object().shape({
|
||||
publicity: Yup.string()
|
||||
.oneOf(['private', 'public'], 'Invalid publicity type')
|
||||
.required('Publicity is required'),
|
||||
expiryDate: Yup.date()
|
||||
.nullable()
|
||||
.required('Expiration date is required')
|
||||
.min(new Date(), 'Expiration date must be in the future'),
|
||||
transactionId: Yup.string()
|
||||
.required('Transaction ID is required'),
|
||||
transactionType: Yup.string()
|
||||
.required('Transaction type is required'),
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { Formik, Form, FormikHelpers } from 'formik';
|
||||
import { useCreatePaymentLink } from '@/hooks/query/payment-link';
|
||||
import { AppToaster } from '@/components';
|
||||
import { SharePaymentLinkFormSchema } from './SharePaymentLinkForm.schema';
|
||||
import { useDialogContext } from '@/components/Dialog/DialogProvider';
|
||||
import { useDialogActions } from '@/hooks/state';
|
||||
import { useSharePaymentLink } from './SharePaymentLinkProvider';
|
||||
|
||||
interface SharePaymentLinkFormProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface SharePaymentLinkFormValues {
|
||||
publicity: string;
|
||||
expiryDate: string;
|
||||
transactionId: string;
|
||||
transactionType: string;
|
||||
}
|
||||
|
||||
const initialValues = {
|
||||
publicity: '',
|
||||
expiryDate: '',
|
||||
transactionId: '',
|
||||
transactionType: '',
|
||||
};
|
||||
|
||||
export const SharePaymentLinkForm = ({
|
||||
children,
|
||||
}: SharePaymentLinkFormProps) => {
|
||||
const { mutateAsync: generateShareLink } = useCreatePaymentLink();
|
||||
const { payload } = useDialogContext();
|
||||
const { setUrl } = useSharePaymentLink();
|
||||
|
||||
const transactionId = payload?.transactionId;
|
||||
const transactionType = payload?.transactionType;
|
||||
|
||||
const formInitialValues = {
|
||||
...initialValues,
|
||||
transactionType,
|
||||
transactionId,
|
||||
};
|
||||
const handleFormSubmit = (
|
||||
values: SharePaymentLinkFormValues,
|
||||
{ setSubmitting }: FormikHelpers<SharePaymentLinkFormValues>,
|
||||
) => {
|
||||
setSubmitting(true);
|
||||
generateShareLink(values)
|
||||
.then((res) => {
|
||||
setSubmitting(false);
|
||||
setUrl(res.link?.link);
|
||||
})
|
||||
.catch(() => {
|
||||
setSubmitting(false);
|
||||
AppToaster.show({
|
||||
message: 'Something went wrong.',
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Formik<SharePaymentLinkFormValues>
|
||||
initialValues={formInitialValues}
|
||||
validationSchema={SharePaymentLinkFormSchema}
|
||||
onSubmit={handleFormSubmit}
|
||||
>
|
||||
<Form>{children}</Form>
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
SharePaymentLinkForm.displayName = 'SharePaymentLinkForm';
|
||||
@@ -0,0 +1,86 @@
|
||||
// @ts-nocheck
|
||||
import {
|
||||
Button,
|
||||
FormGroup,
|
||||
InputGroup,
|
||||
Intent,
|
||||
Position,
|
||||
} from '@blueprintjs/core';
|
||||
import {
|
||||
DialogFooter,
|
||||
FDateInput,
|
||||
FFormGroup,
|
||||
FSelect,
|
||||
Icon,
|
||||
Stack,
|
||||
} from '@/components';
|
||||
import { useSharePaymentLink } from './SharePaymentLinkProvider';
|
||||
import { useClipboard } from '@/hooks/utils/useClipboard';
|
||||
|
||||
export function SharePaymentLinkFormContent() {
|
||||
const { url } = useSharePaymentLink();
|
||||
|
||||
const clipboard = useClipboard();
|
||||
|
||||
const handleCopyBtnClick = () => {
|
||||
clipboard.copy(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack>
|
||||
<FSelect
|
||||
name={'publicity'}
|
||||
items={[
|
||||
{ value: 'private', text: 'Private' },
|
||||
{ value: 'public', text: 'Public' },
|
||||
]}
|
||||
fastField
|
||||
/>
|
||||
<FFormGroup name={'expiryDate'} label={'Expiration Date'} fastField>
|
||||
<FDateInput
|
||||
name={'expiryDate'}
|
||||
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
||||
formatDate={(date) => date.toLocaleDateString()}
|
||||
parseDate={(str) => new Date(str)}
|
||||
inputProps={{
|
||||
fill: true,
|
||||
leftElement: <Icon icon={'date-range'} />,
|
||||
}}
|
||||
fastField
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
{url && (
|
||||
<FormGroup name={'link'} label={'Payment Link'}>
|
||||
<InputGroup
|
||||
name={'link'}
|
||||
value={url}
|
||||
disabled
|
||||
leftElement={
|
||||
<Button onClick={handleCopyBtnClick} minimal>
|
||||
Copy
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<DialogFooter>
|
||||
{url ? (
|
||||
<Button intent={Intent.PRIMARY} onClick={handleCopyBtnClick}>
|
||||
Copy Link
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button type={'submit'} intent={Intent.PRIMARY}>
|
||||
Generate
|
||||
</Button>
|
||||
<Button>Cancel</Button>
|
||||
</>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import React, { createContext, useContext, useState, ReactNode } from 'react';
|
||||
|
||||
interface SharePaymentLinkContextType {
|
||||
url: string;
|
||||
setUrl: React.Dispatch<React.SetStateAction<string>>;
|
||||
}
|
||||
|
||||
const SharePaymentLinkContext =
|
||||
createContext<SharePaymentLinkContextType | null>(null);
|
||||
|
||||
interface SharePaymentLinkProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const SharePaymentLinkProvider: React.FC<
|
||||
SharePaymentLinkProviderProps
|
||||
> = ({ children }) => {
|
||||
const [url, setUrl] = useState<string>('');
|
||||
|
||||
return (
|
||||
<SharePaymentLinkContext.Provider value={{ url, setUrl }}>
|
||||
{children}
|
||||
</SharePaymentLinkContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useSharePaymentLink = () => {
|
||||
const context = useContext(SharePaymentLinkContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useSharePaymentLink must be used within a SharePaymentLinkProvider',
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -2,27 +2,31 @@ import { Text, Classes, Button, Intent } from '@blueprintjs/core';
|
||||
import clsx from 'classnames';
|
||||
import { Box, Group, Stack } from '@/components';
|
||||
import styles from './PaymentPortal.module.scss';
|
||||
import { usePaymentPortalBoot } from './PaymentPortalBoot';
|
||||
|
||||
export function PaymentPortal() {
|
||||
const { sharableLinkMeta } = usePaymentPortalBoot();
|
||||
|
||||
return (
|
||||
<Box className={styles.root}>
|
||||
<Stack className={styles.body}>
|
||||
<Group spacing={8}>
|
||||
<Box className={styles.companyLogoWrap}></Box>
|
||||
<Text>Bigcapital Technology, Inc.</Text>
|
||||
<Text>{sharableLinkMeta?.companyName}</Text>
|
||||
</Group>
|
||||
|
||||
<Stack spacing={6}>
|
||||
<h1 className={styles.bigTitle}>
|
||||
Bigcapital Technology, Inc. Sent an Invoice for $1000.00
|
||||
{sharableLinkMeta?.companyName} Sent an Invoice for{' '}
|
||||
{sharableLinkMeta?.totalFormatted}
|
||||
</h1>
|
||||
<Text className={clsx(Classes.TEXT_MUTED, styles.invoiceDueDate)}>
|
||||
Invoice due September 13, 2024
|
||||
Invoice due {sharableLinkMeta?.dueDateFormatted}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack spacing={2}>
|
||||
<Box className={styles.customerName}>Ahmed Bouhuolia</Box>
|
||||
<Box className={styles.customerName}>{sharableLinkMeta?.customerName}</Box>
|
||||
<Box>Bigcapital Technology, Inc.</Box>
|
||||
<Box>131 Continental Dr Suite 305 Newark,</Box>
|
||||
<Box>Delaware 19713</Box>
|
||||
@@ -30,7 +34,9 @@ export function PaymentPortal() {
|
||||
<Box>ahmed@bigcapital.app</Box>
|
||||
</Stack>
|
||||
|
||||
<h2 className={styles.invoiceNumber}>Invoice INV-000001</h2>
|
||||
<h2 className={styles.invoiceNumber}>
|
||||
Invoice {sharableLinkMeta?.invoiceNo}
|
||||
</h2>
|
||||
|
||||
<Stack spacing={0} className={styles.totals}>
|
||||
<Group
|
||||
@@ -38,12 +44,14 @@ export function PaymentPortal() {
|
||||
className={clsx(styles.totalItem, styles.borderBottomGray)}
|
||||
>
|
||||
<Text>Sub Total</Text>
|
||||
<Text>11.00</Text>
|
||||
<Text>{sharableLinkMeta?.subtotalFormatted}</Text>
|
||||
</Group>
|
||||
|
||||
<Group position={'apart'} className={styles.totalItem}>
|
||||
<Text>Total</Text>
|
||||
<Text style={{ fontWeight: 600 }}>11.00</Text>
|
||||
<Text style={{ fontWeight: 600 }}>
|
||||
{sharableLinkMeta?.totalFormatted}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
<Group
|
||||
@@ -51,7 +59,7 @@ export function PaymentPortal() {
|
||||
className={clsx(styles.totalItem, styles.borderBottomDark)}
|
||||
>
|
||||
<Text>Paid Amount (-)</Text>
|
||||
<Text>11.00</Text>
|
||||
<Text>{sharableLinkMeta?.paymentAmountFormatted}</Text>
|
||||
</Group>
|
||||
|
||||
<Group
|
||||
@@ -59,7 +67,9 @@ export function PaymentPortal() {
|
||||
className={clsx(styles.totalItem, styles.borderBottomDark)}
|
||||
>
|
||||
<Text>Due Amount</Text>
|
||||
<Text style={{ fontWeight: 600 }}>11.00</Text>
|
||||
<Text style={{ fontWeight: 600 }}>
|
||||
{sharableLinkMeta?.dueAmountFormatted}
|
||||
</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
@@ -79,7 +89,7 @@ export function PaymentPortal() {
|
||||
intent={Intent.PRIMARY}
|
||||
className={clsx(styles.footerButton, styles.buyButton)}
|
||||
>
|
||||
Pay $10,000.00
|
||||
Pay {sharableLinkMeta?.totalFormatted}
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
|
||||
@@ -1,22 +1,37 @@
|
||||
import React, { createContext, useContext, ReactNode } from 'react';
|
||||
import {
|
||||
GetSharableLinkMetaResponse,
|
||||
useGetSharableLinkMeta,
|
||||
} from '@/hooks/query/payment-link';
|
||||
|
||||
interface PaymentPortalContextType {
|
||||
// Define the context type here
|
||||
paymentAmount: number;
|
||||
setPaymentAmount: (amount: number) => void;
|
||||
sharableLinkMeta: GetSharableLinkMetaResponse | undefined;
|
||||
isSharableLinkMetaLoading: boolean;
|
||||
}
|
||||
|
||||
const PaymentPortalContext = createContext<PaymentPortalContextType>(
|
||||
{} as PaymentPortalContextType,
|
||||
);
|
||||
|
||||
export const PaymentPortalBoot: React.FC<{ children: ReactNode }> = ({
|
||||
interface PaymentPortalBootProps {
|
||||
linkId: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const PaymentPortalBoot: React.FC<PaymentPortalBootProps> = ({
|
||||
linkId,
|
||||
children,
|
||||
}) => {
|
||||
const [paymentAmount, setPaymentAmount] = React.useState<number>(0);
|
||||
const { data: sharableLinkMeta, isLoading: isSharableLinkMetaLoading } =
|
||||
useGetSharableLinkMeta(linkId);
|
||||
|
||||
const value = {
|
||||
sharableLinkMeta,
|
||||
isSharableLinkMetaLoading,
|
||||
};
|
||||
|
||||
return (
|
||||
<PaymentPortalContext.Provider value={{ paymentAmount, setPaymentAmount }}>
|
||||
<PaymentPortalContext.Provider value={value}>
|
||||
{children}
|
||||
</PaymentPortalContext.Provider>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { PaymentPortal } from './PaymentPortal';
|
||||
import { PaymentPortalBoot } from './PaymentPortalBoot';
|
||||
|
||||
export default function PaymentPortalPage() {
|
||||
return <PaymentPortal />;
|
||||
const { linkId } = useParams<{ linkId: string}>();
|
||||
|
||||
return (
|
||||
<PaymentPortalBoot linkId={linkId}>
|
||||
<PaymentPortal />
|
||||
</PaymentPortalBoot>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user