Skip to content

Commit

Permalink
Merge pull request #387 from team-monite/DEV-12623-sdk-payment-record…
Browse files Browse the repository at this point in the history
…s-implementation

feat(DEV-12623): manual payment record implementation
  • Loading branch information
PeCrio authored Oct 29, 2024
2 parents bff3743 + b878f5d commit ac1bcc9
Show file tree
Hide file tree
Showing 19 changed files with 1,279 additions and 22 deletions.
5 changes: 5 additions & 0 deletions .changeset/stale-islands-collect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@monite/sdk-react': minor
---

feat(DEV-12623): manual payment record implementation
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Controller, FieldError } from 'react-hook-form';
import type { FieldValues, UseControllerProps } from 'react-hook-form';

import { Alert } from '@mui/material';
import { TimeField } from '@mui/x-date-pickers';
import type { TimePickerProps } from '@mui/x-date-pickers';

export const RHFTimePicker = <T extends FieldValues>({
control,
name,
required,
fullWidth,
slotProps,
...other
}: UseControllerProps<T> &
TimePickerProps<Date> & { required?: boolean; fullWidth?: boolean }) => {
const isErrorCustom = (error: FieldError | undefined) =>
error?.type === 'custom';

return (
<Controller
control={control}
name={name}
render={({
field,
fieldState: { error, isTouched },
formState: { isValid },
}) => {
const isInvalid = (isTouched || !isValid) && !isErrorCustom(error);

const date = field.value !== null ? new Date(field.value) : null;

return (
<>
<TimeField
{...field}
{...other}
required={required}
fullWidth={fullWidth}
value={date}
helperText={isInvalid && error?.message}
slotProps={{
...slotProps,
textField: {
...slotProps?.textField,
id: name,
error: isInvalid && !!error?.message,
helperText: isInvalid && error?.message,
},
}}
/>
{isErrorCustom(error) && (
<Alert
severity="error"
icon={false}
sx={{
marginTop: -2,
}}
>
{error?.message}
</Alert>
)}
</>
);
}}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './RHFTimePicker';
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
import { styled, alpha } from '@mui/material/styles';

import { useRecurrenceByInvoiceId } from './components/ReceivableRecurrence/useInvoiceRecurrence';
import { RecordManualPaymentModal } from './components/TabPanels/PaymentTabPanel/RecordManualPaymentModal';
import {
DeliveryMethod,
ExistingInvoiceDetailsView,
Expand Down Expand Up @@ -325,6 +326,16 @@ const ExistingInvoiceDetailsBase = (props: ExistingReceivableDetailsProps) => {
disabled={loading}
>{t(i18n)`Download PDF`}</Button>
)}
<RecordManualPaymentModal invoice={receivable}>
{({ openModal }) => (
<Button
variant="contained"
color="primary"
onClick={openModal}
disabled={loading}
>{t(i18n)`Record payment`}</Button>
)}
</RecordManualPaymentModal>
{buttons.isComposeEmailButtonVisible && (
<Button
variant="contained"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ import { PreviewCustomerSection } from './sections/PreviewCustomerSection';
import { PreviewDetailsSection } from './sections/PreviewDetailsSection';
import { PreviewItemsSection } from './sections/PreviewItemsSection';
import { PreviewPaymentDetailsSection } from './sections/PreviewPaymentDetailsSection';
import { PaymentTabPanel } from './TabPanels/PaymentTabPanel/PaymentTabPanel';

export const Overview = (
invoice: components['schemas']['InvoiceResponsePayload']
) => {
const { i18n } = useLingui();
const [view, setView] = useState<'overview' | 'details' | 'recurrence'>(
'overview'
);
const [view, setView] = useState<
'overview' | 'details' | 'recurrence' | 'payments'
>('overview');
const { data: recurrence, isLoading: isRecurrenceLoading } =
useRecurrenceByInvoiceId(invoice.id);
const tabsBaseId = `Monite-InvoiceDetails-overview-${useId()}-tab-`;
Expand Down Expand Up @@ -53,6 +54,12 @@ export const Overview = (
value="recurrence"
/>
)}
<Tab
label={t(i18n)`Payments`}
id={`${tabsBaseId}-payments-tab`}
aria-controls={`${tabsBaseId}-payments-tabpanel`}
value="payments"
/>
</Tabs>

{view === 'overview' && (
Expand Down Expand Up @@ -95,6 +102,16 @@ export const Overview = (
</Card>
</Box>
)}

{view === 'payments' && (
<Box
role="tabpanel"
id={`${tabsBaseId}-payments-tabpanel`}
aria-labelledby={`${tabsBaseId}-payments-tab`}
>
<PaymentTabPanel invoice={invoice} />
</Box>
)}
</Stack>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { components } from '@/api';
import { useCurrencies } from '@/core/hooks';
import { useEntityUserByAuthToken } from '@/core/queries';
import { MoniteCard } from '@/ui/Card/Card';
import { useDateTimeFormat } from '@/utils';
import { t } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import DriveFileRenameOutlineIcon from '@mui/icons-material/DriveFileRenameOutline';
import { Alert, Box, Skeleton, Typography } from '@mui/material';

import { PaymentRecordDetails } from './RecordManualPaymentModal';

type Props = {
paymentRecords: PaymentRecordDetails;
invoice: components['schemas']['InvoiceResponsePayload'];
};

export const ManualPaymentRecordDetails = ({
paymentRecords,
invoice,
}: Props) => {
const { i18n } = useLingui();
const dateTimeFormat = useDateTimeFormat();
const { formatCurrencyToDisplay } = useCurrencies();

const dateTime = i18n.date(
new Date(paymentRecords.payment_date ?? ''),
dateTimeFormat
);

const { data: entityUser, isLoading: isEntityUserLoading } =
useEntityUserByAuthToken();

const paymentAuthor = `${entityUser?.first_name} ${entityUser?.last_name}`;

return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: 1,
alignItems: 'center',
textAlign: 'center',
}}
>
<Box
sx={{
borderRadius: '100%',
backgroundColor: '#F2F2F2',
width: '44px',
height: '44px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
<DriveFileRenameOutlineIcon sx={{ color: '#707070' }} />
</Box>
<Typography variant="body1">{t(
i18n
)`Manual payment record`}</Typography>
<Box>
<Typography variant="h2">
{formatCurrencyToDisplay(paymentRecords.amount, invoice.currency)}
</Typography>
<Typography variant="body1">{t(
i18n
)`from ${invoice.counterpart_name}`}</Typography>
</Box>
</Box>

<MoniteCard
items={[
{
label: t(i18n)`Received`,
value: !paymentRecords.payment_date ? (
'—'
) : (
<Typography fontWeight={500}>{dateTime}</Typography>
),
},
{
label: t(i18n)`Reference`,
value: !invoice.id ? (
'—'
) : (
<Typography fontWeight={500}>{invoice.id}</Typography>
),
},
{
label: t(i18n)`Created by`,
value: isEntityUserLoading ? (
<Skeleton variant="text" width="50%" />
) : !paymentAuthor ? (
'—'
) : (
<Typography fontWeight={500}>{paymentAuthor}</Typography>
),
},
]}
/>

<Alert severity="warning">
<Typography variant="body2">{t(
i18n
)`Please, check the details of your payment record.
You won't be able to change or delete it after.`}</Typography>
</Alert>
</Box>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { useEffect, useMemo } from 'react';
import { useForm } from 'react-hook-form';

import { components } from '@/api';
import { RHFDatePicker } from '@/components/RHF/RHFDatePicker';
import { RHFTextField } from '@/components/RHF/RHFTextField';
import { useCurrencies } from '@/core/hooks';
import { yupResolver } from '@hookform/resolvers/yup';
import { t } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import {
Button,
CircularProgress,
DialogActions,
DialogContent,
DialogTitle,
FormHelperText,
Grid,
} from '@mui/material';

import { PaymentRecordDetails } from './RecordManualPaymentModal';
import { manualPaymentRecordValidationSchema } from './schemas/manualPaymentRecordValidationSchema';

export type PaymentRecordFormValues = Omit<PaymentRecordDetails, 'created_by'>;
type Props = {
invoice: components['schemas']['InvoiceResponsePayload'];
initialValues?: PaymentRecordFormValues;
isLoading: boolean;
onCancel: () => void;
onSubmit: (data: PaymentRecordFormValues) => void;
};

export const PaymentRecordForm = ({
invoice,
initialValues,
isLoading,
onSubmit,
onCancel,
}: Props) => {
const { i18n } = useLingui();
const { formatCurrencyToDisplay } = useCurrencies();

const { control, handleSubmit, reset } = useForm<PaymentRecordFormValues>({
resolver: yupResolver(
manualPaymentRecordValidationSchema(i18n, invoice.amount_due)
),
defaultValues: useMemo(
() =>
initialValues ?? {
amount: 0,
payment_date: null,
},
[initialValues]
),
});

useEffect(() => {
reset(initialValues);
}, [initialValues, reset]);

return (
<>
<form onSubmit={handleSubmit(onSubmit)}>
<DialogTitle sx={{ px: 4, pt: 4 }} variant="h3" id="dialog-title">{t(
i18n
)`Record payment`}</DialogTitle>
<DialogContent sx={{ p: 4 }}>
<Grid container spacing={2}>
<Grid item xs={6}>
<RHFTextField
label={t(i18n)`Amount`}
name="amount"
control={control}
fullWidth
required
/>
<FormHelperText>
{t(i18n)`Enter full amount due of`}{' '}
{formatCurrencyToDisplay(invoice.amount_due, invoice.currency)}
</FormHelperText>
</Grid>
<Grid item xs={6}>
<RHFDatePicker
label={t(i18n)`Date`}
name="payment_date"
control={control}
/>
</Grid>
</Grid>
</DialogContent>
<DialogActions sx={{ px: 4, pb: 4 }}>
<Button autoFocus onClick={onCancel}>
{t(i18n)`Cancel`}
</Button>
<Button
variant="contained"
startIcon={
isLoading ? <CircularProgress size={20} color="warning" /> : null
}
type="submit"
>
{t(i18n)`Save`}
</Button>
</DialogActions>
</form>
</>
);
};
Loading

0 comments on commit ac1bcc9

Please sign in to comment.