1
0
This commit is contained in:
Ahmed Bouhuolia
2026-03-26 14:59:45 +02:00
parent 75699ba810
commit aa89484b64
11 changed files with 597 additions and 84 deletions
@@ -0,0 +1,81 @@
// @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
>
<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,5 +1,4 @@
// @ts-nocheck // @ts-nocheck
import React from 'react';
import { import {
Intent, Intent,
Button, Button,
@@ -11,18 +10,15 @@ import {
MenuItem, MenuItem,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import styled from 'styled-components'; import styled from 'styled-components';
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 { useVendorFormContext } from './VendorFormProvider'; import { useVendorFormContext } from './VendorFormProvider';
import { safeInvoke } from '@/utils';
/** /**
* Vendor floating actions bar. * Vendor floating actions bar.
*/ */
export default function VendorFloatingActions({ onCancel }) { export function VendorFloatingActions() {
// Formik context. // Formik context.
const { resetForm, isSubmitting, submitForm } = useFormikContext(); const { resetForm, isSubmitting, submitForm } = useFormikContext();
@@ -30,31 +26,23 @@ export default function VendorFloatingActions({ onCancel }) {
const { isNewMode, setSubmitPayload } = useVendorFormContext(); const { isNewMode, setSubmitPayload } = useVendorFormContext();
// Handle the submit button. // Handle the submit button.
const handleSubmitBtnClick = (event) => { const handleSubmitBtnClick = () => {
setSubmitPayload({ noRedirect: false }); setSubmitPayload({ noRedirect: false });
}; };
// Handle the submit & new button click. // Handle the submit & new button click.
const handleSubmitAndNewClick = (event) => { const handleSubmitAndNewClick = () => {
submitForm(); submitForm();
setSubmitPayload({ noRedirect: true }); setSubmitPayload({ noRedirect: true });
}; };
// Handle cancel button click.
const handleCancelBtnClick = (event) => {
safeInvoke(onCancel, event);
};
// Handle clear button click. // Handle clear button click.
const handleClearBtnClick = (event) => { const handleClearBtnClick = () => {
resetForm(); resetForm();
}; };
return ( return (
<Group <FloatingActionsGroup spacing={10}>
spacing={10}
className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}
>
<ButtonGroup> <ButtonGroup>
{/* ----------- Save and New ----------- */} {/* ----------- Save and New ----------- */}
<SaveButton <SaveButton
@@ -92,17 +80,19 @@ export default function VendorFloatingActions({ onCancel }) {
onClick={handleClearBtnClick} onClick={handleClearBtnClick}
text={!isNewMode ? <T id={'reset'} /> : <T id={'clear'} />} text={!isNewMode ? <T id={'reset'} /> : <T id={'clear'} />}
/> />
{/* ----------- Cancel ----------- */} </FloatingActionsGroup>
<Button
className={'ml1'}
disabled={isSubmitting}
onClick={handleCancelBtnClick}
text={<T id={'cancel'} />}
/>
</Group>
); );
} }
const SaveButton = styled(Button)` const FloatingActionsGroup = styled(Group)`
min-width: 100px; padding: 12px 0;
padding-left: 165px;
border-top: 1px solid #50555a;
position: sticky;
bottom: 0;
background: var(--color-card-background);
`;
const SaveButton = styled(Button)`
min-width: 80px;
`; `;
@@ -0,0 +1,126 @@
// @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
>
<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>
{/*----------- 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'} />
<FieldRequiredHint />
<Hint />
</>
}
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
>
<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>
<FInputGroup
name={'website'}
placeholder={'http://'}
leftIcon={<BlueprintIcon icon="globe-network" />}
/>
</FFormGroup>
</Box>
);
}
@@ -0,0 +1,55 @@
// @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";
const vendorFormSections = {
primary: 'primary',
financial: 'financial',
billingAddress: 'billingAddress',
shippingAddress: 'shippingAddress',
notes: 'notes',
};
export function VendorFormContent() {
const [selectedTabId, setSelectedTabId] = useState(vendorFormSections.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={vendorFormSections.primary} title={'Basic'} />
<Tab id={vendorFormSections.financial} title={'Financial'} />
<Tab id={vendorFormSections.billingAddress} title={'Billing address'} />
<Tab id={vendorFormSections.shippingAddress} title={'Ship address'} />
<Tab id={vendorFormSections.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,154 @@
// @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,
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}
/>
</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)}
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 // @ts-nocheck
import React, { useMemo } from 'react'; import { useMemo } from 'react';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import { Formik, Form } from 'formik'; import { Formik, Form } from 'formik';
import { Intent } from '@blueprintjs/core'; import { Intent } from '@blueprintjs/core';
@@ -7,16 +7,13 @@ import classNames from 'classnames';
import styled from 'styled-components'; import styled from 'styled-components';
import { CLASSES } from '@/constants/classes'; import { CLASSES } from '@/constants/classes';
import { AppToaster } from '@/components'; import { AppToaster, Box } from '@/components';
import { import {
CreateVendorFormSchema, CreateVendorFormSchema,
EditVendorFormSchema, EditVendorFormSchema,
} from './VendorForm.schema'; } from './VendorForm.schema';
import VendorTabs from './VendorsTabs'; import { VendorFormContent } from './VendorFormContent';
import VendorFormPrimarySection from './VendorFormPrimarySection';
import VendorFormAfterPrimarySection from './VendorFormAfterPrimarySection';
import VendorFloatingActions from './VendorFloatingActions';
import { withCurrentOrganization } from '@/containers/Organization/withCurrentOrganization'; import { withCurrentOrganization } from '@/containers/Organization/withCurrentOrganization';
@@ -24,8 +21,6 @@ import { useVendorFormContext } from './VendorFormProvider';
import { compose, transformToForm, safeInvoke, parseBoolean } from '@/utils'; import { compose, transformToForm, safeInvoke, parseBoolean } from '@/utils';
import { defaultInitialValues } from './utils'; import { defaultInitialValues } from './utils';
import '@/style/pages/Vendors/Form.scss';
/** /**
* Vendor form. * Vendor form.
*/ */
@@ -106,51 +101,34 @@ function VendorFormFormik({
}; };
return ( return (
<div <Box mx={'auto'} maxWidth={800}>
className={classNames(
CLASSES.PAGE_FORM,
CLASSES.PAGE_FORM_VENDOR,
className,
)}
>
<Formik <Formik
validationSchema={ validationSchema={
isNewMode ? CreateVendorFormSchema : EditVendorFormSchema isNewMode ? CreateVendorFormSchema : EditVendorFormSchema
} }
initialValues={initialFormValues} initialValues={initialFormValues}
onSubmit={handleFormSubmit} onSubmit={handleFormSubmit}
> >
<Form> <Form>
<VendorFormHeaderPrimary> <VendorFormFields>
<VendorFormPrimarySection /> <VendorFormContent onCancel={onCancel} />
</VendorFormHeaderPrimary> </VendorFormFields>
<div className={'page-form__after-priamry-section'}>
<VendorFormAfterPrimarySection />
</div>
<div className={classNames(CLASSES.PAGE_FORM_TABS)}>
<VendorTabs vendor={vendorId} />
</div>
<VendorFloatingActions onCancel={onCancel} />
</Form> </Form>
</Formik> </Formik>
</div> </Box>
); );
} }
export const VendorFormHeaderPrimary = styled.div`
--x-color-border: #e4e4e4;
.bp4-dark & { const VendorFormFields = styled.div`
--x-color-border: var(--color-dark-gray3); .bp4-form-content,
.bp6-form-content {
min-width: 300px;
}
.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 default compose(withCurrentOrganization())(VendorFormFormik);
@@ -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>
<FTextArea name={'note'} fill />
</FFormGroup>
</Box>
);
}
@@ -17,9 +17,9 @@ function VendorFormPageLoading({ children }) {
const { isFormLoading } = useVendorFormContext(); const { isFormLoading } = useVendorFormContext();
return ( return (
<VendorDashboardInsider loading={isFormLoading}> <DashboardInsider loading={isFormLoading}>
{children} {children}
</VendorDashboardInsider> </DashboardInsider>
); );
} }
@@ -44,26 +44,11 @@ export default function VendorFormPage() {
return ( return (
<VendorFormProvider vendorId={id}> <VendorFormProvider vendorId={id}>
<VendorFormPageLoading> <VendorFormPageLoading>
<DashboardCard page> <VendorFormFormik
<VendorFormPageFormik
onSubmitSuccess={handleSubmitSuccess} onSubmitSuccess={handleSubmitSuccess}
onCancel={handleFormCancel} onCancel={handleFormCancel}
/> />
</DashboardCard>
</VendorFormPageLoading> </VendorFormPageLoading>
</VendorFormProvider> </VendorFormProvider>
); );
} }
const VendorFormPageFormik = styled(VendorFormFormik)`
.page-form {
&__floating-actions {
margin-left: -40px;
margin-right: -40px;
}
}
`;
const VendorDashboardInsider = styled(DashboardInsider)`
padding-bottom: 64px;
`;
@@ -0,0 +1,12 @@
import { css } from '@emotion/css';
const vendorFormSectionTitleClass = css`
font-size: 14px;
color: #8f99a8;
margin-bottom: 18px;
margin-top: 0;
`;
export function VendorFormSectionTitle({ children }: { children: React.ReactNode | string }) {
return <h4 className={vendorFormSectionTitleClass}>{children}</h4>;
}
@@ -0,0 +1,81 @@
// @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
>
<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>
);
}