1
0

feat(webapp): customer/vendor form ux improvement (#1053)

* feat(webapp): customer/vendor form ux improvement
This commit is contained in:
Ahmed Bouhuolia
2026-03-27 16:34:52 +02:00
committed by GitHub
58 changed files with 1525 additions and 1001 deletions
+3 -1
View File
@@ -23,7 +23,9 @@
"tenants:migrate:latest": "lerna run cli:tenants:migrate:latest --scope \"@bigcapital/server\"",
"system:seed:latest": "lerna run cli:system:seed:latest --scope \"@bigcapital/server\"",
"tenants:seed:latest": "lerna run cli:tenants:seed:latest --scope \"@bigcapital/server\"",
"generate:sdk-types": "lerna run openapi:export --scope \"@bigcapital/server\" && lerna run generate --scope \"@bigcapital/sdk-ts\" && lerna run build --scope \"@bigcapital/sdk-ts\""
"generate:sdk-types": "lerna run openapi:export --scope \"@bigcapital/server\" && lerna run generate --scope \"@bigcapital/sdk-ts\" && lerna run build --scope \"@bigcapital/sdk-ts\"",
"format": "lerna run format",
"format:check": "lerna run format:check"
},
"devDependencies": {
"@commitlint/cli": "^17.4.2",
@@ -0,0 +1,12 @@
exports.up = function(knex) {
return knex.schema.alterTable('contacts', table => {
table.string('code').nullable().unique();
});
};
exports.down = function(knex) {
return knex.schema.alterTable('contacts', table => {
table.dropColumn('code');
});
};
@@ -1,5 +1,6 @@
import { IsEmail, IsOptional, IsString } from 'class-validator';
import { IsEmail, IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { IsOptional } from '@/common/decorators/Validators';
export class ContactAddressDto {
@ApiProperty({ required: false, description: 'Billing address line 1' })
@@ -155,4 +155,13 @@ export class CreateCustomerDto extends ContactAddressDto {
@IsOptional()
@IsBoolean()
active?: boolean;
@ApiProperty({
required: false,
description: 'Customer code',
example: 'CUST-001',
})
@IsOptional()
@IsString()
code?: string;
}
@@ -1,6 +1,7 @@
import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { IsBoolean, IsEmail, IsNotEmpty, IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { ContactAddressDto } from './ContactAddress.dto';
import { IsOptional } from '@/common/decorators/Validators';
export class EditCustomerDto extends ContactAddressDto {
@ApiProperty({ required: true, description: 'Customer type' })
@@ -62,4 +63,9 @@ export class EditCustomerDto extends ContactAddressDto {
@IsOptional()
@IsBoolean()
active?: boolean;
@ApiProperty({ required: false, description: 'Customer code' })
@IsOptional()
@IsString()
code?: string;
}
@@ -70,6 +70,8 @@ export class Customer extends TenantBaseModel {
note: string;
active: boolean;
code?: string;
/**
* Query builder.
*/
@@ -32,6 +32,7 @@ export interface ICustomerNewDTO extends IContactAddressDTO {
note?: string;
active?: boolean;
code?: string;
}
export interface ICustomerEditDTO extends IContactAddressDTO {
@@ -50,6 +51,7 @@ export interface ICustomerEditDTO extends IContactAddressDTO {
note?: string;
active?: boolean;
code?: string;
}
export interface ICustomersFilter extends IDynamicListFilter {
@@ -115,4 +115,13 @@ export class CreateVendorDto extends ContactAddressDto {
@IsOptional()
@IsBoolean()
active?: boolean;
@ApiProperty({
required: false,
description: 'Vendor code',
example: 'VEND-001',
})
@IsOptional()
@IsString()
code?: string;
}
@@ -1,5 +1,6 @@
import { ContactAddressDto } from '@/modules/Customers/dtos/ContactAddress.dto';
import { IsEmail, IsString, IsBoolean, IsOptional } from 'class-validator';
import { IsEmail, IsString, IsBoolean } from 'class-validator';
import { IsOptional } from '@/common/decorators/Validators';
import { ApiProperty } from '@nestjs/swagger';
export class EditVendorDto extends ContactAddressDto {
@@ -60,4 +61,9 @@ export class EditVendorDto extends ContactAddressDto {
@IsOptional()
@IsBoolean()
active?: boolean;
@ApiProperty({ required: false, description: 'Vendor code' })
@IsOptional()
@IsString()
code?: string;
}
@@ -71,6 +71,8 @@ export class Vendor extends TenantBaseModel {
note: string;
active: boolean;
code?: string;
/**
* Query builder.
*/
@@ -31,6 +31,7 @@ export interface IVendorNewDTO extends IContactAddressDTO {
note?: string;
active?: boolean;
code?: string;
}
export interface IVendorEditDTO extends IContactAddressDTO {
salutation?: string;
@@ -46,6 +47,7 @@ export interface IVendorEditDTO extends IContactAddressDTO {
note?: string;
active?: boolean;
code?: string;
}
export interface IVendorsFilter extends IDynamicListFilter {
@@ -4,6 +4,11 @@ import { FSelect } from '../Forms';
import { useFormikContext } from 'formik';
export type DisplayNameListItem = { label: string };
type DisplayNameFormat = {
format: string;
values: Array<string | undefined>;
required: number[];
};
export interface DisplayNameListProps
extends Omit<
@@ -11,18 +16,14 @@ export interface DisplayNameListProps
'items' | 'valueAccessor' | 'textAccessor' | 'labelAccessor'
> {}
export function DisplayNameList({ ...restProps }: DisplayNameListProps) {
const {
values: {
first_name: firstName,
last_name: lastName,
company_name: companyName,
salutation,
},
} = useFormikContext<any>();
const formats = useMemo(
() => [
function useDisplayNameFormatOptions(
salutation?: string,
firstName?: string,
lastName?: string,
companyName?: string,
): DisplayNameListItem[] {
return useMemo(() => {
const formats: DisplayNameFormat[] = [
{
format: '{1} {2} {3}',
values: [salutation, firstName, lastName],
@@ -31,13 +32,9 @@ export function DisplayNameList({ ...restProps }: DisplayNameListProps) {
{ 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
return formats
.filter(
(format) =>
!format.values.some((value, index) => {
@@ -52,9 +49,29 @@ export function DisplayNameList({ ...restProps }: DisplayNameListProps) {
const replaceWith = value || '';
label = label.replace(`{${index + 1}}`, replaceWith).trim();
});
return { label: label.replace(/\s+/g, ' ') };
}),
[formats],
return {
label: label.replace(/\s+/g, ' ').replace(/\s+,/g, ',').trim(),
};
})
.filter(({ label }) => Boolean(label));
}, [salutation, firstName, lastName, companyName]);
}
export function DisplayNameList({ ...restProps }: DisplayNameListProps) {
const {
values: {
first_name: firstName,
last_name: lastName,
company_name: companyName,
salutation,
},
} = useFormikContext<any>();
const formatOptions = useDisplayNameFormatOptions(
salutation,
firstName,
lastName,
companyName,
);
return (
@@ -62,6 +79,7 @@ export function DisplayNameList({ ...restProps }: DisplayNameListProps) {
items={formatOptions}
valueAccessor={'label'}
textAccessor={'label'}
labelAccessor={'_label'}
placeholder={intl.get('select_display_name_as')}
filterable={false}
{...restProps}
@@ -28,6 +28,7 @@ export function SalutationList({ ...restProps }: SalutationListProps) {
items={items}
valueAccessor={'key'}
textAccessor={'label'}
labelAccessor={'_label'}
placeholder={intl.get('salutation')}
filterable={false}
{...restProps}
@@ -1,153 +1,17 @@
// @ts-nocheck
import React from 'react';
import { Row, Col } from '@/components';
import {
FormattedMessage as T,
FFormGroup,
FInputGroup,
FTextArea,
} from '@/components';
import { Row } from '@/components';
const CustomerBillingAddress = ({}) => {
import CustomerBillingAddress from './CustomerBillingAddress';
import CustomerShippingAddress from './CustomerShippingAddress';
export default function CustomerAddressTabs() {
return (
<div className={'tab-panel--address'}>
<Row>
<Col xs={6}>
<h4>
<T id={'billing_address'} />
</h4>
{/*------------ Billing Address country -----------*/}
<FFormGroup
name={'billing_address_country'}
inline={true}
label={<T id={'country'} />}
>
<FInputGroup name={'billing_address_country'} />
</FFormGroup>
{/*------------ Billing Address 1 -----------*/}
<FFormGroup
name={'billing_address1'}
label={<T id={'address_line_1'} />}
inline={true}
>
<FTextArea name={'billing_address1'} />
</FFormGroup>
{/*------------ Billing Address 2 -----------*/}
<FFormGroup
name={'billing_address2'}
label={<T id={'address_line_2'} />}
inline={true}
>
<FTextArea name={'billing_address2'} />
</FFormGroup>
{/*------------ Billing Address city -----------*/}
<FFormGroup
name={'billing_address_city'}
label={<T id={'city_town'} />}
inline={true}
>
<FInputGroup name={'billing_address_city'} />
</FFormGroup>
{/*------------ Billing Address state -----------*/}
<FFormGroup
name={'billing_address_state'}
label={<T id={'state'} />}
inline={true}
>
<FInputGroup name={'billing_address_state'} />
</FFormGroup>
{/*------------ Billing Address postcode -----------*/}
<FFormGroup
name={'billing_address_postcode'}
label={<T id={'zip_code'} />}
inline={true}
>
<FInputGroup name={'billing_address_postcode'} />
</FFormGroup>
{/*------------ Billing Address phone -----------*/}
<FFormGroup
name={'billing_address_phone'}
label={<T id={'phone'} />}
inline={true}
>
<FInputGroup name={'billing_address_phone'} />
</FFormGroup>
</Col>
<Col xs={6}>
<h4>
<T id={'shipping_address'} />
</h4>
{/*------------ Shipping Address country -----------*/}
<FFormGroup
name={'shipping_address_country'}
label={<T id={'country'} />}
inline={true}
>
<FInputGroup name={'shipping_address_country'} />
</FFormGroup>
{/*------------ Shipping Address 1 -----------*/}
<FFormGroup
name={'shipping_address1'}
label={<T id={'address_line_1'} />}
inline={true}
>
<FTextArea name={'shipping_address1'} />
</FFormGroup>
{/*------------ Shipping Address 2 -----------*/}
<FFormGroup
name={'shipping_address2'}
label={<T id={'address_line_2'} />}
inline={true}
>
<FTextArea name={'shipping_address2'} />
</FFormGroup>
{/*------------ Shipping Address city -----------*/}
<FFormGroup
name={'shipping_address_city'}
label={<T id={'city_town'} />}
inline={true}
>
<FInputGroup name={'shipping_address_city'} />
</FFormGroup>
{/*------------ Shipping Address state -----------*/}
<FFormGroup
name={'shipping_address_state'}
label={<T id={'state'} />}
inline={true}
>
<FInputGroup name={'shipping_address_state'} />
</FFormGroup>
{/*------------ Shipping Address postcode -----------*/}
<FFormGroup
name={'shipping_address_postcode'}
label={<T id={'zip_code'} />}
inline={true}
>
<FInputGroup name={'shipping_address_postcode'} />
</FFormGroup>
{/*------------ Shipping Address phone -----------*/}
<FFormGroup
name={'shipping_address_phone'}
label={<T id={'phone'} />}
inline={true}
>
<FInputGroup name={'shipping_address_phone'} />
</FFormGroup>
</Col>
<CustomerBillingAddress />
<CustomerShippingAddress />
</Row>
</div>
);
};
export default CustomerBillingAddress;
}
@@ -0,0 +1,82 @@
// @ts-nocheck
import React from 'react';
import { Box } from '@/components';
import {
FormattedMessage as T,
FFormGroup,
FInputGroup,
FTextArea,
} from '@/components';
import { CustomerFormSectionTitle } from './CustomerFormSectionTitle';
export function CustomerBillingAddress() {
return (
<Box data-section-id="billingAddress">
<CustomerFormSectionTitle>
<T id={'billing_address'} />
</CustomerFormSectionTitle>
<FFormGroup
name={'billing_address_country'}
label={<T id={'country'} />}
inline
fill
>
<FInputGroup name={'billing_address_country'} fill />
</FFormGroup>
<FFormGroup
name={'billing_address1'}
label={<T id={'address_line_1'} />}
inline
fill
>
<FTextArea name={'billing_address1'} fill />
</FFormGroup>
<FFormGroup
name={'billing_address2'}
label={<T id={'address_line_2'} />}
inline
fill
>
<FTextArea name={'billing_address2'} fill />
</FFormGroup>
<FFormGroup
name={'billing_address_city'}
label={<T id={'city_town'} />}
inline
fill
>
<FInputGroup name={'billing_address_city'} fill />
</FFormGroup>
<FFormGroup
name={'billing_address_state'}
label={<T id={'state'} />}
inline
fill
>
<FInputGroup name={'billing_address_state'} fill />
</FFormGroup>
<FFormGroup
name={'billing_address_postcode'}
label={<T id={'zip_code'} />}
inline
fill
>
<FInputGroup name={'billing_address_postcode'} fill />
</FFormGroup>
<FFormGroup
name={'billing_address_phone'}
label={<T id={'phone'} />}
inline
fill
>
<FInputGroup name={'billing_address_phone'} fill />
</FFormGroup>
</Box>
);
}
@@ -1,4 +1,3 @@
// @ts-nocheck
import React from 'react';
import styled from 'styled-components';
import {
@@ -11,52 +10,36 @@ import {
Menu,
MenuItem,
} from '@blueprintjs/core';
import classNames from 'classnames';
import { useFormikContext } from 'formik';
import { Group, Icon, FormattedMessage as T } from '@/components';
import { CLASSES } from '@/constants/classes';
import { useCustomerFormContext } from './CustomerFormProvider';
import { safeInvoke } from '@/utils';
/**
* Customer floating actions bar.
*/
export default function CustomerFloatingActions({ onCancel }) {
export function CustomerFloatingActions() {
// Customer form context.
const { isNewMode, setSubmitPayload } = useCustomerFormContext();
const { isNewMode, setSubmitPayload } = useCustomerFormContext() as {
isNewMode: boolean;
setSubmitPayload: (payload: { noRedirect: boolean }) => void;
};
// Formik context.
const { resetForm, submitForm, isSubmitting } = useFormikContext();
const { submitForm, isSubmitting } = useFormikContext();
// Handle submit button click.
const handleSubmitBtnClick = (event) => {
const handleSubmitBtnClick = (_event: React.MouseEvent<HTMLElement>) => {
setSubmitPayload({ noRedirect: false });
};
// Handle cancel button click.
const handleCancelBtnClick = (event) => {
safeInvoke(onCancel, event);
};
// handle clear button clicl.
const handleClearBtnClick = (event) => {
resetForm();
};
// Handle submit & new button click.
const handleSubmitAndNewClick = (event) => {
const handleSubmitAndNewClick = (_event: React.MouseEvent<HTMLElement>) => {
submitForm();
setSubmitPayload({ noRedirect: true });
};
return (
<Group
spacing={10}
className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}
>
<FloatingActionsGroup spacing={10}>
<ButtonGroup>
{/* ----------- Save and New ----------- */}
<SaveButton
<Button
disabled={isSubmitting}
loading={isSubmitting}
intent={Intent.PRIMARY}
@@ -73,9 +56,9 @@ export default function CustomerFloatingActions({ onCancel }) {
/>
</Menu>
}
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
position={Position.BOTTOM_RIGHT}
minimal
>
<Button
disabled={isSubmitting}
@@ -84,24 +67,16 @@ export default function CustomerFloatingActions({ onCancel }) {
/>
</Popover>
</ButtonGroup>
{/* ----------- Clear & Reset----------- */}
<Button
className={'ml1'}
disabled={isSubmitting}
onClick={handleClearBtnClick}
text={!isNewMode ? <T id={'reset'} /> : <T id={'clear'} />}
/>
{/* ----------- Cancel ----------- */}
<Button
className={'ml1'}
onClick={handleCancelBtnClick}
text={<T id={'cancel'} />}
/>
</Group>
</FloatingActionsGroup>
);
}
const SaveButton = styled(Button)`
min-width: 100px;
const FloatingActionsGroup = styled(Group)`
padding: 10px 0;
padding-left: 165px;
border-top: 1px solid #50555a;
position: sticky;
bottom: 0;
background: var(--color-card-background);
z-index: 1;
`;
@@ -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>
);
}
@@ -6,34 +6,46 @@ import { FormattedMessage as T, FFormGroup, FInputGroup } from '@/components';
export default function CustomerFormAfterPrimarySection({}) {
return (
<div className={'customer-form__after-primary-section-content'}>
<div>
{/*------------ Customer email -----------*/}
<FFormGroup
name={'email'}
label={<T id={'customer_email'} />}
inline={true}
inline
fill
>
<FInputGroup name={'email'} />
<FInputGroup name={'email'} fill />
</FFormGroup>
{/*------------ Phone number -----------*/}
<FFormGroup
name={'personal_phone'}
label={<T id={'phone_number'} />}
inline={true}
inline
fill
>
<ControlGroup>
<ControlGroup fill>
<FInputGroup
name={'personal_phone'}
placeholder={intl.get('personal')}
fill
/>
<FInputGroup
name={'work_phone'}
placeholder={intl.get('work')}
fill
/>
<FInputGroup name={'work_phone'} placeholder={intl.get('work')} />
</ControlGroup>
</FFormGroup>
{/*------------ Customer website -----------*/}
<FFormGroup name={'website'} label={<T id={'website'} />} inline={true}>
<FInputGroup name={'website'} placeholder={'http://'} />
<FFormGroup
name={'website'}
label={<T id={'website'} />}
inline
fill
>
<FInputGroup name={'website'} placeholder={'http://'} fill />
</FFormGroup>
</div>
);
@@ -0,0 +1,138 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import { ControlGroup, Divider, Icon as BlueprintIcon } from '@blueprintjs/core';
import {
Hint,
FieldRequiredHint,
SalutationList,
DisplayNameList,
FormattedMessage as T,
FInputGroup,
FFormGroup,
Box,
Icon,
Stack,
} from '@/components';
import { CustomerTypeRadioField } from './CustomerTypeRadioField';
import { CustomerFormSectionTitle } from './CustomerFormSectionTitle';
import { useAutofocus } from '@/hooks';
export function CustomerFormBasicSection({}) {
const firstNameFieldRef = useAutofocus();
return (
<Box data-section-id="primary">
<CustomerFormSectionTitle>Customer details</CustomerFormSectionTitle>
{/**-----------Customer type. -----------*/}
<CustomerTypeRadioField />
{/**----------- Contact name -----------*/}
<FFormGroup
name={'salutation'}
label={<T id={'contact_name'} />}
inline
fill
>
<ControlGroup fill>
<SalutationList
name={'salutation'}
popoverProps={{ minimal: true }}
/>
<FInputGroup
name={'first_name'}
placeholder={intl.get('first_name')}
inputRef={(ref) => (firstNameFieldRef.current = ref)}
fill
/>
<FInputGroup
name={'last_name'}
placeholder={intl.get('last_name')}
fill
/>
</ControlGroup>
</FFormGroup>
<FFormGroup
name={'code'}
label={'Customer Code'}
helperText="Add a unique account number to identify, reference and search for the contact."
inline
fill
>
<FInputGroup
name={'code'}
fill />
</FFormGroup>
{/*----------- Company Name -----------*/}
<FFormGroup
name={'company_name'}
label={<T id={'company_name'} />}
inline
fill
>
<FInputGroup name={'company_name'} fill />
</FFormGroup>
{/*----------- Display Name -----------*/}
<FFormGroup
name={'display_name'}
label={<T id={'display_name'} />}
helperText="This is the name that appears on invoices and emails."
inline
fill
>
<DisplayNameList
name={'display_name'}
popoverProps={{ minimal: true }}
buttonProps={{ fill: true }}
/>
</FFormGroup>
<Divider style={{ margin: '20px 0' }} />
{/*------------ Vendor email -----------*/}
<FFormGroup
name={'email'}
label={<T id={'vendor_email'} />}
inline
>
<FInputGroup
name={'email'}
leftIcon={<Icon icon="envelope" />}
/>
</FFormGroup>
{/*------------ Phone number -----------*/}
<FFormGroup
name={'work_phone'}
className={'form-group--phone-number'}
label={<T id={'phone_number'} />}
inline={true}
>
<Stack spacing={10}>
<FInputGroup
name={'work_phone'}
placeholder={intl.get('work')}
leftIcon="phone"
/>
<FInputGroup
name={'personal_phone'}
placeholder={intl.get('mobile')}
/>
</Stack>
</FFormGroup>
{/*------------ Vendor website -----------*/}
<FFormGroup name={'website'} label={<T id={'website'} />} inline={true}>
<FInputGroup
name={'website'}
placeholder={'http://'}
leftIcon={<BlueprintIcon icon="globe-network" />}
/>
</FFormGroup>
</Box>
);
}
@@ -0,0 +1,45 @@
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";
export function CustomerFormContent() {
const [selectedTabId, setSelectedTabId] = useState('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
selectedTabId={selectedTabId}
onChange={handleTabChange}
className={css`position: sticky; top: 20px;`}
vertical
>
<Tab id={'primary'} title={'Basic'} />
<Tab id={'financial'} title={'Financial'} />
<Tab id={'billingAddress'} title={'Billing address'} />
<Tab id={'shippingAddress'} title={'Shipping address'} />
<Tab id={'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,8 +1,7 @@
// @ts-nocheck
import React from 'react';
import classNames from 'classnames';
import { FormGroup, Position, Classes, ControlGroup } from '@blueprintjs/core';
import { FastField, ErrorMessage, useFormikContext } from 'formik';
import { Position, ControlGroup } from '@blueprintjs/core';
import { ErrorMessage, useFormikContext } from 'formik';
import { Features } from '@/constants';
import {
FFormGroup,
@@ -11,11 +10,11 @@ import {
CurrencySelectList,
BranchSelect,
FeatureCan,
Row,
Col,
FMoneyInputGroup,
ExchangeRateInputGroup,
FDateInput,
Icon,
Box,
} from '@/components';
import { useCustomerFormContext } from './CustomerFormProvider';
import {
@@ -24,26 +23,26 @@ import {
useSetPrimaryBranchToForm,
} from './utils';
import { useCurrentOrganization } from '@/hooks/state';
import { CustomerFormSectionTitle } from './CustomerFormSectionTitle';
/**
* Customer financial panel.
*/
export default function CustomerFinancialPanel() {
export function CustomerFormFinancialSection() {
const { currencies, customerId, branches } = useCustomerFormContext();
// Sets the primary branch to form.
useSetPrimaryBranchToForm();
return (
<div className={'tab-panel--financial'}>
<Row>
<Col xs={6}>
{/*------------ Currency -----------*/}
<Box data-section-id="financial">
<CustomerFormSectionTitle>
<T id={'financial'} />
</CustomerFormSectionTitle>
<FFormGroup
name={'currency_code'}
label={<T id={'currency'} />}
fastField
inline
fill
>
<CurrencySelectList
name="currency_code"
@@ -52,39 +51,28 @@ export default function CustomerFinancialPanel() {
/>
</FFormGroup>
{/*------------ Opening balance -----------*/}
<CustomerOpeningBalanceField />
{/*------ Opening Balance Exchange Rate -----*/}
<CustomerOpeningBalanceExchangeRateField />
{/*------------ Opening balance at -----------*/}
<CustomerOpeningBalanceAtField />
{/*------------ Opening branch -----------*/}
<FeatureCan feature={Features.Branches}>
<FFormGroup
label={<T id={'customer.label.opening_branch'} />}
name={'opening_balance_branch_id'}
inline={true}
inline
>
<BranchSelect
name={'opening_balance_branch_id'}
branches={branches}
popoverProps={{ minimal: true }}
fastField
/>
</FFormGroup>
</FeatureCan>
</Col>
</Row>
</div>
</Box>
);
}
/**
* Customer opening balance at date field.
* @returns {JSX.Element}
*/
function CustomerOpeningBalanceAtField() {
const { customerId } = useCustomerFormContext();
@@ -92,10 +80,11 @@ function CustomerOpeningBalanceAtField() {
if (customerId) return null;
return (
<FormGroup
<FFormGroup
name={'opening_balance_at'}
label={<T id={'opening_balance_at'} />}
inline={true}
inline
fill
helperText={<ErrorMessage name="opening_balance_at" />}
>
<FDateInput
@@ -104,16 +93,15 @@ function CustomerOpeningBalanceAtField() {
disabled={customerId}
formatDate={(date) => date.toLocaleDateString()}
parseDate={(str) => new Date(str)}
inputProps={{
leftIcon: <Icon icon={'date-range'} />,
}}
fill={true}
/>
</FormGroup>
</FFormGroup>
);
}
/**
* Customer opening balance field.
* @returns {JSX.Element}
*/
function CustomerOpeningBalanceField() {
const { customerId } = useCustomerFormContext();
const { values } = useFormikContext();
@@ -125,15 +113,17 @@ function CustomerOpeningBalanceField() {
<FFormGroup
label={<T id={'opening_balance'} />}
name={'opening_balance'}
inline={true}
inline
shouldUpdate={openingBalanceFieldShouldUpdate}
shouldUpdateDeps={{ currencyCode: values.currency_code }}
fastField={true}
fill
>
<ControlGroup>
<InputPrependText text={values.currency_code} />
<InputPrependText text={values.currency_code as string} />
<FMoneyInputGroup
name={'opening_balance'}
fastField
inputGroupProps={{ fill: true }}
/>
</ControlGroup>
@@ -141,11 +131,6 @@ function CustomerOpeningBalanceField() {
);
}
/**
* Customer opening balance exchange rate field if the customer has foreign
* currency.
* @returns {JSX.Element}
*/
function CustomerOpeningBalanceExchangeRateField() {
const { values } = useFormikContext();
const { customerId } = useCustomerFormContext();
@@ -158,16 +143,14 @@ function CustomerOpeningBalanceExchangeRateField() {
return null;
}
return (
<FFormGroup
label={' '}
name={'opening_balance_exchange_rate'}
inline={true}
>
<ExchangeRateInputGroup
fromCurrency={values.currency_code}
toCurrency={currentOrganization.base_currency}
name={'opening_balance_exchange_rate'}
onRecalcConfirm={() => {}}
onCancel={() => {}}
formGroupProps={{ label: ' ' }}
/>
</FFormGroup>
);
}
@@ -1,40 +1,95 @@
// @ts-nocheck
import React, { useMemo } from 'react';
import { useMemo } from 'react';
import intl from 'react-intl-universal';
import classNames from 'classnames';
import { Formik, Form } from 'formik';
import { Formik, Form, FormikHelpers } from 'formik';
import { Intent } from '@blueprintjs/core';
import styled from 'styled-components';
import { CLASSES } from '@/constants/classes';
import { CreateCustomerForm, EditCustomerForm } from './CustomerForm.schema';
import { compose, transformToForm, saveInvoke, parseBoolean } from '@/utils';
import { useCustomerFormContext } from './CustomerFormProvider';
import { defaultInitialValues } from './utils';
import { AppToaster } from '@/components';
import CustomerFormPrimarySection from './CustomerFormPrimarySection';
import CustomerFormAfterPrimarySection from './CustomerFormAfterPrimarySection';
import CustomersTabs from './CustomersTabs';
import CustomerFloatingActions from './CustomerFloatingActions';
import { withCurrentOrganization } from '@/containers/Organization/withCurrentOrganization';
import { CustomerFormContent } from './CustomerFormContent';
import '@/style/pages/Customers/Form.scss';
type CustomerFormValues = {
customer_type: string;
salutation: string;
first_name: string;
last_name: string;
company_name: string;
display_name: string;
/**
* Customer form.
*/
function CustomerFormFormik({
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 },
// #ownProps
initialValues: initialCustomerValues,
initialValues: initialCustomerValues = EMPTY_INITIAL_VALUES,
onSubmitSuccess,
onSubmitError,
onCancel,
// `onCancel` is accepted for compatibility but currently not used.
className,
}) {
}: CustomerFormFormikRootProps) {
const {
customer,
submitPayload,
@@ -44,28 +99,28 @@ function CustomerFormFormik({
isNewMode,
} = useCustomerFormContext();
/**
* Initial values in create and edit mode.
*/
const initialValues = useMemo(
const initialValues = useMemo<CustomerFormValues>(
() => ({
...defaultInitialValues,
currency_code: base_currency,
...transformToForm(contactDuplicate || customer, defaultInitialValues),
...transformToForm(contactDuplicate ?? customer ?? {}, defaultInitialValues),
...transformToForm(initialCustomerValues, defaultInitialValues),
}),
}) as CustomerFormValues,
[customer, contactDuplicate, base_currency, initialCustomerValues],
);
// Handles the form submit.
const handleFormSubmit = (values, formArgs) => {
const handleFormSubmit = (
values: CustomerFormValues,
formArgs: FormikHelpers<CustomerFormValues>,
) => {
const { setSubmitting, resetForm } = formArgs;
const formValues = {
...values,
active: parseBoolean(values.active, true),
};
const onSuccess = (res) => {
const onSuccess = (res: { data?: unknown }) => {
AppToaster.show({
message: intl.get(
isNewMode
@@ -83,60 +138,40 @@ function CustomerFormFormik({
setSubmitting(false);
saveInvoke(onSubmitError, values, formArgs, submitPayload);
};
if (isNewMode) {
createCustomerMutate(formValues).then(onSuccess).catch(onError);
} else {
editCustomerMutate([customer.id, formValues])
.then(onSuccess)
.catch(onError);
if (!customer) return;
editCustomerMutate([customer.id, formValues]).then(onSuccess).catch(onError);
}
};
return (
<div
className={classNames(
CLASSES.PAGE_FORM,
CLASSES.PAGE_FORM_CUSTOMER,
className,
)}
>
<Formik
<Formik<CustomerFormValues>
validationSchema={isNewMode ? CreateCustomerForm : EditCustomerForm}
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
<Form>
<CustomerFormHeaderPrimary>
<CustomerFormPrimarySection />
</CustomerFormHeaderPrimary>
<div className={'page-form__after-priamry-section'}>
<CustomerFormAfterPrimarySection />
</div>
<div className={classNames(CLASSES.PAGE_FORM_TABS)}>
<CustomersTabs />
</div>
<CustomerFloatingActions onCancel={onCancel} />
<CustomerFormFields>
<CustomerFormContent />
</CustomerFormFields>
</Form>
</Formik>
</div>
);
}
export const CustomerFormHeaderPrimary = styled.div`
--x-border: #e4e4e4;
.bp4-dark & {
--x-border: var(--color-dark-gray3);
const CustomerFormFields = styled.div`
.bp4-form-content,
.bp6-form-content {
min-width: 300px;
}
.bp4-form-group{
margin-bottom: 20px;
}
.bp4-form-group.bp4-inline label.bp4-label {
min-width: 140px;
}
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);
export const CustomerFormFormik = compose(withCurrentOrganization(undefined))(CustomerFormFormikRoot);
@@ -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>
);
}
@@ -2,73 +2,53 @@
import React from 'react';
import { useParams, useHistory } from 'react-router-dom';
import styled from 'styled-components';
import { DashboardCard, DashboardInsider } from '@/components';
import CustomerFormFormik from './CustomerFormFormik';
import { Box, DashboardCard, DashboardInsider } from '@/components';
import { CustomerFormFormik, ustomerFormFormik } from './CustomerFormFormik';
import {
CustomerFormProvider,
useCustomerFormContext,
} from './CustomerFormProvider';
/**
* Customer form page loading.
* @returns {JSX}
*/
function CustomerFormPageLoading({ children }) {
const { isFormLoading } = useCustomerFormContext();
return (
<CustomerDashboardInsider loading={isFormLoading}>
{children}
</CustomerDashboardInsider>
);
}
/**
* Customer form page.
* @returns {JSX}
*/
export default function CustomerFormPage() {
const history = useHistory();
const { id } = useParams();
const customerId = parseInt(id, 10);
// Handle the form submit success.
return (
<CustomerFormProvider customerId={customerId}>
<CustomerFormPageContent />
</CustomerFormProvider>
);
}
function CustomerFormPageContent() {
const history = useHistory();
const { isFormLoading } = useCustomerFormContext();
const handleSubmitSuccess = (values, formArgs, submitPayload) => {
if (!submitPayload.noRedirect) {
history.push('/customers');
}
};
}
// Handle the form cancel button click.
const handleFormCancel = () => {
history.goBack();
};
return (
<CustomerFormProvider customerId={customerId}>
<CustomerFormPageLoading>
<DashboardCard page>
<CustomerFormPageFormik
<DashboardInsider loading={isFormLoading}>
<Box mx={'auto'} maxWidth={800}>
<CustomerFormFormik
onSubmitSuccess={handleSubmitSuccess}
onCancel={handleFormCancel}
/>
</DashboardCard>
</CustomerFormPageLoading>
</CustomerFormProvider>
);
</Box>
</DashboardInsider>
)
}
const CustomerFormPageFormik = styled(CustomerFormFormik)`
.page-form {
&__floating-actions {
margin-left: -40px;
margin-right: -40px;
}
}
`;
const CustomerDashboardInsider = styled(DashboardInsider)`
padding-bottom: 64px;
`;
@@ -1,80 +0,0 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import classNames from 'classnames';
import { FormGroup, InputGroup, ControlGroup } from '@blueprintjs/core';
import { FastField, Field, ErrorMessage } from 'formik';
import {
Hint,
FieldRequiredHint,
SalutationList,
DisplayNameList,
FormattedMessage as T,
FInputGroup,
FFormGroup,
} from '@/components';
import CustomerTypeRadioField from './CustomerTypeRadioField';
import { CLASSES } from '@/constants/classes';
import { inputIntent } from '@/utils';
import { useAutofocus } from '@/hooks';
/**
* Customer form primary section.
*/
export default function CustomerFormPrimarySection({}) {
const firstNameFieldRef = useAutofocus();
return (
<div className={'customer-form__primary-section-content'}>
{/**-----------Customer type. -----------*/}
<CustomerTypeRadioField />
{/**----------- Contact name -----------*/}
<FFormGroup
name={'salutation'}
label={<T id={'contact_name'} />}
inline={true}
>
<ControlGroup>
<SalutationList
name={'salutation'}
popoverProps={{ minimal: true }}
/>
<FInputGroup
name={'first_name'}
placeholder={intl.get('first_name')}
inputRef={(ref) => (firstNameFieldRef.current = ref)}
/>
<FInputGroup name={'last_name'} placeholder={intl.get('last_name')} />
</ControlGroup>
</FFormGroup>
{/*----------- Company Name -----------*/}
<FFormGroup
name={'company_name'}
label={<T id={'company_name'} />}
inline={true}
>
<FInputGroup name={'company_name'} />
</FFormGroup>
{/*----------- Display Name -----------*/}
<FFormGroup
name={'display_name'}
label={
<>
<T id={'display_name'} />
<FieldRequiredHint />
<Hint />
</>
}
inline={true}
>
<DisplayNameList
name={'display_name'}
popoverProps={{ minimal: true }}
/>
</FFormGroup>
</div>
);
}
@@ -1,5 +1,4 @@
// @ts-nocheck
import React, { useState, createContext } from 'react';
import React, { createContext, useState } from 'react';
import { useLocation } from 'react-router-dom';
import {
useCustomer,
@@ -12,10 +11,60 @@ import {
import { Features } from '@/constants';
import { useFeatureCan } from '@/hooks/state';
const CustomerFormContext = createContext();
type CustomerFormSubmitPayload = {
noRedirect?: boolean;
};
function CustomerFormProvider({ query, customerId, ...props }) {
const { state } = useLocation();
type Customer = {
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,
);
export function CustomerFormProvider({ query, customerId, children }: CustomerFormProviderProps) {
const { state } = useLocation<{ action?: number | string }>();
const contactId = state?.action;
// Features guard.
@@ -33,7 +82,7 @@ function CustomerFormProvider({ query, customerId, ...props }) {
{ enabled: !!contactId },
);
// Handle fetch Currencies data table
const { data: currencies, isLoading: isCurrenciesLoading } = useCurrencies();
const { data: currencies, isLoading: isCurrenciesLoading } = useCurrencies(undefined);
// Fetches the branches list.
const {
@@ -43,23 +92,26 @@ function CustomerFormProvider({ query, customerId, ...props }) {
} = useBranches(query, { enabled: isBranchFeatureCan });
// Form submit payload.
const [submitPayload, setSubmitPayload] = useState({});
const [submitPayload, setSubmitPayload] = useState<CustomerFormSubmitPayload>({});
const { mutateAsync: editCustomerMutate } = useEditCustomer();
const { mutateAsync: createCustomerMutate } = useCreateCustomer();
const editCustomerMutation = useEditCustomer(undefined) as any;
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.
const isNewMode = contactId || !customerId;
const isNewMode = Boolean(contactId) || !customerId;
const isFormLoading =
isCustomerLoading || isCurrenciesLoading || isBranchesLoading;
const provider = {
const provider: CustomerFormContextValue = {
customerId,
customer,
currencies,
branches,
contactDuplicate,
customer: customer as Customer | undefined,
currencies: (currencies as Currency[]) ?? [],
branches: (branches as Branch[]) ?? [],
contactDuplicate: contactDuplicate as Customer | undefined,
submitPayload,
isNewMode,
@@ -73,9 +125,19 @@ function CustomerFormProvider({ query, customerId, ...props }) {
createCustomerMutate,
};
return <CustomerFormContext.Provider value={provider} {...props} />;
return (
<CustomerFormContext.Provider value={provider}>
{children}
</CustomerFormContext.Provider>
);
}
const useCustomerFormContext = () => React.useContext(CustomerFormContext);
export { CustomerFormProvider, useCustomerFormContext };
export const useCustomerFormContext = () => {
const ctx = React.useContext(CustomerFormContext);
if (!ctx) {
throw new Error(
'useCustomerFormContext must be used within a CustomerFormProvider',
);
}
return ctx;
};
@@ -0,0 +1,13 @@
import React from 'react';
import { css } from '@emotion/css';
const customerFormSectionTitleClass = css`
font-size: 14px;
color: #8f99a8;
margin-bottom: 18px;
margin-top: 10px;
`;
export function CustomerFormSectionTitle({ children }: { children: React.ReactNode | string }) {
return <h4 className={customerFormSectionTitleClass}>{children}</h4>;
}
@@ -1,15 +1,11 @@
// @ts-nocheck
import React from 'react';
import classNames from 'classnames';
import { Classes } from '@blueprintjs/core';
import { FormattedMessage as T, FFormGroup, FTextArea } from '@/components';
export default function CustomerNotePanel({ errors, touched, getFieldProps }) {
return (
<div className={'tab-panel--note'}>
<FFormGroup name={'note'} label={<T id={'note'} />} inline={false}>
<FTextArea name={'note'} />
<FFormGroup name={'note'} label={<T id={'note'} />} inline={false} fill>
<FTextArea name={'note'} fill />
</FFormGroup>
</div>
);
}
@@ -0,0 +1,82 @@
// @ts-nocheck
import React from 'react';
import { Box } from '@/components';
import {
FormattedMessage as T,
FFormGroup,
FInputGroup,
FTextArea,
} from '@/components';
import { CustomerFormSectionTitle } from './CustomerFormSectionTitle';
export function CustomerShippingAddress() {
return (
<Box data-section-id="shippingAddress">
<CustomerFormSectionTitle>
<T id={'shipping_address'} />
</CustomerFormSectionTitle>
<FFormGroup
name={'shipping_address_country'}
label={<T id={'country'} />}
inline
fill
>
<FInputGroup name={'shipping_address_country'} fill />
</FFormGroup>
<FFormGroup
name={'shipping_address1'}
label={<T id={'address_line_1'} />}
inline
fill
>
<FTextArea name={'shipping_address1'} fill />
</FFormGroup>
<FFormGroup
name={'shipping_address2'}
label={<T id={'address_line_2'} />}
inline
fill
>
<FTextArea name={'shipping_address2'} fill />
</FFormGroup>
<FFormGroup
name={'shipping_address_city'}
label={<T id={'city_town'} />}
inline
fill
>
<FInputGroup name={'shipping_address_city'} fill />
</FFormGroup>
<FFormGroup
name={'shipping_address_state'}
label={<T id={'state'} />}
inline
fill
>
<FInputGroup name={'shipping_address_state'} fill />
</FFormGroup>
<FFormGroup
name={'shipping_address_postcode'}
label={<T id={'zip_code'} />}
inline
fill
>
<FInputGroup name={'shipping_address_postcode'} fill />
</FFormGroup>
<FFormGroup
name={'shipping_address_phone'}
label={<T id={'phone'} />}
inline
fill
>
<FInputGroup name={'shipping_address_phone'} fill />
</FFormGroup>
</Box>
);
}
@@ -1,27 +1,52 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import classNames from 'classnames';
import { Radio } from '@blueprintjs/core';
import { FormattedMessage as T, FFormGroup, FRadioGroup } from '@/components';
import { handleStringChange, saveInvoke } from '@/utils';
import { Button, ButtonGroup } from '@blueprintjs/core';
import { FastField } from 'formik';
import { FormattedMessage as T, FFormGroup } from '@/components';
/**
* Customer type radio field.
* Customer type selector (button group).
*/
export default function RadioCustomer() {
export function CustomerTypeRadioField() {
return (
<FFormGroup
name={'customer_type'}
label={<T id={'customer_type'} />}
inline
fill
fastField
>
<FRadioGroup name={'customer_type'} inline>
<Radio label={intl.get('business')} value="business" />
<Radio label={intl.get('individual')} value="individual" />
</FRadioGroup>
<FastField name="customer_type">
{({ field, form }) => (
<ButtonGroup>
<Button
type="button"
outlined
small
active={field.value === 'business'}
onClick={() => {
form.setFieldValue('customer_type', 'business');
form.setFieldTouched('customer_type', true);
}}
>
{intl.get('business')}
</Button>
<Button
type="button"
outlined
small
active={field.value === 'individual'}
onClick={() => {
form.setFieldValue('customer_type', 'individual');
form.setFieldTouched('customer_type', true);
}}
>
{intl.get('individual')}
</Button>
</ButtonGroup>
)}
</FastField>
</FFormGroup>
);
}
@@ -5,7 +5,7 @@ import { Tabs, Tab } from '@blueprintjs/core';
import CustomerAddressTabs from './CustomerAddressTabs';
import CustomerAttachmentTabs from './CustomerAttachmentTabs';
import CustomerFinancialPanel from './CustomerFinancialPanel';
import CustomerFinancialPanel from './CustomerFormFinancialSection';
import CustomerNotePanel from './CustomerNotePanel';
export default function CustomersTabs() {
@@ -14,6 +14,7 @@ export const defaultInitialValues = {
last_name: '',
company_name: '',
display_name: '',
code: '',
email: '',
work_phone: '',
@@ -8,9 +8,7 @@ import {
CustomerFormProvider,
useCustomerFormContext,
} from '@/containers/Customers/CustomerForm/CustomerFormProvider';
import CustomerFormFormik, {
CustomerFormHeaderPrimary,
} from '@/containers/Customers/CustomerForm/CustomerFormFormik';
import { CustomerFormFormik } from '@/containers/Customers/CustomerForm/CustomerFormFormik';
import { withDrawerActions } from '@/containers/Drawer/withDrawerActions';
import { DRAWERS } from '@/constants/drawers';
@@ -55,34 +53,14 @@ function QuickCustomerFormDrawer({
return (
<CustomerFormProvider customerId={customerId}>
<DrawerCustomerFormLoading>
<CustomerFormCard>
<CustomerFormFormik
initialValues={{ first_name: displayName }}
onSubmitSuccess={handleSubmitSuccess}
onCancel={handleCancelForm}
/>
</CustomerFormCard>
</DrawerCustomerFormLoading>
</CustomerFormProvider>
);
}
export default R.compose(withDrawerActions)(QuickCustomerFormDrawer);
const CustomerFormCard = styled(Card)`
margin: 15px;
padding: 25px;
margin-bottom: calc(15px + 65px);
${CustomerFormHeaderPrimary} {
padding-top: 0;
}
.page-form {
padding: 0;
&__floating-actions {
margin-left: -41px;
margin-right: -41px;
}
}
`;
@@ -8,8 +8,8 @@ import {
VendorFormProvider,
useVendorFormContext,
} from '@/containers/Vendors/VendorForm/VendorFormProvider';
import VendorFormFormik, {
VendorFormHeaderPrimary,
import {
VendorFormFormik,
} from '@/containers/Vendors/VendorForm/VendorFormFormik';
import { withDrawerActions } from '@/containers/Drawer/withDrawerActions';
@@ -62,13 +62,11 @@ function QuickVendorFormDrawer({
return (
<VendorFormProvider vendorId={vendorId}>
<DrawerVendorFormLoading>
<VendorFormCard>
<VendorFormFormik
initialValues={{ first_name: displayName }}
onSubmitSuccess={handleSubmitSuccess}
onCancel={handleCancelForm}
/>
</VendorFormCard>
</DrawerVendorFormLoading>
</VendorFormProvider>
);
@@ -79,20 +77,3 @@ export default R.compose(
withDashboardActions,
)(QuickVendorFormDrawer);
const VendorFormCard = styled(Card)`
margin: 15px;
padding: 25px;
margin-bottom: calc(15px + 65px);
${VendorFormHeaderPrimary} {
padding-top: 0;
}
.page-form {
padding: 0;
&__floating-actions {
margin-left: -41px;
margin-right: -41px;
}
}
`;
@@ -18,7 +18,6 @@ export default function QuickWriteVendorDrawerContent({ displayName, autofillRef
<DrawerHeaderContent
name={DRAWERS.QUICK_CREATE_CUSTOMER}
title={<T id={'create_a_new_vendor'} />}
/>
<DrawerBody>
<QuickVendorFormDrawer displayName={displayName} autofillRef={autofillRef} />
@@ -5,7 +5,7 @@ import { Dragzone, FormattedMessage as T } from '@/components';
/**
* Vendor Attachment Tab.
*/
function VendorAttachmentTab() {
export function VendorAttachmentTab() {
return (
<div>
<Dragzone
@@ -17,5 +17,3 @@ function VendorAttachmentTab() {
</div>
);
}
export default VendorAttachmentTab;
@@ -0,0 +1,88 @@
// @ts-nocheck
import { Box } from '@/components';
import {
FormattedMessage as T,
FFormGroup,
FInputGroup,
FTextArea,
} from '@/components';
import { VendorFormSectionTitle } from './VendorFormSectionTitle';
export function VendorBillingAddress() {
return (
<Box data-section-id="billingAddress">
<VendorFormSectionTitle>
<T id={'billing_address'} />
</VendorFormSectionTitle>
<FFormGroup
name={'billing_address_country'}
label={<T id={'country'} />}
inline
fill
fastField
>
<FInputGroup name={'billing_address_country'} fill fastField />
</FFormGroup>
<FFormGroup
name={'billing_address1'}
label={<T id={'address_line_1'} />}
inline
fill
fastField
>
<FTextArea name={'billing_address1'} fill fastField />
</FFormGroup>
<FFormGroup
name={'billing_address2'}
label={<T id={'address_line_2'} />}
inline
fill
fastField
>
<FTextArea name={'billing_address2'} fill fastField />
</FFormGroup>
<FFormGroup
name={'billing_address_city'}
label={<T id={'city_town'} />}
inline
fill
fastField
>
<FInputGroup name={'billing_address_city'} fill fastField />
</FFormGroup>
<FFormGroup
name={'billing_address_state'}
label={<T id={'state'} />}
inline
fill
fastField
>
<FInputGroup name={'billing_address_state'} fill fastField />
</FFormGroup>
<FFormGroup
name={'billing_address_postcode'}
label={<T id={'zip_code'} />}
inline
fill
fastField
>
<FInputGroup name={'billing_address_postcode'} fill fastField />
</FFormGroup>
<FFormGroup
name={'billing_address_phone'}
label={<T id={'phone'} />}
inline
fill
fastField
>
<FInputGroup name={'billing_address_phone'} fill fastField />
</FFormGroup>
</Box>
);
}
@@ -28,7 +28,7 @@ import { useCurrentOrganization } from '@/hooks/state';
/**
* Vendor Finaniceal Panel Tab.
*/
export default function VendorFinanicalPanelTab() {
export function VendorFinanicalPanelTab() {
const { currencies, branches } = useVendorFormContext();
// Sets the primary branch to form.
@@ -44,10 +44,12 @@ export default function VendorFinanicalPanelTab() {
label={<T id={'currency'} />}
fastField
inline
fastField
>
<CurrencySelectList
name="currency_code"
items={currencies}
fastField
/>
</FFormGroup>
@@ -93,16 +95,17 @@ function VendorOpeningBalanceField() {
<FFormGroup
name={'opening_balance'}
label={<T id={'opening_balance'} />}
inline={true}
shouldUpdate={openingBalanceFieldShouldUpdate}
shouldUpdateDeps={{ currencyCode: values.currency_code }}
fastField={true}
inline
fastField
>
<ControlGroup>
<InputPrependText text={values.currency_code} />
<FMoneyInputGroup
name={'opening_balance'}
inputGroupProps={{ fill: true }}
fastField
/>
</ControlGroup>
</FFormGroup>
@@ -123,8 +126,9 @@ function VendorOpeningBalanceAtField() {
<FFormGroup
name={'opening_balance_at'}
label={<T id={'opening_balance_at'} />}
inline={true}
helperText={<ErrorMessage name="opening_balance_at" />}
inline
fastField
>
<FDateInput
name={'opening_balance_at'}
@@ -132,7 +136,8 @@ function VendorOpeningBalanceAtField() {
disabled={vendorId}
formatDate={(date) => date.toLocaleDateString()}
parseDate={(str) => new Date(str)}
fill={true}
fill
fastField
/>
</FFormGroup>
);
@@ -156,12 +161,14 @@ function VendorOpeningBalanceExchangeRateField() {
<FFormGroup
label={' '}
name={'opening_balance_exchange_rate'}
inline={true}
inline
fastField
>
<ExchangeRateInputGroup
fromCurrency={values.currency_code}
toCurrency={currentOrganization.base_currency}
name={'opening_balance_exchange_rate'}
fastField
/>
</FFormGroup>
);
@@ -1,5 +1,4 @@
// @ts-nocheck
import React from 'react';
import {
Intent,
Button,
@@ -11,53 +10,37 @@ import {
MenuItem,
} from '@blueprintjs/core';
import styled from 'styled-components';
import classNames from 'classnames';
import { useFormikContext } from 'formik';
import { Group, Icon, FormattedMessage as T } from '@/components';
import { CLASSES } from '@/constants/classes';
import { useVendorFormContext } from './VendorFormProvider';
import { safeInvoke } from '@/utils';
/**
* Vendor floating actions bar.
*/
export default function VendorFloatingActions({ onCancel }) {
export function VendorFloatingActions() {
// Formik context.
const { resetForm, isSubmitting, submitForm } = useFormikContext();
const { isSubmitting, submitForm } = useFormikContext();
// Vendor form context.
const { isNewMode, setSubmitPayload } = useVendorFormContext();
// Handle the submit button.
const handleSubmitBtnClick = (event) => {
const handleSubmitBtnClick = () => {
setSubmitPayload({ noRedirect: false });
};
// Handle the submit & new button click.
const handleSubmitAndNewClick = (event) => {
const handleSubmitAndNewClick = () => {
submitForm();
setSubmitPayload({ noRedirect: true });
};
// Handle cancel button click.
const handleCancelBtnClick = (event) => {
safeInvoke(onCancel, event);
};
// Handle clear button click.
const handleClearBtnClick = (event) => {
resetForm();
};
return (
<Group
spacing={10}
className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}
>
<FloatingActionsGroup spacing={10}>
<ButtonGroup>
{/* ----------- Save and New ----------- */}
<SaveButton
<Button
disabled={isSubmitting}
loading={isSubmitting}
intent={Intent.PRIMARY}
@@ -74,9 +57,9 @@ export default function VendorFloatingActions({ onCancel }) {
/>
</Menu>
}
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
position={Position.BOTTOM_RIGHT}
minimal
>
<Button
disabled={isSubmitting}
@@ -85,24 +68,16 @@ export default function VendorFloatingActions({ onCancel }) {
/>
</Popover>
</ButtonGroup>
{/* ----------- Clear & Reset----------- */}
<Button
className={'ml1'}
disabled={isSubmitting}
onClick={handleClearBtnClick}
text={!isNewMode ? <T id={'reset'} /> : <T id={'clear'} />}
/>
{/* ----------- Cancel ----------- */}
<Button
className={'ml1'}
disabled={isSubmitting}
onClick={handleCancelBtnClick}
text={<T id={'cancel'} />}
/>
</Group>
</FloatingActionsGroup>
);
}
const SaveButton = styled(Button)`
min-width: 100px;
const FloatingActionsGroup = styled(Group)`
padding: 10px 0;
padding-left: 165px;
border-top: 1px solid #50555a;
position: sticky;
bottom: 0;
background: var(--color-card-background);
z-index: 1;
`;
@@ -2,21 +2,22 @@
import React from 'react';
import intl from 'react-intl-universal';
import { ControlGroup } from '@blueprintjs/core';
import { FormattedMessage as T, FFormGroup, FInputGroup } from '@/components';
import { FormattedMessage as T, FFormGroup, FInputGroup, Box } from '@/components';
/**
* Vendor form after primary section.
*/
function VendorFormAfterPrimarySection() {
export function VendorFormAfterPrimarySection() {
return (
<div className={'customer-form__after-primary-section-content'}>
<Box>
{/*------------ Vendor email -----------*/}
<FFormGroup
name={'email'}
label={<T id={'vendor_email'} />}
inline={true}
inline
fastField
>
<FInputGroup name={'email'} />
<FInputGroup name={'email'} fastField />
</FFormGroup>
{/*------------ Phone number -----------*/}
@@ -24,23 +25,23 @@ function VendorFormAfterPrimarySection() {
name={'work_phone'}
className={'form-group--phone-number'}
label={<T id={'phone_number'} />}
inline={true}
inline
fastField
>
<ControlGroup>
<FInputGroup name={'work_phone'} placeholder={intl.get('work')} />
<FInputGroup name={'work_phone'} placeholder={intl.get('work')} fastField />
<FInputGroup
name={'personal_phone'}
placeholder={intl.get('mobile')}
fastField
/>
</ControlGroup>
</FFormGroup>
{/*------------ Vendor website -----------*/}
<FFormGroup name={'website'} label={<T id={'website'} />} inline={true}>
<FInputGroup name={'website'} placeholder={'http://'} />
<FFormGroup name={'website'} label={<T id={'website'} />} inline fastField>
<FInputGroup name={'website'} placeholder={'http://'} fastField />
</FFormGroup>
</div>
</Box>
);
}
export default VendorFormAfterPrimarySection;
@@ -0,0 +1,141 @@
// @ts-nocheck
import intl from 'react-intl-universal';
import { ControlGroup, Divider, Icon as BlueprintIcon } from '@blueprintjs/core';
import {
Hint,
FieldRequiredHint,
SalutationList,
DisplayNameList,
FormattedMessage as T,
FInputGroup,
FFormGroup,
Box,
Icon,
Stack,
} from '@/components';
import { VendorFormSectionTitle } from './VendorFormSectionTitle';
import { useAutofocus } from '@/hooks';
export function VendorFormBasicSection({}) {
const firstNameFieldRef = useAutofocus();
return (
<Box data-section-id="primary">
<VendorFormSectionTitle>Vendor details</VendorFormSectionTitle>
{/**----------- Contact name -----------*/}
<FFormGroup
name={'salutation'}
label={<T id={'contact_name'} />}
inline
fill
fastField
>
<ControlGroup fill>
<SalutationList
name={'salutation'}
popoverProps={{ minimal: true }}
fastField
/>
<FInputGroup
name={'first_name'}
placeholder={intl.get('first_name')}
inputRef={(ref) => (firstNameFieldRef.current = ref)}
fill
fastField
/>
<FInputGroup
name={'last_name'}
placeholder={intl.get('last_name')}
fill
fastField
/>
</ControlGroup>
</FFormGroup>
<FFormGroup
name={'code'}
label={'Vendor Code'}
helperText="Add a unique account number to identify, reference and search for the contact."
inline
fill
fastField
>
<FInputGroup name={'code'} fill fastField />
</FFormGroup>
{/*----------- Company Name -----------*/}
<FFormGroup
name={'company_name'}
label={<T id={'company_name'} />}
inline
fill
fastField
>
<FInputGroup name={'company_name'} fill fastField />
</FFormGroup>
{/*----------- Display Name -----------*/}
<FFormGroup
name={'display_name'}
label={<T id={'display_name'} />}
helperText="This is the name that appears on invoices and emails."
inline
fill
fastField
>
<DisplayNameList
name={'display_name'}
popoverProps={{ minimal: true }}
buttonProps={{ fill: true }}
fastField
/>
</FFormGroup>
<Divider style={{ margin: '20px 0' }} />
{/*------------ Vendor email -----------*/}
<FFormGroup
name={'email'}
label={<T id={'vendor_email'} />}
inline
fastField
>
<FInputGroup
name={'email'}
leftIcon={<Icon icon="envelope" />}
fastField
/>
</FFormGroup>
{/*------------ Phone number -----------*/}
<FFormGroup
name={'work_phone'}
className={'form-group--phone-number'}
label={<T id={'phone_number'} />}
inline
fastField
>
<Stack spacing={10}>
<FInputGroup name={'work_phone'} placeholder={intl.get('work')} leftIcon="phone" fastField
/>
<FInputGroup
name={'personal_phone'}
placeholder={intl.get('mobile')}
fastField
/>
</Stack>
</FFormGroup>
{/*------------ Vendor website -----------*/}
<FFormGroup name={'website'} label={<T id={'website'} />} inline fastField>
<FInputGroup
name={'website'}
placeholder={'http://'}
leftIcon={<BlueprintIcon icon="globe-network" />}
fastField
/>
</FFormGroup>
</Box>
);
}
@@ -0,0 +1,45 @@
// @ts-nocheck
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 { VendorFloatingActions } from "./VendorFloatingActions";
import { VendorFormSections } from "./VendorFormFields";
export function VendorFormContent() {
const [selectedTabId, setSelectedTabId] = useState('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
selectedTabId={selectedTabId}
onChange={handleTabChange}
className={css`position: sticky; top: 20px;`}
vertical
>
<Tab id={'primary'} title={'Basic'} />
<Tab id={'financial'} title={'Financial'} />
<Tab id={'billingAddress'} title={'Billing address'} />
<Tab id={'shippingAddress'} title={'Shipping address'} />
<Tab id={'notes'} title={'Notes'} />
</Tabs>
<VendorFormSections />
</Group>
<VendorFloatingActions />
</Card>
)
}
@@ -0,0 +1,34 @@
// @ts-nocheck
import { Divider } from '@blueprintjs/core';
import { css } from '@emotion/css';
import { Box } from '@/components';
import { VendorFormBasicSection } from './VendorFormBasicSection';
import { VendorFormFinancialSection } from './VendorFormFinancialSection';
import { VendorBillingAddress } from './VendorBillingAddress';
import { VendorShippingAddress } from './VendorShippingAddress';
import { VendorFormNotesSection } from './VendorFormNotesSection';
const vendorFormSectionDividerClass = css`
margin: 20px 0;
`;
export function VendorFormSections() {
return (
<Box>
<VendorFormBasicSection />
<Divider className={vendorFormSectionDividerClass} />
<VendorFormFinancialSection />
<Divider className={vendorFormSectionDividerClass} />
<VendorBillingAddress />
<Divider className={vendorFormSectionDividerClass} />
<VendorShippingAddress />
<Divider className={vendorFormSectionDividerClass} />
<VendorFormNotesSection />
</Box>
);
}
@@ -0,0 +1,159 @@
// @ts-nocheck
import { FormGroup, Position, ControlGroup } from '@blueprintjs/core';
import { ErrorMessage, useFormikContext } from 'formik';
import { Features } from '@/constants';
import {
FFormGroup,
FormattedMessage as T,
InputPrependText,
CurrencySelectList,
BranchSelect,
FeatureCan,
FMoneyInputGroup,
ExchangeRateInputGroup,
FDateInput,
Icon,
Box,
} from '@/components';
import { useVendorFormContext } from './VendorFormProvider';
import {
openingBalanceFieldShouldUpdate,
useIsVendorForeignCurrency,
useSetPrimaryBranchToForm,
} from './utils';
import { useCurrentOrganization } from '@/hooks/state';
import { VendorFormSectionTitle } from './VendorFormSectionTitle';
export function VendorFormFinancialSection() {
const { currencies, vendorId, branches } = useVendorFormContext();
// Sets the primary branch to form.
useSetPrimaryBranchToForm();
return (
<Box data-section-id="financial">
<VendorFormSectionTitle>
<T id={'financial_details'} />
</VendorFormSectionTitle>
<FFormGroup
name={'currency_code'}
label={<T id={'currency'} />}
fastField
inline
fill
>
<CurrencySelectList
name="currency_code"
items={currencies}
disabled={vendorId}
fastField
/>
</FFormGroup>
<VendorOpeningBalanceField />
<VendorOpeningBalanceExchangeRateField />
<VendorOpeningBalanceAtField />
<FeatureCan feature={Features.Branches}>
<FFormGroup
label={<T id={'vendor.label.opening_branch'} />}
name={'opening_balance_branch_id'}
inline
fill
>
<BranchSelect
name={'opening_balance_branch_id'}
branches={branches}
popoverProps={{ minimal: true }}
/>
</FFormGroup>
</FeatureCan>
</Box>
);
}
/**
* Vendor opening balance at date field.
* @returns {JSX.Element}
*/
function VendorOpeningBalanceAtField() {
const { vendorId } = useVendorFormContext();
// Cannot continue if the vendor id is defined.
if (vendorId) return null;
return (
<FFormGroup
name={'opening_balance_at'}
label={<T id={'opening_balance_at'} />}
inline
fill
helperText={<ErrorMessage name="opening_balance_at" />}
>
<FDateInput
name={'opening_balance_at'}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
disabled={vendorId}
formatDate={(date) => date.toLocaleDateString()}
parseDate={(str) => new Date(str)}
inputProps={{
leftIcon: <Icon icon={'date-range'} />,
}}
fill={true}
/>
</FFormGroup>
);
}
function VendorOpeningBalanceField() {
const { vendorId } = useVendorFormContext();
const { values } = useFormikContext();
// Cannot continue if the vendor id is defined.
if (vendorId) return null;
return (
<FFormGroup
label={<T id={'opening_balance'} />}
name={'opening_balance'}
inline
shouldUpdate={openingBalanceFieldShouldUpdate}
shouldUpdateDeps={{ currencyCode: values.currency_code }}
fastField={true}
fill
>
<ControlGroup fill>
<InputPrependText text={values.currency_code as string} />
<FMoneyInputGroup
name={'opening_balance'}
fastField
inputGroupProps={{ fill: true }}
/>
</ControlGroup>
</FFormGroup>
);
}
function VendorOpeningBalanceExchangeRateField() {
const { values } = useFormikContext();
const { vendorId } = useVendorFormContext();
const currentOrganization = useCurrentOrganization();
const isForeignVendor = useIsVendorForeignCurrency();
// Can't continue if the vendor is not foreign.
if (!isForeignVendor || vendorId) {
return null;
}
return (
<ExchangeRateInputGroup
fromCurrency={values.currency_code}
toCurrency={currentOrganization.base_currency}
name={'opening_balance_exchange_rate'}
onRecalcConfirm={() => {}}
onCancel={() => {}}
formGroupProps={{ label: ' ' }}
/>
);
}
@@ -1,5 +1,5 @@
// @ts-nocheck
import React, { useMemo } from 'react';
import { useMemo } from 'react';
import intl from 'react-intl-universal';
import { Formik, Form } from 'formik';
import { Intent } from '@blueprintjs/core';
@@ -7,16 +7,13 @@ import classNames from 'classnames';
import styled from 'styled-components';
import { CLASSES } from '@/constants/classes';
import { AppToaster } from '@/components';
import { AppToaster, Box } from '@/components';
import {
CreateVendorFormSchema,
EditVendorFormSchema,
} from './VendorForm.schema';
import VendorTabs from './VendorsTabs';
import VendorFormPrimarySection from './VendorFormPrimarySection';
import VendorFormAfterPrimarySection from './VendorFormAfterPrimarySection';
import VendorFloatingActions from './VendorFloatingActions';
import { VendorFormContent } from './VendorFormContent';
import { withCurrentOrganization } from '@/containers/Organization/withCurrentOrganization';
@@ -24,12 +21,10 @@ import { useVendorFormContext } from './VendorFormProvider';
import { compose, transformToForm, safeInvoke, parseBoolean } from '@/utils';
import { defaultInitialValues } from './utils';
import '@/style/pages/Vendors/Form.scss';
/**
* Vendor form.
*/
function VendorFormFormik({
function VendorFormFormikBase({
// #withCurrentOrganization
organization: { base_currency },
@@ -52,9 +47,6 @@ function VendorFormFormik({
isNewMode,
} = useVendorFormContext();
/**
* Initial values in create and edit mode.
*/
const initialFormValues = useMemo(
() => ({
...defaultInitialValues,
@@ -106,13 +98,6 @@ function VendorFormFormik({
};
return (
<div
className={classNames(
CLASSES.PAGE_FORM,
CLASSES.PAGE_FORM_VENDOR,
className,
)}
>
<Formik
validationSchema={
isNewMode ? CreateVendorFormSchema : EditVendorFormSchema
@@ -121,36 +106,26 @@ function VendorFormFormik({
onSubmit={handleFormSubmit}
>
<Form>
<VendorFormHeaderPrimary>
<VendorFormPrimarySection />
</VendorFormHeaderPrimary>
<div className={'page-form__after-priamry-section'}>
<VendorFormAfterPrimarySection />
</div>
<div className={classNames(CLASSES.PAGE_FORM_TABS)}>
<VendorTabs vendor={vendorId} />
</div>
<VendorFloatingActions onCancel={onCancel} />
<VendorFormFields>
<VendorFormContent onCancel={onCancel} />
</VendorFormFields>
</Form>
</Formik>
</div>
);
}
export const VendorFormHeaderPrimary = styled.div`
--x-color-border: #e4e4e4;
.bp4-dark & {
--x-color-border: var(--color-dark-gray3);
const VendorFormFields = styled.div`
.bp4-form-content,
.bp6-form-content {
min-width: 300px;
}
.bp4-form-group{
margin-bottom: 20px;
}
.bp4-form-group.bp4-inline label.bp4-label {
min-width: 140px;
}
padding: 10px 0 0;
margin: 0 0 20px;
overflow: hidden;
border-bottom: 1px solid var(--x-color-border);
max-width: 1000px;
`;
export default compose(withCurrentOrganization())(VendorFormFormik);
export const VendorFormFormik = compose(withCurrentOrganization())(VendorFormFormikBase);
@@ -0,0 +1,17 @@
// @ts-nocheck
import { Box, FFormGroup, FormattedMessage as T, FTextArea } from '@/components';
import { VendorFormSectionTitle } from './VendorFormSectionTitle';
export function VendorFormNotesSection() {
return (
<Box data-section-id="notes">
<VendorFormSectionTitle>
<T id={'notes'} />
</VendorFormSectionTitle>
<FFormGroup name={'note'} label={<T id={'note'} />} inline fill fastField>
<FTextArea name={'note'} fill fastField />
</FFormGroup>
</Box>
);
}
@@ -2,12 +2,9 @@
import React from 'react';
import styled from 'styled-components';
import { useParams, useHistory } from 'react-router-dom';
import '@/style/pages/Vendors/PageForm.scss';
import { DashboardCard, DashboardInsider } from '@/components';
import { Box, DashboardCard, DashboardInsider } from '@/components';
import { VendorFormProvider, useVendorFormContext } from './VendorFormProvider';
import VendorFormFormik from './VendorFormFormik';
import { VendorFormFormik } from './VendorFormFormik';
/**
* Vendor form page loading wrapper.
@@ -17,16 +14,16 @@ function VendorFormPageLoading({ children }) {
const { isFormLoading } = useVendorFormContext();
return (
<VendorDashboardInsider loading={isFormLoading}>
<DashboardInsider loading={isFormLoading}>
{children}
</VendorDashboardInsider>
</DashboardInsider>
);
}
/**
* Vendor form page.
*/
export default function VendorFormPage() {
export function VendorFormPage() {
const history = useHistory();
const { id } = useParams();
@@ -44,26 +41,13 @@ export default function VendorFormPage() {
return (
<VendorFormProvider vendorId={id}>
<VendorFormPageLoading>
<DashboardCard page>
<VendorFormPageFormik
<Box mx={'auto'} maxWidth={800}>
<VendorFormFormik
onSubmitSuccess={handleSubmitSuccess}
onCancel={handleFormCancel}
/>
</DashboardCard>
</Box>
</VendorFormPageLoading>
</VendorFormProvider>
);
}
const VendorFormPageFormik = styled(VendorFormFormik)`
.page-form {
&__floating-actions {
margin-left: -40px;
margin-right: -40px;
}
}
`;
const VendorDashboardInsider = styled(DashboardInsider)`
padding-bottom: 64px;
`;
@@ -1,84 +0,0 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import classNames from 'classnames';
import { ControlGroup } from '@blueprintjs/core';
import {
FormattedMessage as T,
FFormGroup,
FInputGroup,
Hint,
FieldRequiredHint,
SalutationList,
DisplayNameList,
} from '@/components';
import { CLASSES } from '@/constants/classes';
import { useAutofocus } from '@/hooks';
/**
* Vendor form primary section.
*/
function VendorFormPrimarySection() {
const firstNameFieldRef = useAutofocus();
return (
<div className={'customer-form__primary-section-content'}>
{/**----------- Vendor name -----------*/}
<FFormGroup
name={'salutation'}
className={classNames('form-group--contact_name')}
label={<T id={'contact_name'} />}
inline={true}
>
<ControlGroup>
<SalutationList
name={'salutation'}
popoverProps={{ minimal: true }}
/>
<FInputGroup
name={'first_name'}
placeholder={intl.get('first_name')}
className={classNames('input-group--first-name')}
inputRef={(ref) => (firstNameFieldRef.current = ref)}
/>
<FInputGroup
name={'last_name'}
placeholder={intl.get('last_name')}
className={classNames('input-group--last-name')}
/>
</ControlGroup>
</FFormGroup>
{/*----------- Company Name -----------*/}
<FFormGroup
name={'company_name'}
className={classNames('form-group--company_name')}
label={<T id={'company_name'} />}
inline={true}
>
<FInputGroup name={'company_name'} />
</FFormGroup>
{/*----------- Display Name -----------*/}
<FFormGroup
name={'display_name'}
label={
<>
<T id={'display_name'} />
<FieldRequiredHint />
<Hint />
</>
}
fastField
inline
>
<DisplayNameList
name={'display_name'}
popoverProps={{ minimal: true }}
/>
</FFormGroup>
</div>
);
}
export default VendorFormPrimarySection;
@@ -33,7 +33,6 @@ function VendorFormProvider({ query, vendorId, ...props }) {
const { data: vendor, isLoading: isVendorLoading } = useVendor(vendorId, {
enabled: !!vendorId,
});
// Handle fetch contact duplicate details.
const { data: contactDuplicate, isLoading: isContactLoading } = useContact(
contactId,
@@ -0,0 +1,12 @@
import { css } from '@emotion/css';
const vendorFormSectionTitleClass = css`
font-size: 14px;
color: #8f99a8;
margin-bottom: 18px;
margin-top: 10px;
`;
export function VendorFormSectionTitle({ children }: { children: React.ReactNode | string }) {
return <h4 className={vendorFormSectionTitleClass}>{children}</h4>;
}
@@ -0,0 +1,88 @@
// @ts-nocheck
import { Box } from '@/components';
import {
FormattedMessage as T,
FFormGroup,
FInputGroup,
FTextArea,
} from '@/components';
import { VendorFormSectionTitle } from './VendorFormSectionTitle';
export function VendorShippingAddress() {
return (
<Box data-section-id="shippingAddress">
<VendorFormSectionTitle>
<T id={'shipping_address'} />
</VendorFormSectionTitle>
<FFormGroup
name={'shipping_address_country'}
label={<T id={'country'} />}
inline
fill
fastField
>
<FInputGroup name={'shipping_address_country'} fill fastField />
</FFormGroup>
<FFormGroup
name={'shipping_address1'}
label={<T id={'address_line_1'} />}
inline
fill
fastField
>
<FTextArea name={'shipping_address1'} fill fastField />
</FFormGroup>
<FFormGroup
name={'shipping_address2'}
label={<T id={'address_line_2'} />}
inline
fill
fastField
>
<FTextArea name={'shipping_address2'} fill fastField />
</FFormGroup>
<FFormGroup
name={'shipping_address_city'}
label={<T id={'city_town'} />}
inline
fill
fastField
>
<FInputGroup name={'shipping_address_city'} fill fastField />
</FFormGroup>
<FFormGroup
name={'shipping_address_state'}
label={<T id={'state'} />}
inline
fill
fastField
>
<FInputGroup name={'shipping_address_state'} fill fastField />
</FFormGroup>
<FFormGroup
name={'shipping_address_postcode'}
label={<T id={'zip_code'} />}
inline
fill
fastField
>
<FInputGroup name={'shipping_address_postcode'} fill fastField />
</FFormGroup>
<FFormGroup
name={'shipping_address_phone'}
label={<T id={'phone'} />}
inline
fill
fastField
>
<FInputGroup name={'shipping_address_phone'} fill fastField />
</FFormGroup>
</Box>
);
}
@@ -1,43 +0,0 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import classNames from 'classnames';
import { Tabs, Tab } from '@blueprintjs/core';
import { CLASSES } from '@/constants/classes';
import VendorFinanicalPanelTab from './VendorFinanicalPanelTab';
import CustomerAddressTabs from '@/containers/Customers/CustomerForm/CustomerAddressTabs';
import CustomerNotePanel from '@/containers/Customers/CustomerForm/CustomerNotePanel';
/**
* Vendor form tabs.
*/
export default function VendorTabs() {
return (
<div className={classNames(CLASSES.PAGE_FORM_TABS)}>
<Tabs
animate={true}
id={'vendor-tabs'}
large={true}
defaultSelectedTabId="financial"
>
<Tab
id={'financial'}
title={intl.get('financial_details')}
panel={<VendorFinanicalPanelTab />}
/>
<Tab
id={'address'}
title={intl.get('address')}
panel={<CustomerAddressTabs />}
/>
<Tab
id="notes"
title={intl.get('notes')}
panel={<CustomerNotePanel />}
/>
</Tabs>
</div>
);
}
@@ -13,6 +13,7 @@ export const defaultInitialValues = {
last_name: '',
company_name: '',
display_name: '',
code: '',
email: '',
work_phone: '',
+2 -2
View File
@@ -1942,8 +1942,8 @@
"vendor_opening_balance.label": "Edit Vendor Opening Balance",
"vendor_opening_balance.label.opening_balance": "Opening balance",
"vendor_opening_balance.label.opening_balance_at": "Opening balance at",
"customer.label.opening_branch": "Opening Balance Branch",
"vendor.label.opening_branch": "Opening Balance Branch",
"customer.label.opening_branch": "Balance Branch",
"vendor.label.opening_branch": "Balance Branch",
"warehouse.error.warehouse_code_not_unique": "Warehouse code not unique",
"warehouse.error.warehouse_has_associated_transactions": "You could not delete the warehouse that has associated transactions.",
"branche.error.warehouse_code_not_unique": "Branch code not unique",
+2 -2
View File
@@ -619,7 +619,7 @@ export const getDashboardRoutes = () => [
{
path: `/vendors/:id/edit`,
component: lazy(
() => import('@/containers/Vendors/VendorForm/VendorFormPage'),
() => import('@/containers/Vendors/VendorForm/VendorFormPage').then(module => ({ default: module.VendorFormPage })),
),
name: 'vendor-edit',
breadcrumb: intl.get('edit_vendor'),
@@ -631,7 +631,7 @@ export const getDashboardRoutes = () => [
{
path: `/vendors/new`,
component: lazy(
() => import('@/containers/Vendors/VendorForm/VendorFormPage'),
() => import('@/containers/Vendors/VendorForm/VendorFormPage').then(module => ({ default: module.VendorFormPage })),
),
name: 'vendor-new',
breadcrumb: intl.get('new_vendor'),
@@ -8,6 +8,12 @@
min-height: 32px;
padding-left: 12px;
padding-right: 12px;
&.bp4-outlined {
.bp4-dark & {
border-color: rgba(255, 255, 255, 0.2);
}
}
}
.bp4-button:not([class*='bp4-intent-']) {
@@ -1,156 +0,0 @@
@import '../../_base.scss';
.page-form--customer {
$self: '.page-form';
padding: 20px;
--x-color-tabs-border: #f0f0f0;
.bp4-dark & {
--x-color-tabs-border: var(--color-dark-gray3);
}
#{$self}__header {
padding: 0;
}
#{$self}__primary-section {
padding: 10px 0 0;
margin: 0 0 20px;
overflow: hidden;
border-bottom: 1px solid #e4e4e4;
max-width: 1000px;
}
.bp4-form-group {
max-width: 500px;
.bp4-control {
margin-top: 8px;
margin-bottom: 8px;
}
&.bp4-inline {
.bp4-label {
min-width: 150px;
}
}
.bp4-form-content {
width: 100%;
}
}
.form-group--contact_name {
max-width: 600px;
.bp4-control-group > * {
flex-shrink: unset;
&:not(:last-child) {
padding-right: 10px;
}
&.input-group--salutation-list {
width: 25%;
}
&.input-group--first-name,
&.input-group--last-name {
width: 37%;
}
}
}
.bp4-form-group {
margin-bottom: 14px;
}
.bp4-tab-panel {
margin-top: 26px;
}
.form-group--phone-number {
.bp4-control-group > * {
flex-shrink: unset;
padding-right: 5px;
padding-left: 5px;
&:first-child {
padding-left: 0;
}
&:last-child {
padding-right: 0;
}
}
}
#{$self}__tabs {
margin-top: 20px;
max-width: 1000px;
h4 {
font-weight: 500;
color: #888;
margin-bottom: 1.2rem;
font-size: 14px;
}
// Tab panels.
.tab-panel {
&--address {
.bp4-form-group {
max-width: 440px;
&.bp4-inline {
.bp4-label {
min-width: 145px;
}
}
.bp4-form-content {
width: 100%;
}
textarea.bp4-input {
max-width: 100%;
width: 100%;
min-height: 50px;
}
}
}
&--note {
.form-group--note {
.bp4-form-group {
max-width: 600px;
}
textarea {
width: 100%;
min-height: 100px;
}
}
}
}
.dropzone-container {
max-width: 600px;
}
}
.bp4-tabs {
.bp4-tab-list {
position: relative;
&:before {
content: '';
position: absolute;
bottom: 0;
width: 100%;
height: 2px;
background: var(--x-color-tabs-border);
}
> *:not(:last-child) {
margin-right: 25px;
}
&.bp4-large > .bp4-tab {
font-size: 15px;
}
}
}
}