1
0
This commit is contained in:
Ahmed Bouhuolia
2026-03-26 14:18:54 +02:00
parent 8f1af97fc0
commit 75699ba810
18 changed files with 382 additions and 252 deletions
@@ -12,7 +12,7 @@ import CustomerDetailsDrawer from '@/containers/Drawers/CustomerDetailsDrawer';
import VendorDetailsDrawer from '@/containers/Drawers/VendorDetailsDrawer'; import VendorDetailsDrawer from '@/containers/Drawers/VendorDetailsDrawer';
import InventoryAdjustmentDetailDrawer from '@/containers/Drawers/InventoryAdjustmentDetailDrawer'; import InventoryAdjustmentDetailDrawer from '@/containers/Drawers/InventoryAdjustmentDetailDrawer';
import CashflowTransactionDetailDrawer from '@/containers/Drawers/CashflowTransactionDetailDrawer'; import CashflowTransactionDetailDrawer from '@/containers/Drawers/CashflowTransactionDetailDrawer';
import QuickCreateCustomerDrawer from '@/containers/Drawers/QuickCreateCustomerDrawer'; // import QuickCreateCustomerDrawer from '@/containers/Drawers/QuickCreateCustomerDrawer';
import QuickCreateItemDrawer from '@/containers/Drawers/QuickCreateItemDrawer'; import QuickCreateItemDrawer from '@/containers/Drawers/QuickCreateItemDrawer';
import QuickWriteVendorDrawer from '@/containers/Drawers/QuickWriteVendorDrawer'; import QuickWriteVendorDrawer from '@/containers/Drawers/QuickWriteVendorDrawer';
import CreditNoteDetailDrawer from '@/containers/Drawers/CreditNoteDetailDrawer'; import CreditNoteDetailDrawer from '@/containers/Drawers/CreditNoteDetailDrawer';
@@ -59,7 +59,7 @@ export default function DrawersContainer() {
<CashflowTransactionDetailDrawer <CashflowTransactionDetailDrawer
name={DRAWERS.CASHFLOW_TRNASACTION_DETAILS} name={DRAWERS.CASHFLOW_TRNASACTION_DETAILS}
/> />
<QuickCreateCustomerDrawer name={DRAWERS.QUICK_CREATE_CUSTOMER} /> {/* <QuickCreateCustomerDrawer name={DRAWERS.QUICK_CREATE_CUSTOMER} /> */}
<QuickCreateItemDrawer name={DRAWERS.QUICK_CREATE_ITEM} /> <QuickCreateItemDrawer name={DRAWERS.QUICK_CREATE_ITEM} />
<QuickWriteVendorDrawer name={DRAWERS.QUICK_WRITE_VENDOR} /> <QuickWriteVendorDrawer name={DRAWERS.QUICK_WRITE_VENDOR} />
<CreditNoteDetailDrawer name={DRAWERS.CREDIT_NOTE_DETAILS} /> <CreditNoteDetailDrawer name={DRAWERS.CREDIT_NOTE_DETAILS} />
@@ -4,6 +4,11 @@ import { FSelect } from '../Forms';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
export type DisplayNameListItem = { label: string }; export type DisplayNameListItem = { label: string };
type DisplayNameFormat = {
format: string;
values: Array<string | undefined>;
required: number[];
};
export interface DisplayNameListProps export interface DisplayNameListProps
extends Omit< extends Omit<
@@ -11,6 +16,47 @@ export interface DisplayNameListProps
'items' | 'valueAccessor' | 'textAccessor' | 'labelAccessor' 'items' | 'valueAccessor' | 'textAccessor' | 'labelAccessor'
> {} > {}
function useDisplayNameFormatOptions(
salutation?: string,
firstName?: string,
lastName?: string,
companyName?: string,
): DisplayNameListItem[] {
return useMemo(() => {
const formats: DisplayNameFormat[] = [
{
format: '{1} {2} {3}',
values: [salutation, firstName, lastName],
required: [1],
},
{ format: '{1} {2}', values: [firstName, lastName], required: [] },
{ format: '{1}, {2}', values: [firstName, lastName], required: [1, 2] },
{ format: '{1}', values: [companyName], required: [1] },
];
return formats
.filter(
(format) =>
!format.values.some((value, index) => {
return !value && format.required.indexOf(index + 1) !== -1;
}),
)
.map((formatOption) => {
const { format, values } = formatOption;
let label = format;
values.forEach((value, index) => {
const replaceWith = value || '';
label = label.replace(`{${index + 1}}`, replaceWith).trim();
});
return {
label: label.replace(/\s+/g, ' ').replace(/\s+,/g, ',').trim(),
};
})
.filter(({ label }) => Boolean(label));
}, [salutation, firstName, lastName, companyName]);
}
export function DisplayNameList({ ...restProps }: DisplayNameListProps) { export function DisplayNameList({ ...restProps }: DisplayNameListProps) {
const { const {
values: { values: {
@@ -21,40 +67,11 @@ export function DisplayNameList({ ...restProps }: DisplayNameListProps) {
}, },
} = useFormikContext<any>(); } = useFormikContext<any>();
const formats = useMemo( const formatOptions = useDisplayNameFormatOptions(
() => [ salutation,
{ firstName,
format: '{1} {2} {3}', lastName,
values: [salutation, firstName, lastName], companyName,
required: [1],
},
{ format: '{1} {2}', values: [firstName, lastName], required: [] },
{ format: '{1}, {2}', values: [firstName, lastName], required: [1, 2] },
{ format: '{1}', values: [companyName], required: [1] },
],
[firstName, lastName, companyName, salutation],
);
const formatOptions: DisplayNameListItem[] = useMemo(
() =>
formats
.filter(
(format) =>
!format.values.some((value, index) => {
return !value && format.required.indexOf(index + 1) !== -1;
}),
)
.map((formatOption) => {
const { format, values } = formatOption;
let label = format;
values.forEach((value, index) => {
const replaceWith = value || '';
label = label.replace(`{${index + 1}}`, replaceWith).trim();
});
return { label: label.replace(/\s+/g, ' ') };
}),
[formats],
); );
return ( return (
@@ -62,6 +79,7 @@ export function DisplayNameList({ ...restProps }: DisplayNameListProps) {
items={formatOptions} items={formatOptions}
valueAccessor={'label'} valueAccessor={'label'}
textAccessor={'label'} textAccessor={'label'}
labelAccessor={'_label'}
placeholder={intl.get('select_display_name_as')} placeholder={intl.get('select_display_name_as')}
filterable={false} filterable={false}
{...restProps} {...restProps}
@@ -7,11 +7,11 @@ import {
FInputGroup, FInputGroup,
FTextArea, FTextArea,
} from '@/components'; } from '@/components';
import CustomerFormSectionTitle from './CustomerFormSectionTitle'; import { CustomerFormSectionTitle } from './CustomerFormSectionTitle';
export default function CustomerBillingAddress() { export function CustomerBillingAddress() {
return ( return (
<Box> <Box data-section-id="billingAddress">
<CustomerFormSectionTitle> <CustomerFormSectionTitle>
<T id={'billing_address'} /> <T id={'billing_address'} />
</CustomerFormSectionTitle> </CustomerFormSectionTitle>
@@ -1,4 +1,3 @@
// @ts-nocheck
import React from 'react'; import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { import {
@@ -11,40 +10,32 @@ import {
Menu, Menu,
MenuItem, MenuItem,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import classNames from 'classnames';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import { Group, Icon, FormattedMessage as T } from '@/components'; import { Group, Icon, FormattedMessage as T } from '@/components';
import { CLASSES } from '@/constants/classes';
import { useCustomerFormContext } from './CustomerFormProvider'; import { useCustomerFormContext } from './CustomerFormProvider';
import { safeInvoke } from '@/utils';
/** export function CustomerFloatingActions() {
* Customer floating actions bar.
*/
export function CustomerFloatingActions({ onCancel }) {
// Customer form context. // Customer form context.
const { isNewMode, setSubmitPayload } = useCustomerFormContext(); const { isNewMode, setSubmitPayload } = useCustomerFormContext() as {
isNewMode: boolean;
setSubmitPayload: (payload: { noRedirect: boolean }) => void;
};
// Formik context. // Formik context.
const { resetForm, submitForm, isSubmitting } = useFormikContext(); const { resetForm, submitForm, isSubmitting } = useFormikContext();
// Handle submit button click. // Handle submit button click.
const handleSubmitBtnClick = (event) => { const handleSubmitBtnClick = (_event: React.MouseEvent<HTMLElement>) => {
setSubmitPayload({ noRedirect: false }); setSubmitPayload({ noRedirect: false });
}; };
// Handle cancel button click.
const handleCancelBtnClick = (event) => {
safeInvoke(onCancel, event);
};
// handle clear button clicl. // handle clear button clicl.
const handleClearBtnClick = (event) => { const handleClearBtnClick = (_event: React.MouseEvent<HTMLElement>) => {
resetForm(); resetForm();
}; };
// Handle submit & new button click. // Handle submit & new button click.
const handleSubmitAndNewClick = (event) => { const handleSubmitAndNewClick = (_event: React.MouseEvent<HTMLElement>) => {
submitForm(); submitForm();
setSubmitPayload({ noRedirect: true }); setSubmitPayload({ noRedirect: true });
}; };
@@ -1,15 +0,0 @@
// @ts-nocheck
import React from 'react';
import { CustomerFormProvider } from './CustomerFormProvider';
import CustomerFormFormik from './CustomerFormFormik';
/**
* Abstracted customer form.
*/
export default function CustomerForm({ customerId }) {
return (
<CustomerFormProvider customerId={customerId}>
<CustomerFormFormik />
</CustomerFormProvider>
);
}
@@ -14,18 +14,15 @@ import {
Icon, Icon,
Stack, Stack,
} from '@/components'; } from '@/components';
import CustomerTypeRadioField from './CustomerTypeRadioField'; import { CustomerTypeRadioField } from './CustomerTypeRadioField';
import CustomerFormSectionTitle from './CustomerFormSectionTitle'; import { CustomerFormSectionTitle } from './CustomerFormSectionTitle';
import { useAutofocus } from '@/hooks'; import { useAutofocus } from '@/hooks';
/** export function CustomerFormBasicSection({}) {
* Customer form primary section.
*/
export default function CustomerFormPrimarySection({}) {
const firstNameFieldRef = useAutofocus(); const firstNameFieldRef = useAutofocus();
return ( return (
<Box> <Box data-section-id="primary">
<CustomerFormSectionTitle>Customer details</CustomerFormSectionTitle> <CustomerFormSectionTitle>Customer details</CustomerFormSectionTitle>
{/**-----------Customer type. -----------*/} {/**-----------Customer type. -----------*/}
@@ -102,7 +99,7 @@ export default function CustomerFormPrimarySection({}) {
<FFormGroup <FFormGroup
name={'email'} name={'email'}
label={<T id={'vendor_email'} />} label={<T id={'vendor_email'} />}
inline={true} inline
> >
<FInputGroup <FInputGroup
name={'email'} name={'email'}
@@ -118,7 +115,11 @@ export default function CustomerFormPrimarySection({}) {
inline={true} inline={true}
> >
<Stack spacing={10}> <Stack spacing={10}>
<FInputGroup name={'work_phone'} placeholder={intl.get('work')} /> <FInputGroup
name={'work_phone'}
placeholder={intl.get('work')}
leftIcon="phone"
/>
<FInputGroup <FInputGroup
name={'personal_phone'} name={'personal_phone'}
placeholder={intl.get('mobile')} placeholder={intl.get('mobile')}
@@ -0,0 +1,53 @@
import { Tab } from "@blueprintjs/core";
import { Card, Group } from "@/components";
import { Tabs } from "@blueprintjs/core";
import { useState } from "react";
import { css } from '@emotion/css';
import { CustomerFloatingActions } from "./CustomerFloatingActions";
import { CustomerFormSections } from "./CustomerFormFields";
const customerFormSections = {
primary: 'primary',
financial: 'financial',
billingAddress: 'billingAddress',
shippingAddress: 'shippingAddress',
notes: 'notes',
};
export function CustomerFormContent() {
const [selectedTabId, setSelectedTabId] = useState(customerFormSections.primary);
const handleTabChange = (tabId: string) => {
const sectionId = String(tabId);
setSelectedTabId(sectionId);
const section = document.querySelector(
`[data-section-id="${sectionId}"]`,
);
if (section) {
section.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
return (
<Card className={css`padding-bottom: 0 !important;`}>
<Group verticalAlign={'top'} alignItems={'flex-start'} flexWrap={'nowrap'}>
<Tabs
vertical
large
selectedTabId={selectedTabId}
onChange={handleTabChange}
className={css`position: sticky; top: 20px; .bp4-large > .bp4-tab{font-size: 14px;} `}
>
<Tab id={customerFormSections.primary} title={'Basic'} />
<Tab id={customerFormSections.financial} title={'Financial'} />
<Tab id={customerFormSections.billingAddress} title={'Billing address'} />
<Tab id={customerFormSections.shippingAddress} title={'Ship address'} />
<Tab id={customerFormSections.notes} title={'Notes'} />
</Tabs>
<CustomerFormSections />
</Group>
<CustomerFloatingActions />
</Card>
)
}
@@ -0,0 +1,33 @@
import { Divider } from '@blueprintjs/core';
import { css } from '@emotion/css';
import { Box } from '@/components';
import { CustomerFormBasicSection } from './CustomerFormBasicSection';
import { CustomerFormFinancialSection } from './CustomerFormFinancialSection';
import { CustomerBillingAddress } from './CustomerBillingAddress';
import { CustomerShippingAddress } from './CustomerShippingAddress';
import { CustomerFormNotesSection } from './CustomerFormNotesSection';
const customerFormSectionDividerClass = css`
margin: 20px 0;
`;
export function CustomerFormSections() {
return (
<Box>
<CustomerFormBasicSection />
<Divider className={customerFormSectionDividerClass} />
<CustomerFormFinancialSection />
<Divider className={customerFormSectionDividerClass} />
<CustomerBillingAddress />
<Divider className={customerFormSectionDividerClass} />
<CustomerShippingAddress />
<Divider className={customerFormSectionDividerClass} />
<CustomerFormNotesSection />
</Box>
);
}
@@ -1,4 +1,3 @@
// @ts-nocheck
import React from 'react'; import React from 'react';
import { FormGroup, Position, ControlGroup } from '@blueprintjs/core'; import { FormGroup, Position, ControlGroup } from '@blueprintjs/core';
import { ErrorMessage, useFormikContext } from 'formik'; import { ErrorMessage, useFormikContext } from 'formik';
@@ -10,8 +9,6 @@ import {
CurrencySelectList, CurrencySelectList,
BranchSelect, BranchSelect,
FeatureCan, FeatureCan,
Row,
Col,
FMoneyInputGroup, FMoneyInputGroup,
ExchangeRateInputGroup, ExchangeRateInputGroup,
FDateInput, FDateInput,
@@ -24,23 +21,20 @@ import {
useSetPrimaryBranchToForm, useSetPrimaryBranchToForm,
} from './utils'; } from './utils';
import { useCurrentOrganization } from '@/hooks/state'; import { useCurrentOrganization } from '@/hooks/state';
import CustomerFormSectionTitle from './CustomerFormSectionTitle'; import { CustomerFormSectionTitle } from './CustomerFormSectionTitle';
/** export function CustomerFormFinancialSection() {
* Customer financial panel.
*/
export default function CustomerFinancialPanel() {
const { currencies, customerId, branches } = useCustomerFormContext(); const { currencies, customerId, branches } = useCustomerFormContext();
// Sets the primary branch to form. // Sets the primary branch to form.
useSetPrimaryBranchToForm(); useSetPrimaryBranchToForm();
return ( return (
<Box> <Box data-section-id="financial">
<CustomerFormSectionTitle> <CustomerFormSectionTitle>
<T id={'financial'} /> <T id={'financial'} />
</CustomerFormSectionTitle> </CustomerFormSectionTitle>
{/*------------ Currency -----------*/}
<FFormGroup <FFormGroup
name={'currency_code'} name={'currency_code'}
label={<T id={'currency'} />} label={<T id={'currency'} />}
@@ -55,16 +49,10 @@ export default function CustomerFinancialPanel() {
/> />
</FFormGroup> </FFormGroup>
{/*------------ Opening balance -----------*/}
<CustomerOpeningBalanceField /> <CustomerOpeningBalanceField />
{/*------ Opening Balance Exchange Rate -----*/}
<CustomerOpeningBalanceExchangeRateField /> <CustomerOpeningBalanceExchangeRateField />
{/*------------ Opening balance at -----------*/}
<CustomerOpeningBalanceAtField /> <CustomerOpeningBalanceAtField />
{/*------------ Opening branch -----------*/}
<FeatureCan feature={Features.Branches}> <FeatureCan feature={Features.Branches}>
<FFormGroup <FFormGroup
label={<T id={'customer.label.opening_branch'} />} label={<T id={'customer.label.opening_branch'} />}
@@ -94,7 +82,7 @@ function CustomerOpeningBalanceAtField() {
if (customerId) return null; if (customerId) return null;
return ( return (
<FormGroup <FFormGroup
name={'opening_balance_at'} name={'opening_balance_at'}
label={<T id={'opening_balance_at'} />} label={<T id={'opening_balance_at'} />}
inline inline
@@ -109,14 +97,10 @@ function CustomerOpeningBalanceAtField() {
parseDate={(str) => new Date(str)} parseDate={(str) => new Date(str)}
fill={true} fill={true}
/> />
</FormGroup> </FFormGroup>
); );
} }
/**
* Customer opening balance field.
* @returns {JSX.Element}
*/
function CustomerOpeningBalanceField() { function CustomerOpeningBalanceField() {
const { customerId } = useCustomerFormContext(); const { customerId } = useCustomerFormContext();
const { values } = useFormikContext(); const { values } = useFormikContext();
@@ -129,15 +113,16 @@ function CustomerOpeningBalanceField() {
label={<T id={'opening_balance'} />} label={<T id={'opening_balance'} />}
name={'opening_balance'} name={'opening_balance'}
inline inline
fill
shouldUpdate={openingBalanceFieldShouldUpdate} shouldUpdate={openingBalanceFieldShouldUpdate}
shouldUpdateDeps={{ currencyCode: values.currency_code }} shouldUpdateDeps={{ currencyCode: values.currency_code }}
fastField={true} fastField={true}
fill
> >
<ControlGroup fill> <ControlGroup fill>
<InputPrependText text={values.currency_code} /> <InputPrependText text={values.currency_code as string} />
<FMoneyInputGroup <FMoneyInputGroup
name={'opening_balance'} name={'opening_balance'}
fastField
inputGroupProps={{ fill: true }} inputGroupProps={{ fill: true }}
/> />
</ControlGroup> </ControlGroup>
@@ -145,11 +130,6 @@ function CustomerOpeningBalanceField() {
); );
} }
/**
* Customer opening balance exchange rate field if the customer has foreign
* currency.
* @returns {JSX.Element}
*/
function CustomerOpeningBalanceExchangeRateField() { function CustomerOpeningBalanceExchangeRateField() {
const { values } = useFormikContext(); const { values } = useFormikContext();
const { customerId } = useCustomerFormContext(); const { customerId } = useCustomerFormContext();
@@ -162,17 +142,14 @@ function CustomerOpeningBalanceExchangeRateField() {
return null; return null;
} }
return ( return (
<FFormGroup
label={' '}
name={'opening_balance_exchange_rate'}
inline
fill
>
<ExchangeRateInputGroup <ExchangeRateInputGroup
fromCurrency={values.currency_code} fromCurrency={values.currency_code}
toCurrency={currentOrganization.base_currency} toCurrency={currentOrganization.base_currency}
name={'opening_balance_exchange_rate'} name={'opening_balance_exchange_rate'}
onRecalcConfirm={() => {}}
onCancel={() => {}}
formGroupProps={{ label: ' ' }}
/> />
</FFormGroup>
); );
} }
@@ -1,39 +1,97 @@
// @ts-nocheck import { useMemo } from 'react';
import React, { useMemo } from 'react';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import classNames from 'classnames'; import { Formik, Form, FormikHelpers } from 'formik';
import { Formik, Form } from 'formik'; import { Intent } from '@blueprintjs/core';
import { Divider, Intent, Tab, Tabs } from '@blueprintjs/core';
import styled from 'styled-components'; import styled from 'styled-components';
import { CLASSES } from '@/constants/classes';
import { CreateCustomerForm, EditCustomerForm } from './CustomerForm.schema'; import { CreateCustomerForm, EditCustomerForm } from './CustomerForm.schema';
import { compose, transformToForm, saveInvoke, parseBoolean } from '@/utils'; import { compose, transformToForm, saveInvoke, parseBoolean } from '@/utils';
import { useCustomerFormContext } from './CustomerFormProvider'; import { useCustomerFormContext } from './CustomerFormProvider';
import { defaultInitialValues } from './utils'; import { defaultInitialValues } from './utils';
import { css } from '@emotion/css';
import { AppToaster, Box, Card, Group } from '@/components';
import CustomerFormPrimarySection from './CustomerFormPrimarySection';
import CustomerFormAfterPrimarySection from './CustomerFormAfterPrimarySection';
import CustomersTabs from './CustomersTabs';
import { CustomerFloatingActions } from './CustomerFloatingActions';
import { AppToaster, Box } from '@/components';
import { withCurrentOrganization } from '@/containers/Organization/withCurrentOrganization'; import { withCurrentOrganization } from '@/containers/Organization/withCurrentOrganization';
import CustomerFinancialPanel from './CustomerFinancialPanel'; import { CustomerFormContent } from './CustomerFormContent';
import CustomerShippingAddress from './CustomerShippingAddress';
import CustomerBillingAddress from './CustomerBillingAddress';
function CustomerFormFormik({ type CustomerFormValues = {
customer_type: string;
salutation: string;
first_name: string;
last_name: string;
company_name: string;
display_name: string;
email?: string;
work_phone?: string;
personal_phone?: string;
website?: string;
note?: string;
active: boolean | string;
billing_address_country: string;
billing_address1: string;
billing_address2: string;
billing_address_city: string;
billing_address_state: string;
billing_address_postcode?: string;
billing_address_phone?: string;
shipping_address_country: string;
shipping_address1: string;
shipping_address2: string;
shipping_address_city: string;
shipping_address_state: string;
shipping_address_postcode?: string;
shipping_address_phone?: string;
currency_code: string;
opening_balance?: string | number;
opening_balance_at?: string;
opening_balance_exchange_rate?: string;
opening_balance_branch_id?: string;
[key: string]: any;
};
type CustomerFormSubmitPayload = {
noRedirect?: boolean;
};
type CustomerFormFormikRootProps = {
organization: {
base_currency: string;
};
// #ownProps
initialValues?: Partial<CustomerFormValues>;
onSubmitSuccess?: (
values: CustomerFormValues,
formArgs: FormikHelpers<CustomerFormValues>,
submitPayload: CustomerFormSubmitPayload,
responseData?: unknown,
) => void;
onSubmitError?: (
values: CustomerFormValues,
formArgs: FormikHelpers<CustomerFormValues>,
submitPayload: CustomerFormSubmitPayload,
errorData?: unknown,
) => void;
onCancel?: () => void;
className?: string;
};
const EMPTY_INITIAL_VALUES: Partial<CustomerFormValues> = {};
function CustomerFormFormikRoot({
organization: { base_currency }, organization: { base_currency },
// #ownProps // #ownProps
initialValues: initialCustomerValues, initialValues: initialCustomerValues = EMPTY_INITIAL_VALUES,
onSubmitSuccess, onSubmitSuccess,
onSubmitError, onSubmitError,
onCancel, // `onCancel` is accepted for compatibility but currently not used.
className, className,
}) { }: CustomerFormFormikRootProps) {
const { const {
customer, customer,
submitPayload, submitPayload,
@@ -43,28 +101,28 @@ function CustomerFormFormik({
isNewMode, isNewMode,
} = useCustomerFormContext(); } = useCustomerFormContext();
/** const initialValues = useMemo<CustomerFormValues>(
* Initial values in create and edit mode.
*/
const initialValues = useMemo(
() => ({ () => ({
...defaultInitialValues, ...defaultInitialValues,
currency_code: base_currency, currency_code: base_currency,
...transformToForm(contactDuplicate || customer, defaultInitialValues), ...transformToForm(contactDuplicate ?? customer ?? {}, defaultInitialValues),
...transformToForm(initialCustomerValues, defaultInitialValues), ...transformToForm(initialCustomerValues, defaultInitialValues),
}), }) as CustomerFormValues,
[customer, contactDuplicate, base_currency, initialCustomerValues], [customer, contactDuplicate, base_currency, initialCustomerValues],
); );
// Handles the form submit. // Handles the form submit.
const handleFormSubmit = (values, formArgs) => { const handleFormSubmit = (
values: CustomerFormValues,
formArgs: FormikHelpers<CustomerFormValues>,
) => {
const { setSubmitting, resetForm } = formArgs; const { setSubmitting, resetForm } = formArgs;
const formValues = { const formValues = {
...values, ...values,
active: parseBoolean(values.active, true), active: parseBoolean(values.active, true),
}; };
const onSuccess = (res) => { const onSuccess = (res: { data?: unknown }) => {
AppToaster.show({ AppToaster.show({
message: intl.get( message: intl.get(
isNewMode isNewMode
@@ -86,30 +144,25 @@ function CustomerFormFormik({
if (isNewMode) { if (isNewMode) {
createCustomerMutate(formValues).then(onSuccess).catch(onError); createCustomerMutate(formValues).then(onSuccess).catch(onError);
} else { } else {
editCustomerMutate([customer.id, formValues]) if (!customer) return;
.then(onSuccess) editCustomerMutate([customer.id, formValues]).then(onSuccess).catch(onError);
.catch(onError);
} }
}; };
return ( return (
<div <Box mx={'auto'} maxWidth={800}>
className={classNames(CLASSES.PAGE_FORM, className)} <Formik<CustomerFormValues>
>
<Formik
validationSchema={isNewMode ? CreateCustomerForm : EditCustomerForm} validationSchema={isNewMode ? CreateCustomerForm : EditCustomerForm}
initialValues={initialValues} initialValues={initialValues}
onSubmit={handleFormSubmit} onSubmit={handleFormSubmit}
> >
<Form> <Form>
<CustomerFormFields> <CustomerFormFields>
<Box px={'20px'} py={'10px'} mx={'auto'} maxWidth={'800px'}> <CustomerFormContent />
<CustomerFormContent />
</Box>
</CustomerFormFields> </CustomerFormFields>
</Form> </Form>
</Formik> </Formik>
</div> </Box>
); );
} }
@@ -124,56 +177,4 @@ const CustomerFormFields = styled.div`
} }
`; `;
export const CustomerFormHeaderPrimary = styled.div` export const CustomerFormFormik = compose(withCurrentOrganization(undefined))(CustomerFormFormikRoot);
--x-border: #e4e4e4;
.bp4-dark & {
--x-border: var(--color-dark-gray3);
}
padding: 10px 0 0;
margin: 0 0 20px;
overflow: hidden;
border-bottom: 1px solid var(--x-border);
max-width: 1000px;
`;
export default compose(withCurrentOrganization())(CustomerFormFormik);
function CustomerFormContent() {
return (
<Card>
<Group verticalAlign={'top'} alignItems={'flex-start'} flexWrap={'nowrap'}>
<Tabs vertical large defaultSelectedTabId={'primary'} className={css`position: sticky; top: 20px;`}>
<Tab id={'primary'} title={'Basic'} />
<Tab id={'financial'} title={'Financial'} />
<Tab id={'billing_address'} title={'Billing address'} />
<Tab id={'shipping_address'} title={'Ship address'} />
</Tabs>
<CustomerFormBasicSection />
</Group>
<CustomerFloatingActions />
</Card>
)
}
const customerFormSectionDividerClass = css`
margin: 20px 0;
`;
function CustomerFormBasicSection() {
return (
<Box>
<CustomerFormPrimarySection />
<Divider className={customerFormSectionDividerClass} />
<CustomerFinancialPanel />
<Divider className={customerFormSectionDividerClass} />
<CustomerBillingAddress />
<Divider className={customerFormSectionDividerClass} />
<CustomerShippingAddress />
</Box>
);
}
@@ -0,0 +1,16 @@
import { Box, FFormGroup, FormattedMessage as T, FTextArea } from '@/components';
import { CustomerFormSectionTitle } from './CustomerFormSectionTitle';
export function CustomerFormNotesSection() {
return (
<Box data-section-id="notes">
<CustomerFormSectionTitle>
<T id={'notes'} />
</CustomerFormSectionTitle>
<FFormGroup name={'note'} label={<T id={'note'} />} inline>
<FTextArea name={'note'} fill />
</FFormGroup>
</Box>
);
}
@@ -5,7 +5,7 @@ import styled from 'styled-components';
import { DashboardCard, DashboardInsider } from '@/components'; import { DashboardCard, DashboardInsider } from '@/components';
import CustomerFormFormik from './CustomerFormFormik'; import { CustomerFormFormik } from './CustomerFormFormik';
import { import {
CustomerFormProvider, CustomerFormProvider,
useCustomerFormContext, useCustomerFormContext,
@@ -19,9 +19,9 @@ function CustomerFormPageLoading({ children }) {
const { isFormLoading } = useCustomerFormContext(); const { isFormLoading } = useCustomerFormContext();
return ( return (
<CustomerDashboardInsider loading={isFormLoading}> <DashboardInsider loading={isFormLoading}>
{children} {children}
</CustomerDashboardInsider> </DashboardInsider>
); );
} }
@@ -49,7 +49,7 @@ export default function CustomerFormPage() {
return ( return (
<CustomerFormProvider customerId={customerId}> <CustomerFormProvider customerId={customerId}>
<CustomerFormPageLoading> <CustomerFormPageLoading>
<CustomerFormPageFormik <CustomerFormFormik
onSubmitSuccess={handleSubmitSuccess} onSubmitSuccess={handleSubmitSuccess}
onCancel={handleFormCancel} onCancel={handleFormCancel}
/> />
@@ -57,9 +57,3 @@ export default function CustomerFormPage() {
</CustomerFormProvider> </CustomerFormProvider>
); );
} }
const CustomerFormPageFormik = styled(CustomerFormFormik)`
`;
const CustomerDashboardInsider = styled(DashboardInsider)`
`;
@@ -1,5 +1,4 @@
// @ts-nocheck import React, { createContext, useState } from 'react';
import React, { useState, createContext } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { import {
useCustomer, useCustomer,
@@ -12,10 +11,60 @@ import {
import { Features } from '@/constants'; import { Features } from '@/constants';
import { useFeatureCan } from '@/hooks/state'; import { useFeatureCan } from '@/hooks/state';
const CustomerFormContext = createContext(); type CustomerFormSubmitPayload = {
noRedirect?: boolean;
};
function CustomerFormProvider({ query, customerId, ...props }) { type Customer = {
const { state } = useLocation(); id: number;
[key: string]: any;
};
type Currency = {
currency_code: string;
[key: string]: any;
};
type Branch = {
id: number;
primary?: boolean;
[key: string]: any;
};
type CustomerFormContextValue = {
customerId?: number;
customer?: Customer;
currencies: Currency[];
branches: Branch[];
contactDuplicate?: Customer;
submitPayload: CustomerFormSubmitPayload;
isNewMode: boolean;
isCustomerLoading: boolean;
isCurrenciesLoading: boolean;
isBranchesSuccess: boolean;
isFormLoading: boolean;
setSubmitPayload: React.Dispatch<
React.SetStateAction<CustomerFormSubmitPayload>
>;
editCustomerMutate: (args: [number, any]) => Promise<any>;
createCustomerMutate: (values: any) => Promise<any>;
};
type CustomerFormProviderProps = {
query?: unknown;
customerId?: number;
children?: React.ReactNode;
};
const CustomerFormContext = createContext<CustomerFormContextValue | undefined>(
undefined,
);
function CustomerFormProvider({ query, customerId, children }: CustomerFormProviderProps) {
const { state } = useLocation<{ action?: number | string }>();
const contactId = state?.action; const contactId = state?.action;
// Features guard. // Features guard.
@@ -33,7 +82,7 @@ function CustomerFormProvider({ query, customerId, ...props }) {
{ enabled: !!contactId }, { enabled: !!contactId },
); );
// Handle fetch Currencies data table // Handle fetch Currencies data table
const { data: currencies, isLoading: isCurrenciesLoading } = useCurrencies(); const { data: currencies, isLoading: isCurrenciesLoading } = useCurrencies(undefined);
// Fetches the branches list. // Fetches the branches list.
const { const {
@@ -43,23 +92,26 @@ function CustomerFormProvider({ query, customerId, ...props }) {
} = useBranches(query, { enabled: isBranchFeatureCan }); } = useBranches(query, { enabled: isBranchFeatureCan });
// Form submit payload. // Form submit payload.
const [submitPayload, setSubmitPayload] = useState({}); const [submitPayload, setSubmitPayload] = useState<CustomerFormSubmitPayload>({});
const { mutateAsync: editCustomerMutate } = useEditCustomer(); const editCustomerMutation = useEditCustomer(undefined) as any;
const { mutateAsync: createCustomerMutate } = useCreateCustomer(); const createCustomerMutation = useCreateCustomer(undefined) as any;
const editCustomerMutate = editCustomerMutation.mutateAsync as CustomerFormContextValue['editCustomerMutate'];
const createCustomerMutate =
createCustomerMutation.mutateAsync as CustomerFormContextValue['createCustomerMutate'];
// determines whether the form new or duplicate mode. // determines whether the form new or duplicate mode.
const isNewMode = contactId || !customerId; const isNewMode = Boolean(contactId) || !customerId;
const isFormLoading = const isFormLoading =
isCustomerLoading || isCurrenciesLoading || isBranchesLoading; isCustomerLoading || isCurrenciesLoading || isBranchesLoading;
const provider = { const provider: CustomerFormContextValue = {
customerId, customerId,
customer, customer: customer as Customer | undefined,
currencies, currencies: (currencies as Currency[]) ?? [],
branches, branches: (branches as Branch[]) ?? [],
contactDuplicate, contactDuplicate: contactDuplicate as Customer | undefined,
submitPayload, submitPayload,
isNewMode, isNewMode,
@@ -73,9 +125,21 @@ function CustomerFormProvider({ query, customerId, ...props }) {
createCustomerMutate, createCustomerMutate,
}; };
return <CustomerFormContext.Provider value={provider} {...props} />; return (
<CustomerFormContext.Provider value={provider}>
{children}
</CustomerFormContext.Provider>
);
} }
const useCustomerFormContext = () => React.useContext(CustomerFormContext); const useCustomerFormContext = () => {
const ctx = React.useContext(CustomerFormContext);
if (!ctx) {
throw new Error(
'useCustomerFormContext must be used within a CustomerFormProvider',
);
}
return ctx;
};
export { CustomerFormProvider, useCustomerFormContext }; export { CustomerFormProvider, useCustomerFormContext };
@@ -1,4 +1,3 @@
// @ts-nocheck
import React from 'react'; import React from 'react';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
@@ -9,6 +8,6 @@ const customerFormSectionTitleClass = css`
margin-top: 0; margin-top: 0;
`; `;
export default function CustomerFormSectionTitle({ children }) { export function CustomerFormSectionTitle({ children }: { children: React.ReactNode | string }) {
return <h4 className={customerFormSectionTitleClass}>{children}</h4>; return <h4 className={customerFormSectionTitleClass}>{children}</h4>;
} }
@@ -7,11 +7,11 @@ import {
FInputGroup, FInputGroup,
FTextArea, FTextArea,
} from '@/components'; } from '@/components';
import CustomerFormSectionTitle from './CustomerFormSectionTitle'; import { CustomerFormSectionTitle } from './CustomerFormSectionTitle';
export default function CustomerShippingAddress() { export function CustomerShippingAddress() {
return ( return (
<Box> <Box data-section-id="shippingAddress">
<CustomerFormSectionTitle> <CustomerFormSectionTitle>
<T id={'shipping_address'} /> <T id={'shipping_address'} />
</CustomerFormSectionTitle> </CustomerFormSectionTitle>
@@ -8,7 +8,7 @@ import { FormattedMessage as T, FFormGroup } from '@/components';
/** /**
* Customer type selector (button group). * Customer type selector (button group).
*/ */
export default function CustomerTypeRadioField() { export function CustomerTypeRadioField() {
return ( return (
<FFormGroup <FFormGroup
name={'customer_type'} name={'customer_type'}
@@ -5,7 +5,7 @@ import { Tabs, Tab } from '@blueprintjs/core';
import CustomerAddressTabs from './CustomerAddressTabs'; import CustomerAddressTabs from './CustomerAddressTabs';
import CustomerAttachmentTabs from './CustomerAttachmentTabs'; import CustomerAttachmentTabs from './CustomerAttachmentTabs';
import CustomerFinancialPanel from './CustomerFinancialPanel'; import CustomerFinancialPanel from './CustomerFormFinancialSection';
import CustomerNotePanel from './CustomerNotePanel'; import CustomerNotePanel from './CustomerNotePanel';
export default function CustomersTabs() { export default function CustomersTabs() {
@@ -8,9 +8,7 @@ import {
CustomerFormProvider, CustomerFormProvider,
useCustomerFormContext, useCustomerFormContext,
} from '@/containers/Customers/CustomerForm/CustomerFormProvider'; } from '@/containers/Customers/CustomerForm/CustomerFormProvider';
import CustomerFormFormik, { import { CustomerFormFormik } from '@/containers/Customers/CustomerForm/CustomerFormFormik';
CustomerFormHeaderPrimary,
} from '@/containers/Customers/CustomerForm/CustomerFormFormik';
import { withDrawerActions } from '@/containers/Drawer/withDrawerActions'; import { withDrawerActions } from '@/containers/Drawer/withDrawerActions';
import { DRAWERS } from '@/constants/drawers'; import { DRAWERS } from '@/constants/drawers';
@@ -56,11 +54,11 @@ function QuickCustomerFormDrawer({
<CustomerFormProvider customerId={customerId}> <CustomerFormProvider customerId={customerId}>
<DrawerCustomerFormLoading> <DrawerCustomerFormLoading>
<CustomerFormCard> <CustomerFormCard>
<CustomerFormFormik {/* <CustomerFormFormik
initialValues={{ first_name: displayName }} initialValues={{ first_name: displayName }}
onSubmitSuccess={handleSubmitSuccess} onSubmitSuccess={handleSubmitSuccess}
onCancel={handleCancelForm} onCancel={handleCancelForm}
/> /> */}
</CustomerFormCard> </CustomerFormCard>
</DrawerCustomerFormLoading> </DrawerCustomerFormLoading>
</CustomerFormProvider> </CustomerFormProvider>
@@ -74,9 +72,9 @@ const CustomerFormCard = styled(Card)`
padding: 25px; padding: 25px;
margin-bottom: calc(15px + 65px); margin-bottom: calc(15px + 65px);
${CustomerFormHeaderPrimary} { // ${CustomerFormHeaderPrimary} {
padding-top: 0; // padding-top: 0;
} // }
.page-form { .page-form {
padding: 0; padding: 0;