re-structure to monorepo.

This commit is contained in:
a.bouhuolia
2023-02-03 01:02:31 +02:00
parent 8242ec64ba
commit 7a0a13f9d5
10400 changed files with 46966 additions and 17223 deletions

View File

@@ -0,0 +1,21 @@
// @ts-nocheck
import React from 'react';
import { Dragzone, FormattedMessage as T } from '@/components';
/**
* Vendor Attahment Tab.
*/
function VendorAttahmentTab() {
return (
<div>
<Dragzone
initialFiles={[]}
onDrop={null}
onDeleteFile={[]}
hint={<T id={'attachments_maximum'} />}
/>
</div>
);
}
export default VendorAttahmentTab;

View File

@@ -0,0 +1,136 @@
// @ts-nocheck
import React from 'react';
import moment from 'moment';
import classNames from 'classnames';
import { FormGroup, ControlGroup, Position, Classes } from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import { FastField, ErrorMessage } from 'formik';
import { Features } from '@/constants';
import {
FFormGroup,
MoneyInputGroup,
InputPrependText,
CurrencySelectList,
BranchSelect,
BranchSelectButton,
FeatureCan,
Row,
Col,
FormattedMessage as T,
} from '@/components';
import { useSetPrimaryBranchToForm } from './utils';
import { momentFormatter, tansformDateValue, inputIntent } from '@/utils';
import { useVendorFormContext } from './VendorFormProvider';
/**
* Vendor Finaniceal Panel Tab.
*/
export default function VendorFinanicalPanelTab() {
const { vendorId, currencies, branches } = useVendorFormContext();
// Sets the primary branch to form.
useSetPrimaryBranchToForm();
return (
<div className={'tab-panel--financial'}>
<Row>
<Col xs={6}>
{/*------------ Opening balance at -----------*/}
<FastField name={'opening_balance_at'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'opening_balance_at'} />}
className={classNames('form-group--select-list', Classes.FILL)}
intent={inputIntent({ error, touched })}
inline={true}
helperText={<ErrorMessage name="opening_balance_at" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
onChange={(date) => {
form.setFieldValue(
'opening_balance_at',
moment(date).format('YYYY-MM-DD'),
);
}}
value={tansformDateValue(value)}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
disabled={vendorId}
/>
</FormGroup>
)}
</FastField>
{/*------------ Opening balance -----------*/}
<FastField name={'opening_balance'}>
{({ form, field, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'opening_balance'} />}
className={classNames(
'form-group--opening-balance',
Classes.FILL,
)}
intent={inputIntent({ error, touched })}
inline={true}
>
<ControlGroup>
<InputPrependText text={form.values.currency_code} />
<MoneyInputGroup
value={value}
inputGroupProps={{
fill: true,
// ...field,
}}
onChange={(balance) => {
form.setFieldValue('opening_balance', balance);
}}
disabled={vendorId}
/>
</ControlGroup>
</FormGroup>
)}
</FastField>
{/*------------ Opening branch -----------*/}
<FeatureCan feature={Features.Branches}>
<FFormGroup
label={<T id={'vendor.label.opening_branch'} />}
name={'opening_balance_branch_id'}
inline={true}
className={classNames('form-group--select-list', Classes.FILL)}
>
<BranchSelect
name={'opening_balance_branch_id'}
branches={branches}
input={BranchSelectButton}
popoverProps={{ minimal: true }}
/>
</FFormGroup>
</FeatureCan>
{/*------------ Currency -----------*/}
<FastField name={'currency_code'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'currency'} />}
className={classNames(
'form-group--select-list',
'form-group--balance-currency',
Classes.FILL,
)}
inline={true}
>
<CurrencySelectList
currenciesList={currencies}
selectedCurrencyCode={value}
onCurrencySelected={(currency) => {
form.setFieldValue('currency_code', currency.currency_code);
}}
/>
</FormGroup>
)}
</FastField>
</Col>
</Row>
</div>
);
}

View File

@@ -0,0 +1,106 @@
// @ts-nocheck
import React from 'react';
import {
Intent,
Button,
ButtonGroup,
Popover,
PopoverInteractionKind,
Position,
Menu,
MenuItem,
} from '@blueprintjs/core';
import styled from 'styled-components';
import classNames from 'classnames';
import { useFormikContext } from 'formik';
import { 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 }) {
// Formik context.
const { resetForm, isSubmitting, submitForm } = useFormikContext();
// Vendor form context.
const { isNewMode, setSubmitPayload } = useVendorFormContext();
// Handle the submit button.
const handleSubmitBtnClick = (event) => {
setSubmitPayload({ noRedirect: false });
submitForm();
};
// Handle the submit & new button click.
const handleSubmitAndNewClick = (event) => {
submitForm();
setSubmitPayload({ noRedirect: true });
};
// Handle cancel button click.
const handleCancelBtnClick = (event) => {
safeInvoke(onCancel, event);
};
// Handle clear button click.
const handleClearBtnClick = (event) => {
resetForm();
};
return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
<ButtonGroup>
{/* ----------- Save and New ----------- */}
<SaveButton
disabled={isSubmitting}
loading={isSubmitting}
intent={Intent.PRIMARY}
type="submit"
onClick={handleSubmitBtnClick}
text={!isNewMode ? <T id={'edit'} /> : <T id={'save'} />}
/>
<Popover
content={
<Menu>
<MenuItem
text={<T id={'save_and_new'} />}
onClick={handleSubmitAndNewClick}
/>
</Menu>
}
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
/>
</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'} />}
/>
</div>
);
}
const SaveButton = styled(Button)`
min-width: 100px;
`;

View File

@@ -0,0 +1,43 @@
// @ts-nocheck
import * as Yup from 'yup';
import intl from 'react-intl-universal';
const Schema = Yup.object().shape({
salutation: Yup.string().trim(),
first_name: Yup.string().trim(),
last_name: Yup.string().trim(),
company_name: Yup.string().trim(),
display_name: Yup.string().trim().required().label(intl.get('display_name_')),
email: Yup.string().email().nullable(),
work_phone: Yup.number(),
personal_phone: Yup.number(),
website: Yup.string().url().nullable(),
active: Yup.boolean(),
note: Yup.string().trim(),
billing_address_country: Yup.string().trim(),
billing_address_1: Yup.string().trim(),
billing_address_2: Yup.string().trim(),
billing_address_city: Yup.string().trim(),
billing_address_state: Yup.string().trim(),
billing_address_postcode: Yup.number().nullable(),
billing_address_phone: Yup.number(),
shipping_address_country: Yup.string().trim(),
shipping_address_1: Yup.string().trim(),
shipping_address_2: Yup.string().trim(),
shipping_address_city: Yup.string().trim(),
shipping_address_state: Yup.string().trim(),
shipping_address_postcode: Yup.number().nullable(),
shipping_address_phone: Yup.number(),
opening_balance: Yup.number().nullable(),
currency_code: Yup.string(),
opening_balance_at: Yup.date(),
opening_balance_branch_id: Yup.string(),
});
export const CreateVendorFormSchema = Schema;
export const EditVendorFormSchema = Schema;

View File

@@ -0,0 +1,78 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import { FormGroup, InputGroup, ControlGroup } from '@blueprintjs/core';
import { FastField, ErrorMessage } from 'formik';
import { FormattedMessage as T } from '@/components';
import { inputIntent } from '@/utils';
/**
* Vendor form after primary section.
*/
function VendorFormAfterPrimarySection() {
return (
<div class="customer-form__after-primary-section-content">
{/*------------ Vendor email -----------*/}
<FastField name={'email'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'email'} />}
className={'form-group--email'}
label={<T id={'vendor_email'} />}
inline={true}
>
<InputGroup {...field} />
</FormGroup>
)}
</FastField>
{/*------------ Phone number -----------*/}
<FormGroup
className={'form-group--phone-number'}
label={<T id={'phone_number'} />}
inline={true}
>
<ControlGroup>
<FastField name={'work_phone'}>
{({ field, meta: { error, touched } }) => (
<InputGroup
intent={inputIntent({ error, touched })}
placeholder={intl.get('work')}
{...field}
/>
)}
</FastField>
<FastField name={'personal_phone'}>
{({ field, meta: { error, touched } }) => (
<InputGroup
intent={inputIntent({ error, touched })}
placeholder={intl.get('mobile')}
{...field}
/>
)}
</FastField>
</ControlGroup>
</FormGroup>
{/*------------ Vendor website -----------*/}
<FastField name={'website'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'website'} />}
className={'form-group--website'}
label={<T id={'website'} />}
inline={true}
>
<InputGroup placeholder={'http://'} {...field} />
</FormGroup>
)}
</FastField>
</div>
);
}
export default VendorFormAfterPrimarySection;

View File

@@ -0,0 +1,137 @@
// @ts-nocheck
import React, { useMemo } from 'react';
import intl from 'react-intl-universal';
import { Formik, Form } from 'formik';
import { Intent } from '@blueprintjs/core';
import classNames from 'classnames';
import { CLASSES } from '@/constants/classes';
import { AppToaster } 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 withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
import { useVendorFormContext } from './VendorFormProvider';
import { compose, transformToForm, safeInvoke } from '@/utils';
import { defaultInitialValues } from './utils';
import '@/style/pages/Vendors/Form.scss';
/**
* Vendor form.
*/
function VendorFormFormik({
// #withCurrentOrganization
organization: { base_currency },
// #ownProps
onSubmitSuccess,
onSubmitError,
onCancel,
className,
}) {
// Vendor form context.
const {
vendorId,
vendor,
contactDuplicate,
createVendorMutate,
editVendorMutate,
setSubmitPayload,
submitPayload,
isNewMode,
} = useVendorFormContext();
/**
* Initial values in create and edit mode.
*/
const initialValues = useMemo(
() => ({
...defaultInitialValues,
currency_code: base_currency,
...transformToForm(vendor, defaultInitialValues),
...transformToForm(contactDuplicate, defaultInitialValues),
}),
[vendor, contactDuplicate, base_currency],
);
// Handles the form submit.
const handleFormSubmit = (values, form) => {
const { setSubmitting, resetForm } = form;
const requestForm = { ...values };
setSubmitting(true);
const onSuccess = (response) => {
AppToaster.show({
message: intl.get(
isNewMode
? 'the_vendor_has_been_created_successfully'
: 'the_item_vendor_has_been_edited_successfully',
),
intent: Intent.SUCCESS,
});
setSubmitPayload(false);
setSubmitting(false);
resetForm();
safeInvoke(onSubmitSuccess, values, form, submitPayload, response);
};
const onError = () => {
setSubmitPayload(false);
setSubmitting(false);
safeInvoke(onSubmitError, values, form, submitPayload);
};
if (isNewMode) {
createVendorMutate(requestForm).then(onSuccess).catch(onError);
} else {
editVendorMutate([vendor.id, requestForm]).then(onSuccess).catch(onError);
}
};
return (
<div
className={classNames(
CLASSES.PAGE_FORM,
CLASSES.PAGE_FORM_VENDOR,
className,
)}
>
<Formik
validationSchema={
isNewMode ? CreateVendorFormSchema : EditVendorFormSchema
}
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
<Form>
<div className={classNames(CLASSES.PAGE_FORM_HEADER_PRIMARY)}>
<VendorFormPrimarySection />
</div>
<div className={'page-form__after-priamry-section'}>
<VendorFormAfterPrimarySection />
</div>
<div className={classNames(CLASSES.PAGE_FORM_TABS)}>
<VendorTabs vendor={vendorId} />
</div>
<VendorFloatingActions onCancel={onCancel} />
</Form>
</Formik>
</div>
);
}
export default compose(withCurrentOrganization())(VendorFormFormik);

View File

@@ -0,0 +1,69 @@
// @ts-nocheck
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 { VendorFormProvider, useVendorFormContext } from './VendorFormProvider';
import VendorFormFormik from './VendorFormFormik';
/**
* Vendor form page loading wrapper.
* @returns {JSX}
*/
function VendorFormPageLoading({ children }) {
const { isFormLoading } = useVendorFormContext();
return (
<VendorDashboardInsider loading={isFormLoading}>
{children}
</VendorDashboardInsider>
);
}
/**
* Vendor form page.
*/
export default function VendorFormPage() {
const history = useHistory();
const { id } = useParams();
// Handle the form submit success.
const handleSubmitSuccess = (values, formArgs, submitPayload) => {
if (!submitPayload.noRedirect) {
history.push('/vendors');
}
};
// Handle the form cancel button click.
const handleFormCancel = () => {
history.goBack();
};
return (
<VendorFormProvider vendorId={id}>
<VendorFormPageLoading>
<DashboardCard page>
<VendorFormPageFormik
onSubmitSuccess={handleSubmitSuccess}
onCancel={handleFormCancel}
/>
</DashboardCard>
</VendorFormPageLoading>
</VendorFormProvider>
);
}
const VendorFormPageFormik = styled(VendorFormFormik)`
.page-form {
&__floating-actions {
margin-left: -40px;
margin-right: -40px;
}
}
`;
const VendorDashboardInsider = styled(DashboardInsider)`
padding-bottom: 64px;
`;

View File

@@ -0,0 +1,124 @@
// @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 { FormattedMessage as T } from '@/components';
import { CLASSES } from '@/constants/classes';
import { inputIntent } from '@/utils';
import { useAutofocus } from '@/hooks';
import {
Hint,
FieldRequiredHint,
SalutationList,
DisplayNameList,
} from '@/components';
/**
* Vendor form primary section.
*/
function VendorFormPrimarySection() {
const firstNameFieldRef = useAutofocus();
return (
<div className={'customer-form__primary-section-content'}>
{/**----------- Vendor name -----------*/}
<FormGroup
className={classNames('form-group--contact_name')}
label={<T id={'contact_name'} />}
inline={true}
>
<ControlGroup>
<FastField name={'salutation'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<SalutationList
onItemSelect={(salutation) => {
form.setFieldValue('salutation', salutation.label);
}}
selectedItem={value}
popoverProps={{ minimal: true }}
className={classNames(
CLASSES.FORM_GROUP_LIST_SELECT,
CLASSES.FILL,
'input-group--salutation-list',
'select-list--fill-button',
)}
/>
)}
</FastField>
<FastField name={'first_name'}>
{({ field, meta: { error, touched } }) => (
<InputGroup
placeholder={intl.get('first_name')}
intent={inputIntent({ error, touched })}
className={classNames('input-group--first-name')}
inputRef={(ref) => (firstNameFieldRef.current = ref)}
{...field}
/>
)}
</FastField>
<FastField name={'last_name'}>
{({ field, meta: { error, touched } }) => (
<InputGroup
placeholder={intl.get('last_name')}
intent={inputIntent({ error, touched })}
className={classNames('input-group--last-name')}
{...field}
/>
)}
</FastField>
</ControlGroup>
</FormGroup>
{/*----------- Company Name -----------*/}
<FastField name={'company_name'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
className={classNames('form-group--company_name')}
label={<T id={'company_name'} />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'company_name'} />}
inline={true}
>
<InputGroup {...field} />
</FormGroup>
)}
</FastField>
{/*----------- Display Name -----------*/}
<Field name={'display_name'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
helperText={<ErrorMessage name={'display_name'} />}
intent={inputIntent({ error, touched })}
label={
<>
<T id={'display_name'} />
<FieldRequiredHint />
<Hint />
</>
}
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, CLASSES.FILL)}
inline={true}
>
<DisplayNameList
firstName={form.values.first_name}
lastName={form.values.last_name}
company={form.values.company_name}
salutation={form.values.salutation}
onItemSelect={(displayName) => {
form.setFieldValue('display_name', displayName.label);
}}
selectedItem={value}
popoverProps={{ minimal: true }}
/>
</FormGroup>
)}
</Field>
</div>
);
}
export default VendorFormPrimarySection;

View File

@@ -0,0 +1,88 @@
// @ts-nocheck
import React, { useState, createContext } from 'react';
import { omit } from 'lodash';
import { useLocation } from 'react-router-dom';
import {
useVendor,
useContact,
useCurrencies,
useCreateVendor,
useEditVendor,
useBranches,
} from '@/hooks/query';
import { Features } from '@/constants';
import { useFeatureCan } from '@/hooks/state';
const VendorFormContext = createContext();
/**
* Vendor form provider.
*/
function VendorFormProvider({ query, vendorId, ...props }) {
const { state } = useLocation();
const contactId = state?.action;
// Features guard.
const { featureCan } = useFeatureCan();
const isBranchFeatureCan = featureCan(Features.Branches);
// Handle fetch Currencies data table
const { data: currencies, isLoading: isCurrenciesLoading } = useCurrencies();
// Handle fetch vendor details.
const { data: vendor, isLoading: isVendorLoading } = useVendor(vendorId, {
enabled: !!vendorId,
});
// Handle fetch contact duplicate details.
const { data: contactDuplicate, isLoading: isContactLoading } = useContact(
contactId,
{ enabled: !!contactId },
);
// Fetches the branches list.
const {
data: branches,
isLoading: isBranchesLoading,
isSuccess: isBranchesSuccess,
} = useBranches(query, { enabled: isBranchFeatureCan });
// Create and edit vendor mutations.
const { mutateAsync: createVendorMutate } = useCreateVendor();
const { mutateAsync: editVendorMutate } = useEditVendor();
// Form submit payload.
const [submitPayload, setSubmitPayload] = useState({});
// determines whether the form new or duplicate mode.
const isNewMode = contactId || !vendorId;
const isFormLoading =
isVendorLoading ||
isContactLoading ||
isCurrenciesLoading ||
isBranchesLoading;
const provider = {
vendorId,
currencies,
vendor,
branches,
contactDuplicate: { ...omit(contactDuplicate, ['opening_balance_at']) },
submitPayload,
isNewMode,
isFormLoading,
isBranchesSuccess,
createVendorMutate,
editVendorMutate,
setSubmitPayload,
};
return <VendorFormContext.Provider value={provider} {...props} />;
}
const useVendorFormContext = () => React.useContext(VendorFormContext);
export { VendorFormProvider, useVendorFormContext };

View File

@@ -0,0 +1,50 @@
// @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 VendorAttahmentTab from './VendorAttahmentTab';
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 />}
/>
<Tab
id={'attachement'}
title={intl.get('attachement')}
panel={<VendorAttahmentTab />}
/>
</Tabs>
</div>
);
}

View File

@@ -0,0 +1,58 @@
// @ts-nocheck
import React from 'react';
import moment from 'moment';
import { useFormikContext } from 'formik';
import { first } from 'lodash';
import { useVendorFormContext } from './VendorFormProvider';
export const defaultInitialValues = {
salutation: '',
first_name: '',
last_name: '',
company_name: '',
display_name: '',
email: '',
work_phone: '',
personal_phone: '',
website: '',
note: '',
active: true,
billing_address_country: '',
billing_address_1: '',
billing_address_2: '',
billing_address_city: '',
billing_address_state: '',
billing_address_postcode: '',
billing_address_phone: '',
shipping_address_country: '',
shipping_address_1: '',
shipping_address_2: '',
shipping_address_city: '',
shipping_address_state: '',
shipping_address_postcode: '',
shipping_address_phone: '',
opening_balance: '',
currency_code: '',
opening_balance_at: moment(new Date()).format('YYYY-MM-DD'),
opening_balance_branch_id: '',
};
export const useSetPrimaryBranchToForm = () => {
const { setFieldValue } = useFormikContext();
const { branches, isBranchesSuccess } = useVendorFormContext();
React.useEffect(() => {
if (isBranchesSuccess) {
const primaryBranch = branches.find((b) => b.primary) || first(branches);
if (primaryBranch) {
setFieldValue('opening_balance_branch_id', primaryBranch.id);
}
}
}, [isBranchesSuccess, setFieldValue, branches]);
};

View File

@@ -0,0 +1,18 @@
// @ts-nocheck
import React from 'react';
const VendorDeleteAlert = React.lazy(
() => import('@/containers/Alerts/Vendors/VendorDeleteAlert'),
);
const VendorActivateAlert = React.lazy(
() => import('@/containers/Alerts/Vendors/VendorActivateAlert'),
);
const VendorInactivateAlert = React.lazy(
() => import('@/containers/Alerts/Vendors/VendorInactivateAlert'),
);
export default [
{ name: 'vendor-delete', component: VendorDeleteAlert },
{ name: 'vendor-activate', component: VendorActivateAlert },
{ name: 'vendor-inactivate', component: VendorInactivateAlert },
];

View File

@@ -0,0 +1,173 @@
// @ts-nocheck
import React from 'react';
import {
NavbarGroup,
NavbarDivider,
Button,
Classes,
Intent,
Switch,
Alignment,
} from '@blueprintjs/core';
import {
If,
Can,
Icon,
FormattedMessage as T,
DashboardActionViewsList,
DashboardFilterButton,
DashboardActionsBar,
DashboardRowsHeightButton,
AdvancedFilterPopover,
} from '@/components';
import { useRefreshVendors } from '@/hooks/query/vendors';
import { VendorAction, AbilitySubject } from '@/constants/abilityOption';
import { useVendorsListContext } from './VendorsListProvider';
import { useHistory } from 'react-router-dom';
import withVendors from './withVendors';
import withVendorsActions from './withVendorsActions';
import withSettings from '@/containers/Settings/withSettings';
import withSettingsActions from '@/containers/Settings/withSettingsActions';
import { compose } from '@/utils';
/**
* Vendors actions bar.
*/
function VendorActionsBar({
// #withVendors
vendorsFilterConditions,
// #withVendorActions
setVendorsTableState,
vendorsInactiveMode,
// #withSettings
vendorsTableSize,
// #withSettingsActions
addSetting,
}) {
const history = useHistory();
// Vendors list context.
const { vendorsViews, fields } = useVendorsListContext();
// Handles new vendor button click.
const onClickNewVendor = () => {
history.push('/vendors/new');
};
// Vendors refresh action.
const { refresh } = useRefreshVendors();
// Handle the active tab change.
const handleTabChange = (viewSlug) => {
setVendorsTableState({ viewSlug });
};
// Handle inactive switch changing.
const handleInactiveSwitchChange = (event) => {
const checked = event.target.checked;
setVendorsTableState({ inactiveMode: checked });
};
// Handle click a refresh sale estimates
const handleRefreshBtnClick = () => {
refresh();
};
const handleTableRowSizeChange = (size) => {
addSetting('vendors', 'tableSize', size);
};
return (
<DashboardActionsBar>
<NavbarGroup>
<DashboardActionViewsList
resourceName={'vendors'}
views={vendorsViews}
onChange={handleTabChange}
/>
<NavbarDivider />
<Can I={VendorActionsBar.Create} a={AbilitySubject.Vendor}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'plus'} />}
text={<T id={'new_vendor'} />}
onClick={onClickNewVendor}
/>
<NavbarDivider />
</Can>
<AdvancedFilterPopover
advancedFilterProps={{
conditions: vendorsFilterConditions,
defaultFieldKey: 'display_name',
fields: fields,
onFilterChange: (filterConditions) => {
setVendorsTableState({ filterRoles: filterConditions });
},
}}
>
<DashboardFilterButton
conditionsCount={vendorsFilterConditions.length}
/>
</AdvancedFilterPopover>
<If condition={false}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="trash-16" iconSize={16} />}
text={<T id={'delete'} />}
intent={Intent.DANGER}
/>
</If>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-import-16" iconSize={16} />}
text={<T id={'import'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
/>
<NavbarDivider />
<DashboardRowsHeightButton
initialValue={vendorsTableSize}
onChange={handleTableRowSizeChange}
/>
<NavbarDivider />
<Can I={VendorAction.Edit} a={AbilitySubject.Vendor}>
<Switch
labelElement={<T id={'inactive'} />}
defaultChecked={vendorsInactiveMode}
onChange={handleInactiveSwitchChange}
/>
</Can>
</NavbarGroup>
<NavbarGroup align={Alignment.RIGHT}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="refresh-16" iconSize={14} />}
onClick={handleRefreshBtnClick}
/>
</NavbarGroup>
</DashboardActionsBar>
);
}
export default compose(
withVendorsActions,
withSettingsActions,
withVendors(({ vendorsTableState }) => ({
vendorsInactiveMode: vendorsTableState.inactiveMode,
vendorsFilterConditions: vendorsTableState.filterRoles,
})),
withSettings(({ vendorsSettings }) => ({
vendorsTableSize: vendorsSettings?.tableSize,
})),
)(VendorActionsBar);

View File

@@ -0,0 +1,51 @@
// @ts-nocheck
import React from 'react';
import { Alignment, Navbar, NavbarGroup } from '@blueprintjs/core';
import { useVendorsListContext } from './VendorsListProvider';
import { DashboardViewsTabs } from '@/components';
import withVendorsActions from './withVendorsActions';
import withVendors from './withVendors';
import { transfromViewsToTabs, compose } from '@/utils';
/**
* Vendors views tabs.
*/
function VendorViewsTabs({
// #withVendorsActions
setVendorsTableState,
// #withVendors
vendorsCurrentView,
}) {
const { vendorsViews } = useVendorsListContext();
// Transformes the resource views to tabs.
const tabs = transfromViewsToTabs(vendorsViews);
// Handle dashboard tabs change.
const handleTabsChange = (viewSlug) => {
setVendorsTableState({ viewSlug });
};
return (
<Navbar className="navbar--dashboard-views">
<NavbarGroup align={Alignment.LEFT}>
<DashboardViewsTabs
currentViewSlug={vendorsCurrentView}
resourceName={'vendors'}
tabs={tabs}
onChange={handleTabsChange}
/>
</NavbarGroup>
</Navbar>
);
}
export default compose(
withVendorsActions,
withVendors(({ vendorsTableState }) => ({
vendorsCurrentView: vendorsTableState.viewSlug,
})),
)(VendorViewsTabs);

View File

@@ -0,0 +1,41 @@
// @ts-nocheck
import React from 'react';
import { Button, Intent } from '@blueprintjs/core';
import { useHistory } from 'react-router-dom';
import { EmptyStatus } from '@/components';
import { Can, FormattedMessage as T } from '@/components';
import { VendorAction, AbilitySubject } from '@/constants/abilityOption';
export default function VendorsEmptyStatus() {
const history = useHistory();
return (
<EmptyStatus
title={<T id={'create_and_manage_your_organization_s_vendors'} />}
description={
<p>
<T id={'here_a_list_of_your_organization_products_and_services'} />
</p>
}
action={
<>
<Can I={VendorAction.Create} a={AbilitySubject.Vendor}>
<Button
intent={Intent.PRIMARY}
large={true}
onClick={() => {
history.push('/vendors/new');
}}
>
<T id={'new_vendor'} />
</Button>
<Button intent={Intent.NONE} large={true}>
<T id={'learn_more'} />
</Button>
</Can>
</>
}
/>
);
}

View File

@@ -0,0 +1,58 @@
// @ts-nocheck
import React, { useEffect } from 'react';
import '@/style/pages/Vendors/List.scss';
import { DashboardPageContent } from '@/components';
import { VendorsListProvider } from './VendorsListProvider';
import VendorActionsBar from './VendorActionsBar';
import VendorViewsTabs from './VendorViewsTabs';
import VendorsTable from './VendorsTable';
import withVendors from './withVendors';
import withVendorsActions from './withVendorsActions';
import { compose } from '@/utils';
/**
* Vendors list page.
*/
function VendorsList({
// #withVendors
vendorsTableState,
vendorsTableStateChanged,
// #withVendorsActions
resetVendorsTableState,
}) {
// Resets the vendors table state once the page unmount.
useEffect(
() => () => {
resetVendorsTableState();
},
[resetVendorsTableState],
);
return (
<VendorsListProvider
tableState={vendorsTableState}
tableStateChanged={vendorsTableStateChanged}
>
<VendorActionsBar />
<DashboardPageContent>
<VendorViewsTabs />
<VendorsTable />
</DashboardPageContent>
</VendorsListProvider>
);
}
export default compose(
withVendors(({ vendorsTableState, vendorsTableStateChanged }) => ({
vendorsTableState,
vendorsTableStateChanged,
})),
withVendorsActions,
)(VendorsList);

View File

@@ -0,0 +1,67 @@
// @ts-nocheck
import React, { createContext } from 'react';
import { isEmpty } from 'lodash';
import { DashboardInsider } from '@/components';
import { useResourceMeta, useResourceViews, useVendors } from '@/hooks/query';
import { getFieldsFromResourceMeta } from '@/utils';
import { transformVendorsStateToQuery } from './utils';
const VendorsListContext = createContext();
function VendorsListProvider({ tableState, tableStateChanged, ...props }) {
// Transformes the vendors table state to fetch query.
const tableQuery = transformVendorsStateToQuery(tableState);
// Fetch vendors list with pagination meta.
const {
data: { vendors, pagination, filterMeta },
isLoading: isVendorsLoading,
isFetching: isVendorsFetching,
} = useVendors(tableQuery, { keepPreviousData: true });
// Fetch customers resource views and fields.
const { data: vendorsViews, isLoading: isVendorsViewsLoading } =
useResourceViews('vendors');
// Fetch the customers resource fields.
const {
data: resourceMeta,
isLoading: isResourceMetaLoading,
isFetching: isResourceMetaFetching,
} = useResourceMeta('customers');
// Detarmines the datatable empty status.
const isEmptyStatus =
isEmpty(vendors) && !isVendorsLoading && !tableStateChanged;
const provider = {
vendors,
pagination,
vendorsViews,
fields: getFieldsFromResourceMeta(resourceMeta.fields),
resourceMeta,
isResourceMetaLoading,
isResourceMetaFetching,
isVendorsViewsLoading,
isVendorsLoading,
isVendorsFetching,
isEmptyStatus,
};
return (
<DashboardInsider
loading={isVendorsViewsLoading || isResourceMetaLoading}
name={'vendors-list'}
>
<VendorsListContext.Provider value={provider} {...props} />
</DashboardInsider>
);
}
const useVendorsListContext = () => React.useContext(VendorsListContext);
export { VendorsListProvider, useVendorsListContext };

View File

@@ -0,0 +1,173 @@
// @ts-nocheck
import React from 'react';
import { useHistory } from 'react-router';
import { TABLES } from '@/constants/tables';
import {
DataTable,
TableSkeletonRows,
TableSkeletonHeader,
DashboardContentTable,
} from '@/components';
import VendorsEmptyStatus from './VendorsEmptyStatus';
import { useVendorsListContext } from './VendorsListProvider';
import { useMemorizedColumnsWidths } from '@/hooks';
import { ActionsMenu, useVendorsTableColumns } from './components';
import withVendors from './withVendors';
import withVendorsActions from './withVendorsActions';
import withAlertsActions from '@/containers/Alert/withAlertActions';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
import withSettings from '@/containers/Settings/withSettings';
import { compose } from '@/utils';
/**
* Vendors table.
*/
function VendorsTable({
// #withVendorsActions
setVendorsTableState,
// #withVendors
vendorsTableState,
// #withAlertsActions
openAlert,
// #withDrawerActions
openDrawer,
// #withDialogActions
openDialog,
// #withSettings
vendorsTableSize,
}) {
// Vendors list context.
const {
vendors,
pagination,
isVendorsFetching,
isVendorsLoading,
isEmptyStatus,
} = useVendorsListContext();
// Vendors table columns.
const columns = useVendorsTableColumns();
// History context.
const history = useHistory();
// Handle edit vendor data table
const handleEditVendor = (vendor) => {
history.push(`/vendors/${vendor.id}/edit`);
};
// Handle cancel/confirm inactive.
const handleInactiveVendor = ({ id, contact_service }) => {
openAlert('vendor-inactivate', {
vendorId: id,
service: contact_service,
});
};
// Handle cancel/confirm activate.
const handleActivateVendor = ({ id, contact_service }) => {
openAlert('vendor-activate', { vendorId: id, service: contact_service });
};
// Handle click delete vendor.
const handleDeleteVendor = ({ id }) => {
openAlert('vendor-delete', { contactId: id });
};
// Handle contact duplicate .
const handleContactDuplicate = ({ id }) => {
openDialog('contact-duplicate', {
contactId: id,
});
};
// Handle view detail item.
const handleViewDetailVendor = ({ id }) => {
openDrawer('vendor-detail-drawer', { vendorId: id });
};
// Handle cell click.
const handleCellClick = (cell, event) => {
openDrawer('vendor-detail-drawer', { vendorId: cell.row.original.id });
};
// Local storage memorizing columns widths.
const [initialColumnsWidths, , handleColumnResizing] =
useMemorizedColumnsWidths(TABLES.VENDORS);
// Handle fetch data once the page index, size or sort by of the table change.
const handleFetchData = React.useCallback(
({ pageSize, pageIndex, sortBy }) => {
setVendorsTableState({
pageIndex,
pageSize,
sortBy,
});
},
[setVendorsTableState],
);
// Display empty status instead of the table.
if (isEmptyStatus) {
return <VendorsEmptyStatus />;
}
return (
<DashboardContentTable>
<DataTable
noInitialFetch={true}
columns={columns}
data={vendors}
loading={isVendorsLoading}
headerLoading={isVendorsLoading}
progressBarLoading={isVendorsFetching}
onFetchData={handleFetchData}
selectionColumn={true}
expandable={false}
sticky={true}
pagination={true}
manualSortBy={true}
pagesCount={pagination.pagesCount}
autoResetSortBy={false}
autoResetPage={false}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
ContextMenu={ActionsMenu}
onCellClick={handleCellClick}
initialColumnsWidths={initialColumnsWidths}
onColumnResizing={handleColumnResizing}
size={vendorsTableSize}
payload={{
onEdit: handleEditVendor,
onDelete: handleDeleteVendor,
onDuplicate: handleContactDuplicate,
onInactivate: handleInactiveVendor,
onActivate: handleActivateVendor,
onViewDetails: handleViewDetailVendor,
}}
/>
</DashboardContentTable>
);
}
export default compose(
withVendorsActions,
withAlertsActions,
withDialogActions,
withDrawerActions,
withVendors(({ vendorsTableState }) => ({ vendorsTableState })),
withSettings(({ vendorsSettings }) => ({
vendorsTableSize: vendorsSettings?.tableSize,
})),
)(VendorsTable);

View File

@@ -0,0 +1,197 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import {
Button,
Popover,
Menu,
MenuItem,
MenuDivider,
Position,
Tooltip,
Intent,
Classes,
} from '@blueprintjs/core';
import { Can, Icon, Money, If, AvaterCell } from '@/components';
import { VendorAction, AbilitySubject } from '@/constants/abilityOption';
import { safeCallback, firstLettersArgs } from '@/utils';
/**
* Actions menu.
*/
export function ActionsMenu({
row: { original },
payload: {
onEdit,
onDelete,
onDuplicate,
onInactivate,
onActivate,
onViewDetails,
},
}) {
return (
<Menu>
<MenuItem
icon={<Icon icon="reader-18" />}
text={intl.get('view_details')}
onClick={safeCallback(onViewDetails, original)}
/>
<Can I={VendorAction.Edit} a={AbilitySubject.Vendor}>
<MenuDivider />
<MenuItem
icon={<Icon icon="pen-18" />}
text={intl.get('edit_vendor')}
onClick={safeCallback(onEdit, original)}
/>
</Can>
<Can I={VendorAction.Create} a={AbilitySubject.Customer}>
<MenuItem
icon={<Icon icon="duplicate-16" />}
text={intl.get('duplicate')}
onClick={safeCallback(onDuplicate, original)}
/>
</Can>
<Can I={VendorAction.Edit} a={AbilitySubject.Vendor}>
<If condition={original.active}>
<MenuItem
text={intl.get('inactivate_item')}
icon={<Icon icon="pause-16" iconSize={16} />}
onClick={safeCallback(onInactivate, original)}
/>
</If>
<If condition={!original.active}>
<MenuItem
text={intl.get('activate_item')}
icon={<Icon icon="play-16" iconSize={16} />}
onClick={safeCallback(onActivate, original)}
/>
</If>
</Can>
<Can I={VendorAction.Delete} a={AbilitySubject.Vendor}>
<MenuDivider />
<MenuItem
icon={<Icon icon="trash-16" iconSize={16} />}
text={intl.get('delete_vendor')}
intent={Intent.DANGER}
onClick={safeCallback(onDelete, original)}
/>
</Can>
</Menu>
);
}
/**
* Actions cell.
*/
export function ActionsCell(props) {
return (
<Popover
content={<ActionsMenu {...props} />}
position={Position.RIGHT_BOTTOM}
>
<Button icon={<Icon icon="more-h-16" iconSize={16} />} />
</Popover>
);
}
/**
* Avatar table accessor.
*/
export function AvatarAccessor(row) {
return <span className="avatar">{firstLettersArgs(row.display_name)}</span>;
}
/**
* Phone number accessor.
*/
export function PhoneNumberAccessor(row) {
return <div className={'work_phone'}>{row.work_phone}</div>;
}
/**
* Balance accessor.
*/
export function BalanceAccessor({ closing_balance, currency_code }) {
return <Money amount={closing_balance} currency={currency_code} />;
}
/**
* Note column accessor.
*/
export function NoteAccessor(row) {
return (
<If condition={row.note}>
<Tooltip
className={Classes.TOOLTIP_INDICATOR}
content={row.note}
position={Position.LEFT_TOP}
hoverOpenDelay={50}
>
<Icon icon={'file-alt'} iconSize={16} />
</Tooltip>
</If>
);
}
/**
* Retrieve the vendors table columns.
*/
export function useVendorsTableColumns() {
return React.useMemo(
() => [
{
id: 'avatar',
Header: '',
Cell: AvaterCell,
className: 'avatar',
width: 45,
disableResizing: true,
disableSortBy: true,
clickable: true,
},
{
id: 'display_name',
Header: intl.get('display_name'),
accessor: 'display_name',
className: 'display_name',
width: 150,
clickable: true,
},
{
id: 'company_name',
Header: intl.get('company_name'),
accessor: 'company_name',
className: 'company_name',
width: 150,
clickable: true,
},
{
id: 'work_phone',
Header: intl.get('phone_number'),
accessor: PhoneNumberAccessor,
className: 'work_phone',
width: 100,
clickable: true,
},
{
id: 'note',
Header: intl.get('note'),
accessor: NoteAccessor,
disableSortBy: true,
width: 85,
clickable: true,
},
{
id: 'balance',
Header: intl.get('receivable_balance'),
accessor: BalanceAccessor,
align: 'right',
width: 100,
clickable: true,
},
],
[],
);
}

View File

@@ -0,0 +1,24 @@
// @ts-nocheck
import { useCallback } from 'react';
import intl from 'react-intl-universal';
import { Intent } from '@blueprintjs/core';
import { AppToaster } from '@/components';
import { transformTableStateToQuery } from '@/utils';
// Transform API errors in toasts messages.
export const transformErrors = (errors) => {
if (errors.some((e) => e.type === 'VENDOR.HAS.BILLS')) {
AppToaster.show({
message: intl.get('vendor_has_bills'),
intent: Intent.DANGER,
});
}
};
export const transformVendorsStateToQuery = (tableState) => {
return {
...transformTableStateToQuery(tableState),
inactive_mode: tableState.inactiveMode || false,
};
}

View File

@@ -0,0 +1,20 @@
// @ts-nocheck
import { connect } from 'react-redux';
import {
getVendorsTableStateFactory,
vendorsTableStateChangedFactory,
} from '@/store/vendors/vendors.selectors';
export default (mapState) => {
const getVendorsTableState = getVendorsTableStateFactory();
const vendorsTableStateChanged = vendorsTableStateChangedFactory();
const mapStateToProps = (state, props) => {
const mapped = {
vendorsTableState: getVendorsTableState(state, props),
vendorsTableStateChanged: vendorsTableStateChanged(state, props),
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -0,0 +1,13 @@
// @ts-nocheck
import { connect } from 'react-redux';
import {
setVendorsTableState,
resetVendorsTableState,
} from '@/store/vendors/vendors.actions';
const mapDispatchToProps = (dispatch) => ({
setVendorsTableState: (queries) => dispatch(setVendorsTableState(queries)),
resetVendorsTableState: () => dispatch(resetVendorsTableState()),
});
export default connect(null, mapDispatchToProps);

View File

@@ -0,0 +1,52 @@
// @ts-nocheck
import intl from 'react-intl-universal';
import { RESOURCES_TYPES } from '@/constants/resourcesTypes';
import { AbilitySubject, VendorAction } from '@/constants/abilityOption';
import withDrawerActions from '../Drawer/withDrawerActions';
/**
* Vendor univesal search item select action.
*/
function VendorUniversalSearchSelectComponent({
resourceType,
resourceId,
onAction,
// #withDrawerActions
openDrawer,
}) {
if (resourceType === RESOURCES_TYPES.VENDOR) {
openDrawer('vendor-detail-drawer', { vendorId: resourceId });
onAction && onAction();
}
return null;
}
const VendorUniversalSearchSelectAction = withDrawerActions(
VendorUniversalSearchSelectComponent,
);
/**
* Transformes vendor resource item to search.
*/
const vendorToSearch = (contact) => ({
id: contact.id,
text: contact.display_name,
label: contact.balance > 0 ? contact.formatted_balance + '' : '',
reference: contact,
});
/**
* Binds universal search invoice configure.
*/
export const universalSearchVendorBind = () => ({
resourceType: RESOURCES_TYPES.VENDOR,
optionItemLabel: intl.get('vendors'),
selectItemAction: VendorUniversalSearchSelectAction,
itemSelect: vendorToSearch,
permission: {
ability: VendorAction.View,
subject: AbilitySubject.Vendor,
},
});

View File

@@ -0,0 +1,24 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import { Intent } from '@blueprintjs/core';
import { AppToaster } from '@/components';
export const transformErrors = (errors) => {
if (errors.find((error) => error.type === 'VENDOR.HAS.ASSOCIATED.BILLS')) {
AppToaster.show({
message: intl.get(
'cannot_delete_vendor_that_has_associated_purchase_bills',
),
intent: Intent.DANGER,
});
}
if (errors.find((error) => error.type === 'VENDOR_HAS_TRANSACTIONS')) {
AppToaster.show({
message: intl.get(
'this_vendor_cannot_be_deleted_as_it_is_associated_with_transactions',
),
intent: Intent.DANGER,
});
}
};

View File

@@ -0,0 +1,11 @@
// @ts-nocheck
import { connect } from 'react-redux';
import { getVendorByIdFactory } from '@/store/vendors/vendors.selectors';
export default () => {
const getVendorById = getVendorByIdFactory();
const mapStateToProps = (state, props) => ({
vendor: getVendorById(state, props),
});
return connect(mapStateToProps);
};