1
0

Merge branch 'develop' into feat/financial-audit-trail

This commit is contained in:
Ahmed Bouhuolia
2026-05-17 19:55:15 +02:00
committed by GitHub
603 changed files with 100554 additions and 15680 deletions
+2 -7
View File
@@ -4,7 +4,7 @@ FROM node:18.16.0-alpine AS builder
WORKDIR /app
# Install pnpm
RUN npm install -g pnpm@8.10.2
RUN npm install -g pnpm@9.0.5
# Install build dependencies
RUN apk add --no-cache python3 build-base chromium
@@ -15,18 +15,13 @@ ENV PYTHON=/usr/bin/python3
# Copy package files for dependency installation
COPY --chown=node:node package.json pnpm-lock.yaml pnpm-workspace.yaml lerna.json ./
COPY --chown=node:node packages/webapp/package.json ./packages/webapp/
COPY --chown=node:node shared/bigcapital-utils/package.json ./shared/bigcapital-utils/
COPY --chown=node:node shared/pdf-templates/package.json ./shared/pdf-templates/
COPY --chown=node:node shared/email-components/package.json ./shared/email-components/
COPY --chown=node:node shared ./shared
# Install all dependencies (including devDependencies for build)
RUN pnpm install
# Copy source code for webapp and dependencies
COPY --chown=node:node ./packages/webapp ./packages/webapp
COPY --chown=node:node ./shared/bigcapital-utils ./shared/bigcapital-utils
COPY --chown=node:node ./shared/pdf-templates ./shared/pdf-templates
COPY --chown=node:node ./shared/email-components ./shared/email-components
# Build webapp package
RUN pnpm run build:webapp
+3
View File
@@ -5,6 +5,7 @@
"dependencies": {
"@bigcapital/email-components": "workspace:*",
"@bigcapital/pdf-templates": "workspace:*",
"@bigcapital/sdk-ts": "workspace:*",
"@bigcapital/utils": "workspace:*",
"@blueprintjs-formik/core": "^0.3.7",
"@blueprintjs-formik/datetime": "^0.4.0",
@@ -137,6 +138,8 @@
"build": "vite build",
"preview": "cross-env PORT=4173 vite preview",
"typecheck": "tsc --noEmit",
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,md}\"",
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,md}\"",
"test": "node scripts/test.js",
"storybook": "start-storybook -p 6006"
},
@@ -19,7 +19,7 @@ export function CurrencySelectList({
name={name}
items={items}
textAccessor={'currency_code'}
valueAccessor={'id'}
valueAccessor={'currency_code'}
placeholder={placeholder}
popoverProps={{ minimal: true, usePortal: true, inline: false }}
{...props}
@@ -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,6 +16,47 @@ export interface DisplayNameListProps
'items' | 'valueAccessor' | 'textAccessor' | 'labelAccessor'
> {}
function useDisplayNameFormatOptions(
salutation?: string,
firstName?: string,
lastName?: string,
companyName?: string,
): DisplayNameListItem[] {
return useMemo(() => {
const formats: DisplayNameFormat[] = [
{
format: '{1} {2} {3}',
values: [salutation, firstName, lastName],
required: [1],
},
{ format: '{1} {2}', values: [firstName, lastName], required: [] },
{ format: '{1}, {2}', values: [firstName, lastName], required: [1, 2] },
{ format: '{1}', values: [companyName], required: [1] },
];
return formats
.filter(
(format) =>
!format.values.some((value, index) => {
return !value && format.required.indexOf(index + 1) !== -1;
}),
)
.map((formatOption) => {
const { format, values } = formatOption;
let label = format;
values.forEach((value, index) => {
const replaceWith = value || '';
label = label.replace(`{${index + 1}}`, replaceWith).trim();
});
return {
label: label.replace(/\s+/g, ' ').replace(/\s+,/g, ',').trim(),
};
})
.filter(({ label }) => Boolean(label));
}, [salutation, firstName, lastName, companyName]);
}
export function DisplayNameList({ ...restProps }: DisplayNameListProps) {
const {
values: {
@@ -21,40 +67,11 @@ export function DisplayNameList({ ...restProps }: DisplayNameListProps) {
},
} = useFormikContext<any>();
const formats = useMemo(
() => [
{
format: '{1} {2} {3}',
values: [salutation, firstName, lastName],
required: [1],
},
{ format: '{1} {2}', values: [firstName, lastName], required: [] },
{ format: '{1}, {2}', values: [firstName, lastName], required: [1, 2] },
{ format: '{1}', values: [companyName], required: [1] },
],
[firstName, lastName, companyName, salutation],
);
const formatOptions: DisplayNameListItem[] = useMemo(
() =>
formats
.filter(
(format) =>
!format.values.some((value, index) => {
return !value && format.required.indexOf(index + 1) !== -1;
}),
)
.map((formatOption) => {
const { format, values } = formatOption;
let label = format;
values.forEach((value, index) => {
const replaceWith = value || '';
label = label.replace(`{${index + 1}}`, replaceWith).trim();
});
return { label: label.replace(/\s+/g, ' ') };
}),
[formats],
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}
@@ -10,12 +10,17 @@ const TextStatusRoot = styled.span`
${(props) =>
props.intent === 'warning' &&
`
color: #ec5b0a;`}
color: #c87619;`}
${(props) =>
props.intent === 'danger' &&
`
color: #f17377;`}
${(props) =>
props.intent === 'success' &&
`
color: #2ba01d;`}
color: #238551;`}
${(props) =>
props.intent === 'none' &&
@@ -1,7 +1,5 @@
// @ts-nocheck
import React from 'react';
import React, { KeyboardEvent, ReactNode } from 'react';
import intl from 'react-intl-universal';
import classNames from 'classnames';
import { isUndefined } from 'lodash';
import {
Overlay,
@@ -10,11 +8,14 @@ import {
MenuItem,
Spinner,
Intent,
OverlayProps,
Button,
} from '@blueprintjs/core';
import { QueryList } from '@blueprintjs/select';
import { CLASSES } from '@/constants/classes';
import { Icon, If, ListSelect, FormattedMessage as T } from '@/components';
import { QueryList, ItemRenderer } from '@blueprintjs/select';
import { x } from '@xstyled/emotion';
import { css } from '@emotion/css';
import { Icon, If, FormattedMessage as T } from '@/components';
import { Select } from '@blueprintjs-formik/select';
import {
UniversalSearchProvider,
useUniversalSearchContext,
@@ -22,59 +23,297 @@ import {
import { filterItemsByResourceType } from './utils';
import { RESOURCES_TYPES } from '@/constants/resourcesTypes';
// Resource type from RESOURCES_TYPES constant
type ResourceType = string;
// Search type option item
interface SearchTypeOption {
key: ResourceType;
label: string;
}
// Universal search item
interface UniversalSearchItem {
id: number | string;
_type: ResourceType;
text: string;
subText?: string;
label?: string;
[key: string]: any;
}
// CSS styles for complex selectors
const overlayStyles = css`
.bp4-overlay-appear,
.bp4-overlay-enter {
filter: blur(20px);
opacity: 0.2;
}
.bp4-overlay-appear-active,
.bp4-overlay-enter-active {
filter: blur(0);
opacity: 1;
transition:
filter 0.2s cubic-bezier(0.4, 1, 0.75, 0.9),
opacity 0.2s cubic-bezier(0.4, 1, 0.75, 0.9);
}
.bp4-overlay-exit {
filter: blur(0);
opacity: 1;
}
.bp4-overlay-exit-active {
filter: blur(20px);
opacity: 0.2;
transition:
filter 0.2s cubic-bezier(0.4, 1, 0.75, 0.9),
opacity 0.2s cubic-bezier(0.4, 1, 0.75, 0.9);
}
`;
const containerStyles = css`
position: fixed;
filter: blur(0);
opacity: 1;
background-color: var(--color-universal-search-background);
border-radius: 3px;
box-shadow:
0 0 0 1px rgba(16, 22, 26, 0.1),
0 4px 8px rgba(16, 22, 26, 0.2),
0 18px 46px 6px rgba(16, 22, 26, 0.2);
left: calc(50% - 250px);
top: 20vh;
width: 500px;
z-index: 20;
.bp4-input-group {
.bp4-icon {
margin: 16px;
color: var(--color-universal-search-icon);
svg {
stroke: currentColor;
fill: none;
fill-rule: evenodd;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2;
--text-opacity: 1;
}
}
}
.bp4-input-group .bp4-input {
border: 0;
box-shadow: 0 0 0 0;
height: 50px;
line-height: 50px;
font-size: 20px;
}
.bp4-input-group.bp4-large .bp4-input:not(:first-child) {
padding-left: 50px !important;
}
.bp4-input-group.bp4-large .bp4-input:not(:last-child) {
padding-right: 130px !important;
}
.bp4-menu {
border-top: 1px solid var(--color-universal-search-menu-border);
max-height: calc(60vh - 20px);
overflow: auto;
.bp4-menu-item {
.bp4-text-muted {
font-size: 12px;
.bp4-icon {
color: var(--bp4-gray-600);
}
}
&.bp4-intent-primary {
&.bp4-active {
background-color: var(--bp4-blue-100);
color: var(--bp4-dark-gray-800);
.bp4-menu-item-label {
color: var(--bp4-gray-600);
}
}
}
&-label {
flex-direction: row;
text-align: right;
}
}
}
.bp4-input-action {
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
}
`;
const inputRightElementsStyles = css`
display: flex;
margin: 10px;
.bp4-spinner {
margin-right: 6px;
}
`;
const footerStyles = css`
padding: 12px 12px;
border-top: 1px solid var(--color-universal-search-footer-divider);
`;
const actionBaseStyles = css`
&:not(:first-of-type) {
margin-left: 14px;
}
.bp4-tag {
background: var(--color-universal-search-tag-background);
color: var(--color-universal-search-tag-text);
}
`;
const actionArrowsStyles = css`
&:not(:first-of-type) {
margin-left: 14px;
}
.bp4-tag {
background: var(--color-universal-search-tag-background);
color: var(--color-universal-search-tag-text);
padding: 0;
text-align: center;
line-height: 16px;
margin-left: 4px;
svg {
fill: var(--color-universal-search-tag-text);
height: 100%;
display: block;
width: 100%;
padding: 2px;
}
}
`;
// UniversalSearchInputRightElements props
interface UniversalSearchInputRightElementsProps {
/** Callback when search type changes */
onSearchTypeChange?: (option: SearchTypeOption) => void;
}
/**
* Universal search input action.
*/
function UniversalSearchInputRightElements({ onSearchTypeChange }) {
const { isLoading, searchType, defaultSearchResource, searchTypeOptions } =
function UniversalSearchInputRightElements({
onSearchTypeChange,
}: UniversalSearchInputRightElementsProps) {
const { isLoading, searchType, searchTypeOptions } =
useUniversalSearchContext();
// Find the currently selected item object.
const selectedItem = searchTypeOptions.find(
(item) => item.key === searchType,
);
// Handle search type option change.
const handleSearchTypeChange = (option) => {
onSearchTypeChange && onSearchTypeChange(option);
const handleSearchTypeChange = (option: SearchTypeOption) => {
onSearchTypeChange?.(option);
};
// Item renderer for the select dropdown.
const itemRenderer: ItemRenderer<SearchTypeOption> = (
item,
{ handleClick },
) => {
return <MenuItem text={item.label} key={item.key} onClick={handleClick} />;
};
return (
<div className={CLASSES.UNIVERSAL_SEARCH_INPUT_RIGHT_ELEMENTS}>
<x.div display="flex" m="10px" className={inputRightElementsStyles}>
<If condition={isLoading}>
<Spinner tagName="div" intent={Intent.NONE} size={18} value={null} />
<Spinner tagName="div" intent={Intent.NONE} size={18} />
</If>
<ListSelect
<Select<SearchTypeOption>
items={searchTypeOptions}
itemRenderer={itemRenderer}
onItemSelect={handleSearchTypeChange}
selectedValue={selectedItem?.key}
valueAccessor={'key'}
labelAccessor={'label'}
filterable={false}
initialSelectedItem={defaultSearchResource}
selectedItem={searchType}
selectedItemProp={'key'}
textProp={'label'}
// defaultText={intl.get('type')}
popoverProps={{
minimal: true,
captureDismiss: true,
className: CLASSES.UNIVERSAL_SEARCH_TYPE_SELECT_OVERLAY,
}}
buttonProps={{
minimal: true,
className: CLASSES.UNIVERSAL_SEARCH_TYPE_SELECT_BTN,
}}
input={({ activeItem }) => (
<Button minimal={true} text={activeItem?.label} />
)}
/>
</div>
</x.div>
);
}
// QueryList renderer props
interface QueryListRendererProps {
/** Current query string */
query: string;
/** Callback when query changes */
handleQueryChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
/** Item list element */
itemList: ReactNode;
/** Class name */
className?: string;
/** Handle key down */
handleKeyDown?: (event: KeyboardEvent<HTMLDivElement>) => void;
/** Handle key up */
handleKeyUp?: (event: KeyboardEvent<HTMLDivElement>) => void;
}
// UniversalSearchQueryList props
interface UniversalSearchQueryListProps {
/** Whether the search is open */
isOpen: boolean;
/** Whether the search is loading */
isLoading: boolean;
/** Callback when search type changes */
onSearchTypeChange?: (option: SearchTypeOption) => void;
/** Current search type */
searchType: ResourceType;
/** Items to display */
items: UniversalSearchItem[];
/** Renderer for items */
itemRenderer?: ItemRenderer<UniversalSearchItem>;
/** Callback when an item is selected */
onItemSelect?: (item: UniversalSearchItem, event?: any) => void;
/** Current query string */
query: string;
/** Callback when query changes */
onQueryChange?: (query: string) => void;
}
/**
* Universal search query list.
*/
function UniversalSearchQueryList(props) {
const { isOpen, isLoading, onSearchTypeChange, searchType, ...restProps } =
props;
function UniversalSearchQueryList({
isOpen,
isLoading,
onSearchTypeChange,
...restProps
}: UniversalSearchQueryListProps) {
return (
<QueryList
{...restProps}
<QueryList<UniversalSearchItem>
{...(restProps as any)}
initialContent={null}
renderer={(listProps) => (
renderer={(listProps: QueryListRendererProps) => (
<UniversalSearchBar
isOpen={isOpen}
onSearchTypeChange={onSearchTypeChange}
@@ -100,47 +339,53 @@ function UniversalSearchQueryList(props) {
*/
function UniversalQuerySearchActions() {
return (
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_ACTIONS)}>
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_ACTION_SELECT)}>
<x.div display="flex">
<x.div className={actionBaseStyles}>
<Tag>ENTER</Tag>
<span class={'text'}>{intl.get('universal_search.enter_text')}</span>
</div>
<x.span ml="6px">{intl.get('universal_search.enter_text')}</x.span>
</x.div>
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_ACTION_CLOSE)}>
<x.div className={actionBaseStyles}>
<Tag>ESC</Tag>{' '}
<span class={'text'}>{intl.get('universal_search.close_text')}</span>
</div>
<x.span ml="6px">{intl.get('universal_search.close_text')}</x.span>
</x.div>
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_ACTION_ARROWS)}>
<x.div className={actionArrowsStyles}>
<Tag>
<Icon icon={'arrow-up-24'} iconSize={16} />
</Tag>
<Tag>
<Icon icon={'arrow-down-24'} iconSize={16} />
</Tag>
<span class="text">{intl.get('universal_seach.navigate_text')}</span>
</div>
</div>
<x.span ml="6px">{intl.get('universal_seach.navigate_text')}</x.span>
</x.div>
</x.div>
);
}
// UniversalSearchBar props
interface UniversalSearchBarProps extends QueryListRendererProps {
/** Whether the search is open */
isOpen: boolean;
/** Callback when search type changes */
onSearchTypeChange?: (option: SearchTypeOption) => void;
}
/**
* Universal search input bar with items list.
*/
function UniversalSearchBar({ isOpen, onSearchTypeChange, ...listProps }) {
function UniversalSearchBar({
isOpen,
onSearchTypeChange,
...listProps
}: UniversalSearchBarProps) {
const { handleKeyDown, handleKeyUp } = listProps;
const handlers = isOpen
? { onKeyDown: handleKeyDown, onKeyUp: handleKeyUp }
: {};
return (
<div
className={classNames(
CLASSES.UNIVERSAL_SEARCH_OMNIBAR,
listProps.className,
)}
{...handlers}
>
<x.div {...handlers}>
<InputGroup
large={true}
leftIcon={<Icon icon={'universal-search'} iconSize={20} />}
@@ -155,17 +400,44 @@ function UniversalSearchBar({ isOpen, onSearchTypeChange, ...listProps }) {
autoFocus={true}
/>
{listProps.itemList}
</div>
</x.div>
);
}
// UniversalSearch props
export interface UniversalSearchProps {
/** Default search resource type */
defaultSearchResource?: ResourceType;
/** Controlled search resource type */
searchResource?: ResourceType;
/** Overlay props */
overlayProps?: OverlayProps;
/** Whether the search overlay is open */
isOpen: boolean;
/** Whether the search is loading */
isLoading: boolean;
/** Callback when search type changes */
onSearchTypeChange?: (resource: SearchTypeOption) => void;
/** Items to display */
items: UniversalSearchItem[];
/** Available search type options */
searchTypeOptions: SearchTypeOption[];
/** Renderer for items */
itemRenderer?: ItemRenderer<UniversalSearchItem>;
/** Callback when an item is selected */
onItemSelect?: (item: UniversalSearchItem, event?: any) => void;
/** Current query string */
query: string;
/** Callback when query changes */
onQueryChange?: (query: string) => void;
}
/**
* Universal search.
*/
export function UniversalSearch({
defaultSearchResource,
searchResource,
overlayProps,
isOpen,
isLoading,
@@ -173,9 +445,9 @@ export function UniversalSearch({
items,
searchTypeOptions,
...queryListProps
}) {
}: UniversalSearchProps) {
// Search type state.
const [searchType, setSearchType] = React.useState(
const [searchType, setSearchType] = React.useState<ResourceType>(
defaultSearchResource || RESOURCES_TYPES.CUSTOMER,
);
// Handle search resource type controlled mode.
@@ -189,9 +461,9 @@ export function UniversalSearch({
}, [searchResource, defaultSearchResource]);
// Handle search type change.
const handleSearchTypeChange = (searchTypeResource) => {
const handleSearchTypeChange = (searchTypeResource: SearchTypeOption) => {
setSearchType(searchTypeResource.key);
onSearchTypeChange && onSearchTypeChange(searchTypeResource);
onSearchTypeChange?.(searchTypeResource);
};
// Filters query list items based on the given search type.
const filteredItems = filterItemsByResourceType(items, searchType);
@@ -200,7 +472,7 @@ export function UniversalSearch({
<Overlay
hasBackdrop={true}
isOpen={isOpen}
className={classNames(CLASSES.UNIVERSAL_SEARCH_OVERLAY)}
className={overlayStyles}
{...overlayProps}
>
<UniversalSearchProvider
@@ -209,7 +481,7 @@ export function UniversalSearch({
defaultSearchResource={defaultSearchResource}
searchTypeOptions={searchTypeOptions}
>
<div className={classNames(CLASSES.UNIVERSAL_SEARCH)}>
<x.div className={containerStyles}>
<UniversalSearchQueryList
isOpen={isOpen}
isLoading={isLoading}
@@ -218,10 +490,10 @@ export function UniversalSearch({
{...queryListProps}
items={filteredItems}
/>
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_FOOTER)}>
<x.div className={footerStyles}>
<UniversalQuerySearchActions />
</div>
</div>
</x.div>
</x.div>
</UniversalSearchProvider>
</Overlay>
);
@@ -1,30 +1,82 @@
// @ts-nocheck
import React, { createContext } from 'react';
import React, { createContext, ReactNode, useContext } from 'react';
const UniversalSearchContext = createContext();
// The resource type value from RESOURCES_TYPES constant
type ResourceType = string;
// Search type option item
interface SearchTypeOption {
key: ResourceType;
label: string;
}
// Context value type
interface UniversalSearchContextValue {
/** Whether the search is loading */
isLoading: boolean;
/** Current search type/resource type */
searchType: ResourceType;
/** Default search resource type */
defaultSearchResource?: ResourceType;
/** List of available search type options */
searchTypeOptions: SearchTypeOption[];
}
// Create the context with undefined as initial value
const UniversalSearchContext = createContext<
UniversalSearchContextValue | undefined
>(undefined);
// Provider props interface
interface UniversalSearchProviderProps {
/** Whether the search is loading */
isLoading: boolean;
/** Default search resource type */
defaultSearchResource?: ResourceType;
/** Current search type/resource type */
searchType: ResourceType;
/** List of available search type options */
searchTypeOptions: SearchTypeOption[];
/** Child elements */
children: ReactNode;
}
/**
* Universal search data provider.
*/
function UniversalSearchProvider({
export function UniversalSearchProvider({
isLoading,
defaultSearchResource,
searchType,
searchTypeOptions,
...props
}) {
children,
}: UniversalSearchProviderProps) {
// Provider payload.
const provider = {
const provider: UniversalSearchContextValue = {
isLoading,
searchType,
defaultSearchResource,
searchTypeOptions,
};
return <UniversalSearchContext.Provider value={provider} {...props} />;
return (
<UniversalSearchContext.Provider value={provider}>
{children}
</UniversalSearchContext.Provider>
);
}
const useUniversalSearchContext = () =>
React.useContext(UniversalSearchContext);
/**
* Hook to access the universal search context.
* @throws Error if used outside of UniversalSearchProvider
*/
export const useUniversalSearchContext = (): UniversalSearchContextValue => {
const context = useContext(UniversalSearchContext);
export { UniversalSearchProvider, useUniversalSearchContext };
if (context === undefined) {
throw new Error(
'useUniversalSearchContext must be used within a UniversalSearchProvider',
);
}
return context;
};
+8 -10
View File
@@ -1,12 +1,10 @@
// @ts-nocheck
import React from 'react';
import PropTypes from 'prop-types';
import React, { ReactNode } from 'react';
export const If = (props) =>
props.condition ? (props.render ? props.render() : props.children) : null;
interface IfProps {
condition: boolean;
children?: ReactNode;
render?: () => ReactNode;
}
If.propTypes = {
// condition: PropTypes.bool.isRequired,
children: PropTypes.node,
render: PropTypes.func,
};
export const If = (props: IfProps): React.ReactElement | null =>
props.condition ? (props.render ? <>{props.render()}</> : <>{props.children}</>) : null;
@@ -26,7 +26,7 @@ export const ACCOUNT_TYPE = {
export const ACCOUNT_PARENT_TYPE = {
CURRENT_ASSET: 'current-asset',
FIXED_ASSET: 'fixed-asset',
NON_CURRENT_ASSET: 'non-ACCOUNT_PARENT_TYPE.CURRENT_ASSET',
NON_CURRENT_ASSET: 'non-current-asset',
CURRENT_LIABILITY: 'current-liability',
LOGN_TERM_LIABILITY: 'long-term-liability',
@@ -41,7 +41,7 @@ export const ACCOUNT_ROOT_TYPE = {
ASSET: 'asset',
LIABILITY: 'liability',
EQUITY: 'equity',
EXPENSE: 'expene',
EXPENSE: 'expense',
INCOME: 'income',
};
@@ -19,7 +19,7 @@ export const getAddMoneyInOptions = () => [
export const getAddMoneyOutOptions = () => [
{
name: intl.get('banking.owner_drawings'),
value: 'OwnerDrawing',
value: 'owner_drawing',
},
{
name: intl.get('banking.expenses'),
@@ -31,11 +31,11 @@ export const getAddMoneyOutOptions = () => [
},
];
export const TRANSACRIONS_TYPE = [
export const TRANSACTIONS_TYPE = [
'OwnerContribution',
'OtherIncome',
'TransferFromAccount',
'OnwersDrawing',
'OwnerDrawing',
'OtherExpense',
'TransferToAccount',
];
@@ -32,38 +32,38 @@ export default function MakeJournalFloatingAction() {
// Handle submit & publish button click.
const handleSubmitPublishBtnClick = (event) => {
submitForm();
setSubmitPayload({ redirect: true, publish: true });
submitForm();
};
// Handle submit, publish & new button click.
const handleSubmitPublishAndNewBtnClick = (event) => {
submitForm();
setSubmitPayload({ redirect: false, publish: true, resetForm: true });
submitForm();
};
// Handle submit, publish & edit button click.
const handleSubmitPublishContinueEditingBtnClick = (event) => {
submitForm();
setSubmitPayload({ redirect: false, publish: true });
submitForm();
};
// Handle submit as draft button click.
const handleSubmitDraftBtnClick = (event) => {
submitForm();
setSubmitPayload({ redirect: true, publish: false });
submitForm();
};
// Handle submit as draft & new button click.
const handleSubmitDraftAndNewBtnClick = (event) => {
submitForm();
setSubmitPayload({ redirect: false, publish: false, resetForm: true });
submitForm();
};
// Handle submit as draft & continue editing button click.
const handleSubmitDraftContinueEditingBtnClick = (event) => {
submitForm();
setSubmitPayload({ redirect: false, publish: false });
submitForm();
};
// Handle cancel button click.
@@ -213,17 +213,17 @@ export const currenciesFieldShouldUpdate = (newProps, oldProps) => {
export const useSetPrimaryBranchToForm = () => {
const { setFieldValue } = useFormikContext();
const { branches, isBranchesSuccess } = useMakeJournalFormContext();
const { branches, isBranchesSuccess, isNewMode } = useMakeJournalFormContext();
React.useEffect(() => {
if (isBranchesSuccess) {
if (isBranchesSuccess && isNewMode) {
const primaryBranch = branches.find((b) => b.primary) || first(branches);
if (primaryBranch) {
setFieldValue('branch_id', primaryBranch.id);
}
}
}, [isBranchesSuccess, setFieldValue, branches]);
}, [isBranchesSuccess, setFieldValue, branches, isNewMode]);
};
export const useManualJournalCreditTotal = () => {
@@ -58,7 +58,7 @@ export default function InviteAcceptForm() {
data: { errors },
},
}) => {
if (errors.find((e) => e.type === 'INVITE.TOKEN.NOT.FOUND')) {
if (errors.find((e) => e.type === 'INVITE_TOKEN_INVALID')) {
AppToaster.show({
message: intl.get('an_unexpected_error_occurred'),
intent: Intent.DANGER,
@@ -71,14 +71,6 @@ export default function InviteAcceptForm() {
phone_number: 'This phone number is used in another account.',
});
}
if (errors.find((e) => e.type === 'INVITE.TOKEN.NOT.FOUND')) {
AppToaster.show({
message: intl.get('an_unexpected_error_occurred'),
intent: Intent.DANGER,
position: Position.BOTTOM,
});
history.push('/auth/login');
}
setSubmitting(false);
},
);
@@ -29,14 +29,22 @@ function InviteAcceptProvider({ token, ...props }) {
if (inviteMetaError) { history.push('/auth/login'); }
}, [history, inviteMetaError]);
// Transform the backend response to match frontend expectations.
const transformedInviteMeta = inviteMeta
? {
email: inviteMeta.inviteToken?.email,
organizationName: inviteMeta.orgName,
}
: null;
// Provider payload.
const provider = {
token,
inviteMeta,
inviteMeta: transformedInviteMeta,
inviteMetaError,
isInviteMetaError,
isInviteMetaLoading,
inviteAcceptMutate
inviteAcceptMutate,
};
if (inviteMetaError) {
@@ -45,7 +53,6 @@ function InviteAcceptProvider({ token, ...props }) {
return (
<InviteAcceptLoading isLoading={isInviteMetaLoading}>
{ isInviteMetaError }
<InviteAcceptContext.Provider value={provider} {...props} />
</InviteAcceptLoading>
);
@@ -1,18 +0,0 @@
.root {
text-align: center;
}
.title{
font-size: 18px;
font-weight: 600;
margin-bottom: 0.5rem;
color: #252A31;
}
.description{
margin-bottom: 1rem;
font-size: 15px;
line-height: 1.45;
color: #404854;
}
@@ -1,12 +1,13 @@
// @ts-nocheck
import { Button, Intent } from '@blueprintjs/core';
import { x } from '@xstyled/emotion';
import AuthInsider from './AuthInsider';
import { AuthInsiderCard } from './_components';
import styles from './RegisterVerify.module.scss';
import { AppToaster, Stack } from '@/components';
import { useAuthActions, useAuthUserVerifyEmail } from '@/hooks/state';
import { useAuthSignUpVerifyResendMail } from '@/hooks/query';
import { AuthContainer } from './AuthContainer';
import { useIsDarkMode } from '@/hooks/useDarkMode';
export default function RegisterVerify() {
const { setLogout } = useAuthActions();
@@ -14,6 +15,7 @@ export default function RegisterVerify() {
useAuthSignUpVerifyResendMail();
const emailAddress = useAuthUserVerifyEmail();
const isDarkMode = useIsDarkMode();
const handleResendMailBtnClick = () => {
resendSignUpVerifyMail()
@@ -37,12 +39,24 @@ export default function RegisterVerify() {
return (
<AuthContainer>
<AuthInsider>
<AuthInsiderCard className={styles.root}>
<h2 className={styles.title}>Please verify your email</h2>
<p className={styles.description}>
<AuthInsiderCard textAlign="center">
<x.h2
fontSize="18px"
fontWeight={600}
mb="0.5rem"
color={isDarkMode ? 'rgba(255, 255, 255, 0.85)' : '#252A31'}
>
Please verify your email
</x.h2>
<x.p
mb="1rem"
fontSize="15px"
lineHeight="1.45"
color={isDarkMode ? 'rgba(255, 255, 255, 0.7)' : '#404854'}
>
We sent an email to <strong>{emailAddress}</strong> Click the link
inside to get started.
</p>
</x.p>
<Stack spacing={4}>
<Button
@@ -35,11 +35,16 @@ function UncategorizeBankTransactionsBulkAlert({
uncategorizeTransactions({ ids: uncategorizeTransactionsIds })
.then(() => {
AppToaster.show({
message: 'The bank feeds of the bank account has been resumed.',
message: 'The selected transactions have been uncategorized.',
intent: Intent.SUCCESS,
});
})
.catch((error) => {})
.catch((error) => {
AppToaster.show({
message: 'Something went wrong while uncategorizing transactions.',
intent: Intent.DANGER,
});
})
.finally(() => {
closeAlert(name);
});
@@ -2,7 +2,7 @@
import React from 'react';
import intl from 'react-intl-universal';
import { Intent, Menu, MenuItem, Tag } from '@blueprintjs/core';
import { FormatDateCell, Icon } from '@/components';
import { Icon } from '@/components';
import { safeCallback } from '@/utils';
import { useAccountTransactionsContext } from './AccountTransactionsProvider';
import FinancialLoadingBar from '@/containers/FinancialStatements/FinancialLoadingBar';
@@ -75,8 +75,7 @@ export function useAccountTransactionsColumns() {
{
id: 'date',
Header: intl.get('date'),
accessor: 'date',
Cell: FormatDateCell,
accessor: 'formatted_date',
width: 110,
className: 'date',
clickable: true,
@@ -82,7 +82,7 @@ function CashflowBankAccount({
const handleEditAccount = () => {
openDialog(DialogsName.AccountForm, {
action: AccountDialogAction.Edit,
id: account.id,
accountId: account.id,
});
};
// Handle money in menu item actions.
@@ -92,7 +92,7 @@ function CategorizeTransactionFormSubContent() {
} else if (values.transactionType === 'transfer_to_account') {
return <CategorizeTransactionToAccount />;
// Owner drawings.
} else if (values.transactionType === 'OwnerDrawing') {
} else if (values.transactionType === 'owner_drawing') {
return <CategorizeTransactionOwnerDrawings />;
}
return null;
@@ -23,6 +23,8 @@ import {
FFormGroup,
FTextArea,
FMoneyInputGroup,
Icon,
FDateInput,
} from '@/components';
import { CLASSES, ACCOUNT_TYPE, Features } from '@/constants';
@@ -17,6 +17,7 @@ import {
FTextArea,
FInputGroup,
FDateInput,
Icon,
} from '@/components';
import { ACCOUNT_TYPE, CLASSES, Features } from '@/constants';
import {
@@ -19,7 +19,7 @@ import { useMoneyInDailogContext } from './MoneyInDialogProvider';
*/
export default function TransactionTypeFields() {
// Money in dialog context.
const { cashflowAccounts, setAccountId } = useMoneyInDailogContext();
const { cashflowAccounts, setAccountId, accountId } = useMoneyInDailogContext();
// Retrieves the add money in button options.
const addMoneyInOptions = useMemo(() => getAddMoneyInOptions(), []);
@@ -55,8 +55,8 @@ export default function TransactionTypeFields() {
<FAccountsSuggestField
name={'cashflow_account_id'}
items={cashflowAccounts}
onItemSelect={({ id }) => {
setAccountId(id);
onItemChange={(value) => {
setAccountId(value);
}}
/>
</FFormGroup>
@@ -46,4 +46,8 @@ export const BranchRowDivider = styled.div`
height: 1px;
background: #ebf1f6;
margin-bottom: 15px;
.bp4-dark & {
background: var(--color-dark-gray5);
}
`;
@@ -17,7 +17,7 @@ function MoneyOutContentFields() {
const transactionType = useMemo(() => {
switch (values.transaction_type) {
case 'OwnerDrawing':
case 'owner_drawing':
return <OwnerDrawingsFormFields />;
case 'other_expense':
@@ -60,8 +60,8 @@ function TransactionTypeFields() {
<FAccountsSuggestField
name={'cashflow_account_id'}
items={cashflowAccounts}
onItemSelect={({ id }) => {
setAccountId(id);
onItemChange={(value) => {
setAccountId(value);
}}
/>
</FFormGroup>
@@ -45,4 +45,8 @@ export const BranchRowDivider = styled.div`
height: 1px;
background: #ebf1f6;
margin-bottom: 15px;
.bp4-dark & {
background: var(--color-dark-gray5);
}
`;
@@ -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_address_1'}
label={<T id={'address_line_1'} />}
inline={true}
>
<FTextArea name={'billing_address_1'} />
</FFormGroup>
{/*------------ Billing Address 2 -----------*/}
<FFormGroup
name={'billing_address_2'}
label={<T id={'address_line_2'} />}
inline={true}
>
<FTextArea name={'billing_address_2'} />
</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_address_1'}
label={<T id={'address_line_1'} />}
inline={true}
>
<FTextArea name={'shipping_address_1'} />
</FFormGroup>
{/*------------ Shipping Address 2 -----------*/}
<FFormGroup
name={'shipping_address_2'}
label={<T id={'address_line_2'} />}
inline={true}
>
<FTextArea name={'shipping_address_2'} />
</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;
`;
@@ -17,28 +17,28 @@ const Schema = Yup.object().shape({
.label(intl.get('display_name_')),
email: Yup.string().email().nullable(),
work_phone: Yup.number(),
personal_phone: Yup.number(),
work_phone: Yup.string().nullable(),
personal_phone: Yup.string().nullable(),
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_address1: Yup.string().trim(),
billing_address2: Yup.string().trim(),
billing_address_city: Yup.string().trim(),
billing_address_state: Yup.string().trim(),
billing_address_postcode: Yup.string().nullable(),
billing_address_phone: Yup.number(),
billing_address_phone: Yup.string().nullable(),
shipping_address_country: Yup.string().trim(),
shipping_address_1: Yup.string().trim(),
shipping_address_2: Yup.string().trim(),
shipping_address1: Yup.string().trim(),
shipping_address2: Yup.string().trim(),
shipping_address_city: Yup.string().trim(),
shipping_address_state: Yup.string().trim(),
shipping_address_postcode: Yup.string().nullable(),
shipping_address_phone: Yup.number(),
shipping_address_phone: Yup.string().nullable(),
opening_balance: Yup.number().nullable(),
currency_code: Yup.string(),
@@ -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,67 +23,56 @@ 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"
items={currencies}
disabled={customerId}
/>
/>
</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 } from '@/utils';
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,25 +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 };
const formValues = {
...values,
active: parseBoolean(values.active, true),
};
const onSuccess = (res) => {
const onSuccess = (res: { data?: unknown }) => {
AppToaster.show({
message: intl.get(
isNewMode
@@ -80,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.
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
onSubmitSuccess={handleSubmitSuccess}
onCancel={handleFormCancel}
/>
</DashboardCard>
</CustomerFormPageLoading>
<CustomerFormPageContent />
</CustomerFormProvider>
);
}
const CustomerFormPageFormik = styled(CustomerFormFormik)`
.page-form {
&__floating-actions {
margin-left: -40px;
margin-right: -40px;
function CustomerFormPageContent() {
const history = useHistory();
const { isFormLoading } = useCustomerFormContext();
const handleSubmitSuccess = (values, formArgs, submitPayload) => {
if (!submitPayload.noRedirect) {
history.push('/customers');
}
}
`;
const CustomerDashboardInsider = styled(DashboardInsider)`
padding-bottom: 64px;
`;
// Handle the form cancel button click.
const handleFormCancel = () => {
history.goBack();
};
return (
<DashboardInsider loading={isFormLoading}>
<Box mx={'auto'} maxWidth={800}>
<CustomerFormFormik
onSubmitSuccess={handleSubmitSuccess}
onCancel={handleFormCancel}
/>
</Box>
</DashboardInsider>
)
}
@@ -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}
>
<InputGroup 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>
</div>
<FFormGroup name={'note'} label={<T id={'note'} />} inline={false} fill>
<FTextArea name={'note'} fill />
</FFormGroup>
);
}
@@ -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: '',
@@ -23,16 +24,16 @@ export const defaultInitialValues = {
active: true,
billing_address_country: '',
billing_address_1: '',
billing_address_2: '',
billing_address1: '',
billing_address2: '',
billing_address_city: '',
billing_address_state: '',
billing_address_postcode: '',
billing_address_phone: '',
shipping_address_country: '',
shipping_address_1: '',
shipping_address_2: '',
shipping_address1: '',
shipping_address2: '',
shipping_address_city: '',
shipping_address_state: '',
shipping_address_postcode: '',
@@ -15,10 +15,16 @@ export const AccountDialogAction = {
*/
export const transformApiErrors = (errors) => {
const fields = {};
if (errors.find((e) => e.type === 'account_code_required')) {
fields.code = intl.get('account_code_is_required');
}
if (errors.find((e) => e.type === 'NOT_UNIQUE_CODE')) {
fields.code = intl.get('account_code_is_not_unique');
}
if (errors.find((e) => e.type === 'ACCOUNT.NAME.NOT.UNIQUE')) {
if (errors.find((e) => e.type === 'account_code_not_unique')) {
fields.code = intl.get('account_code_is_not_unique');
}
if (errors.find((e) => e.type === 'account_name_not_unqiue')) {
fields.name = intl.get('account_name_is_already_used');
}
if (
@@ -2,7 +2,6 @@
import intl from 'react-intl-universal';
import React from 'react';
import { FormatDateCell } from '@/components';
import { useAccountDrawerTableOptionsContext } from './AccountDrawerTableOptionsProvider';
/**
@@ -15,8 +14,7 @@ export const useAccountReadEntriesColumns = () => {
() => [
{
Header: intl.get('transaction_date'),
accessor: 'date',
Cell: FormatDateCell,
accessor: 'formatted_date',
width: 110,
textOverview: true,
},
@@ -15,7 +15,7 @@ import { useContactDetailDrawerContext } from './ContactDetailDrawerProvider';
import { withAlertActions } from '@/containers/Alert/withAlertActions';
import { withDrawerActions } from '@/containers/Drawer/withDrawerActions';
import { DashboardActionsBar, Icon, FormattedMessage as T } from '@/components';
import { DrawerActionsBar, Icon, FormattedMessage as T } from '@/components';
import { safeCallback, compose } from '@/utils';
@@ -46,7 +46,7 @@ function ContactDetailActionsBar({
};
return (
<DashboardActionsBar>
<DrawerActionsBar>
<NavbarGroup>
<Button
className={Classes.MINIMAL}
@@ -63,7 +63,7 @@ function ContactDetailActionsBar({
onClick={safeCallback(onDeleteContact)}
/>
</NavbarGroup>
</DashboardActionsBar>
</DrawerActionsBar>
);
}
@@ -23,7 +23,6 @@ import { withDialogActions } from '@/containers/Dialog/withDialogActions';
import { withDrawerActions } from '@/containers/Drawer/withDrawerActions';
import {
DashboardActionsBar,
Can,
Icon,
FormattedMessage as T,
@@ -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} />
@@ -20,7 +20,7 @@ import {
If,
Icon,
FormattedMessage as T,
DashboardActionsBar,
DrawerActionsBar,
Can,
} from '@/components';
@@ -63,7 +63,7 @@ function VendorCreditDetailActionsBar({
};
return (
<DashboardActionsBar>
<DrawerActionsBar>
<NavbarGroup>
<Can I={VendorCreditAction.Edit} a={AbilitySubject.VendorCredit}>
<Button
@@ -105,7 +105,7 @@ function VendorCreditDetailActionsBar({
</If>
</Can>
</NavbarGroup>
</DashboardActionsBar>
</DrawerActionsBar>
);
}
@@ -152,17 +152,17 @@ export const transformFormValuesToRequest = (values) => {
export const useSetPrimaryBranchToForm = () => {
const { setFieldValue } = useFormikContext();
const { branches, isBranchesSuccess } = useExpenseFormContext();
const { branches, isBranchesSuccess, isNewMode } = useExpenseFormContext();
React.useEffect(() => {
if (isBranchesSuccess) {
if (isBranchesSuccess && isNewMode) {
const primaryBranch = branches.find((b) => b.primary) || first(branches);
if (primaryBranch) {
setFieldValue('branch_id', primaryBranch.id);
}
}
}, [isBranchesSuccess, setFieldValue, branches]);
}, [isBranchesSuccess, setFieldValue, branches, isNewMode]);
};
/**
@@ -57,7 +57,7 @@ function GlobalErrors({
if (globalErrors.access_denied) {
toastKeySomethingWrong = AppToaster.show(
{
message: intl.get('global_error.you_dont_have_permissions'),
message: globalErrors.access_denied.message || intl.get('global_error.you_dont_have_permissions'),
intent: Intent.DANGER,
onDismiss: () => {
globalErrorsSet({ access_denied: false });
@@ -111,12 +111,12 @@ export default function PreferencesGeneralForm({ isSubmitting }) {
>
<Stack>
<FInputGroup
name={'address.address_1'}
name={'address.address1'}
placeholder={'Address 1'}
fastField
/>
<FInputGroup
name={'address.address_2'}
name={'address.address2'}
placeholder={'Address 2'}
fastField
/>
@@ -231,25 +231,25 @@ export const handleErrors = (errors, { setErrors }) => {
export const useSetPrimaryBranchToForm = () => {
const { setFieldValue } = useFormikContext();
const { branches, isBranchesSuccess } = useBillFormContext();
const { branches, isBranchesSuccess, isNewMode } = useBillFormContext();
React.useEffect(() => {
if (isBranchesSuccess) {
if (isBranchesSuccess && isNewMode) {
const primaryBranch = branches.find((b) => b.primary) || first(branches);
if (primaryBranch) {
setFieldValue('branch_id', primaryBranch.id);
}
}
}, [isBranchesSuccess, setFieldValue, branches]);
}, [isBranchesSuccess, setFieldValue, branches, isNewMode]);
};
export const useSetPrimaryWarehouseToForm = () => {
const { setFieldValue } = useFormikContext();
const { warehouses, isWarehousesSuccess } = useBillFormContext();
const { warehouses, isWarehousesSuccess, isNewMode } = useBillFormContext();
React.useEffect(() => {
if (isWarehousesSuccess) {
if (isWarehousesSuccess && isNewMode) {
const primaryWarehouse =
warehouses.find((b) => b.primary) || first(warehouses);
@@ -257,7 +257,7 @@ export const useSetPrimaryWarehouseToForm = () => {
setFieldValue('warehouse_id', primaryWarehouse.id);
}
}
}, [isWarehousesSuccess, setFieldValue, warehouses]);
}, [isWarehousesSuccess, setFieldValue, warehouses, isNewMode]);
};
/**
@@ -1,10 +1,10 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import { MenuItem } from '@blueprintjs/core';
import { MenuItem, Intent } from '@blueprintjs/core';
import { formattedAmount } from '@/utils';
import { T, Icon, Choose, If } from '@/components';
import { T, Icon, Choose, If, TextStatus } from '@/components';
import { RESOURCES_TYPES } from '@/constants/resourcesTypes';
import { AbilitySubject, BillAction } from '@/constants/abilityOption';
@@ -41,35 +41,35 @@ export function BillStatus({ bill }) {
return (
<Choose>
<Choose.When condition={bill.is_fully_paid && bill.is_open}>
<span class="fully-paid-text">
<TextStatus intent={Intent.SUCCESS}>
<T id={'paid'} />
</span>
</TextStatus>
</Choose.When>
<Choose.When condition={bill.is_open}>
<Choose>
<Choose.When condition={bill.is_overdue}>
<span className={'overdue-status'}>
<TextStatus intent={Intent.DANGER}>
{intl.get('overdue_by', { overdue: bill.overdue_days })}
</span>
</TextStatus>
</Choose.When>
<Choose.Otherwise>
<span className={'due-status'}>
<TextStatus intent={Intent.WARNING}>
{intl.get('due_in', { due: bill.remaining_days })}
</span>
</TextStatus>
</Choose.Otherwise>
</Choose>
<If condition={bill.is_partially_paid}>
<span className="partial-paid">
<TextStatus intent={Intent.WARNING}>
{intl.get('day_partially_paid', {
due: formattedAmount(bill.due_amount, bill.currency_code),
})}
</span>
</TextStatus>
</If>
</Choose.When>
<Choose.Otherwise>
<span class="draft">
<TextStatus intent={Intent.NONE}>
<T id={'draft'} />
</span>
</TextStatus>
</Choose.Otherwise>
</Choose>
);
@@ -156,25 +156,25 @@ export const useObserveVendorCreditNoSettings = (prefix, nextNumber) => {
export const useSetPrimaryBranchToForm = () => {
const { setFieldValue } = useFormikContext();
const { branches, isBranchesSuccess } = useVendorCreditNoteFormContext();
const { branches, isBranchesSuccess, isNewMode } = useVendorCreditNoteFormContext();
React.useEffect(() => {
if (isBranchesSuccess) {
if (isBranchesSuccess && isNewMode) {
const primaryBranch = branches.find((b) => b.primary) || first(branches);
if (primaryBranch) {
setFieldValue('branch_id', primaryBranch.id);
}
}
}, [isBranchesSuccess, setFieldValue, branches]);
}, [isBranchesSuccess, setFieldValue, branches, isNewMode]);
};
export const useSetPrimaryWarehouseToForm = () => {
const { setFieldValue } = useFormikContext();
const { warehouses, isWarehousesSuccess } = useVendorCreditNoteFormContext();
const { warehouses, isWarehousesSuccess, isNewMode } = useVendorCreditNoteFormContext();
React.useEffect(() => {
if (isWarehousesSuccess) {
if (isWarehousesSuccess && isNewMode) {
const primaryWarehouse =
warehouses.find((b) => b.primary) || first(warehouses);
@@ -182,7 +182,7 @@ export const useSetPrimaryWarehouseToForm = () => {
setFieldValue('warehouse_id', primaryWarehouse.id);
}
}
}, [isWarehousesSuccess, setFieldValue, warehouses]);
}, [isWarehousesSuccess, setFieldValue, warehouses, isNewMode]);
};
/**
@@ -115,17 +115,17 @@ export const transformFormToRequest = (form) => {
export const useSetPrimaryBranchToForm = () => {
const { setFieldValue } = useFormikContext();
const { branches, isBranchesSuccess } = usePaymentMadeFormContext();
const { branches, isBranchesSuccess, isNewMode } = usePaymentMadeFormContext();
React.useEffect(() => {
if (isBranchesSuccess) {
if (isBranchesSuccess && isNewMode) {
const primaryBranch = branches.find((b) => b.primary) || first(branches);
if (primaryBranch) {
setFieldValue('branch_id', primaryBranch.id);
}
}
}, [isBranchesSuccess, setFieldValue, branches]);
}, [isBranchesSuccess, setFieldValue, branches, isNewMode]);
};
/**
@@ -16,6 +16,7 @@ import { useDrawerActions } from '@/hooks/state';
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider';
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
import { Box } from '@/components';
export function CreditNoteCustomizeContent() {
const { payload, name } = useDrawerContext();
@@ -45,7 +46,9 @@ function CreditNoteCustomizeFormContent() {
return (
<ElementCustomizeContent>
<ElementCustomize.PaperTemplate>
<CreditNotePaperTemplateFormConnected />
<Box overflow="auto" flex="1 1" px={4} py={6}>
<CreditNotePaperTemplateFormConnected />
</Box>
</ElementCustomize.PaperTemplate>
<ElementCustomize.FieldsTab id={'general'} label={'General'}>
@@ -148,25 +148,25 @@ export const entriesFieldShouldUpdate = (newProps, oldProps) => {
export const useSetPrimaryBranchToForm = () => {
const { setFieldValue } = useFormikContext();
const { branches, isBranchesSuccess } = useCreditNoteFormContext();
const { branches, isBranchesSuccess, isNewMode } = useCreditNoteFormContext();
React.useEffect(() => {
if (isBranchesSuccess) {
if (isBranchesSuccess && isNewMode) {
const primaryBranch = branches.find((b) => b.primary) || first(branches);
if (primaryBranch) {
setFieldValue('branch_id', primaryBranch.id);
}
}
}, [isBranchesSuccess, setFieldValue, branches]);
}, [isBranchesSuccess, setFieldValue, branches, isNewMode]);
};
export const useSetPrimaryWarehouseToForm = () => {
const { setFieldValue } = useFormikContext();
const { warehouses, isWarehousesSuccess } = useCreditNoteFormContext();
const { warehouses, isWarehousesSuccess, isNewMode } = useCreditNoteFormContext();
React.useEffect(() => {
if (isWarehousesSuccess) {
if (isWarehousesSuccess && isNewMode) {
const primaryWarehouse =
warehouses.find((b) => b.primary) || first(warehouses);
@@ -174,7 +174,7 @@ export const useSetPrimaryWarehouseToForm = () => {
setFieldValue('warehouse_id', primaryWarehouse.id);
}
}
}, [isWarehousesSuccess, setFieldValue, warehouses]);
}, [isWarehousesSuccess, setFieldValue, warehouses, isNewMode]);
};
/**
@@ -16,6 +16,7 @@ import { useDrawerActions } from '@/hooks/state';
import { BrandingTemplateForm } from '@/containers/BrandingTemplates/BrandingTemplateForm';
import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider';
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
import { Box } from '@/components';
export function EstimateCustomizeContent() {
const { payload, name } = useDrawerContext();
@@ -44,7 +45,9 @@ function EstimateCustomizeFormContent() {
return (
<ElementCustomizeContent>
<ElementCustomize.PaperTemplate>
<EstimatePaperTemplateFormConnected />
<Box overflow="auto" flex="1 1" px={4} py={6}>
<EstimatePaperTemplateFormConnected />
</Box>
</ElementCustomize.PaperTemplate>
<ElementCustomize.FieldsTab id={'general'} label={'General'}>
@@ -182,10 +182,10 @@ export const transfromsFormValuesToRequest = (values) => {
export const useSetPrimaryWarehouseToForm = () => {
const { setFieldValue } = useFormikContext();
const { warehouses, isWarehousesSuccess } = useEstimateFormContext();
const { warehouses, isWarehousesSuccess, isNewMode } = useEstimateFormContext();
React.useEffect(() => {
if (isWarehousesSuccess) {
if (isWarehousesSuccess && isNewMode) {
const primaryWarehouse =
warehouses.find((b) => b.primary) || first(warehouses);
@@ -193,22 +193,22 @@ export const useSetPrimaryWarehouseToForm = () => {
setFieldValue('warehouse_id', primaryWarehouse.id);
}
}
}, [isWarehousesSuccess, setFieldValue, warehouses]);
}, [isWarehousesSuccess, setFieldValue, warehouses, isNewMode]);
};
export const useSetPrimaryBranchToForm = () => {
const { setFieldValue } = useFormikContext();
const { branches, isBranchesSuccess } = useEstimateFormContext();
const { branches, isBranchesSuccess, isNewMode } = useEstimateFormContext();
React.useEffect(() => {
if (isBranchesSuccess) {
if (isBranchesSuccess && isNewMode) {
const primaryBranch = branches.find((b) => b.primary) || first(branches);
if (primaryBranch) {
setFieldValue('branch_id', primaryBranch.id);
}
}
}, [isBranchesSuccess, setFieldValue, branches]);
}, [isBranchesSuccess, setFieldValue, branches, isNewMode]);
};
/**
@@ -18,7 +18,7 @@ const estimatePreviewCss = css`
export const EstimateSendMailReceiptPreview = () => {
return (
<Stack>
<Stack spacing={0}>
<EstimateSendMailPreviewHeader />
<Stack px={4} py={6}>
@@ -7,7 +7,7 @@ import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
export function EstimateSendPdfPreviewConnected() {
return (
<Stack>
<Stack spacing={0}>
<EstimateSendMailPreviewHeader />
<Stack px={4} py={6}>
@@ -1,9 +1,9 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import { MenuItem } from '@blueprintjs/core';
import { MenuItem, Intent } from '@blueprintjs/core';
import { Choose, T, Icon } from '@/components';
import { Choose, T, Icon, TextStatus } from '@/components';
import { RESOURCES_TYPES } from '@/constants/resourcesTypes';
import { AbilitySubject, SaleEstimateAction } from '@/constants/abilityOption';
@@ -37,28 +37,28 @@ export const EstimateUniversalSearchSelect = withDrawerActions(
export const EstimateStatus = ({ estimate }) => (
<Choose>
<Choose.When condition={estimate.is_delivered && estimate.is_approved}>
<span class="approved">
<TextStatus intent={Intent.SUCCESS}>
<T id={'approved'} />
</span>
</TextStatus>
</Choose.When>
<Choose.When condition={estimate.is_delivered && estimate.is_rejected}>
<span class="reject">
<TextStatus intent={Intent.DANGER}>
<T id={'rejected'} />
</span>
</TextStatus>
</Choose.When>
<Choose.When
condition={
estimate.is_delivered && !estimate.is_rejected && !estimate.is_approved
}
>
<span class="delivered">
<TextStatus intent={Intent.SUCCESS}>
<T id={'delivered'} />
</span>
</TextStatus>
</Choose.When>
<Choose.Otherwise>
<span class="draft">
<TextStatus intent={Intent.NONE}>
<T id={'draft'} />
</span>
</TextStatus>
</Choose.Otherwise>
</Choose>
);
@@ -264,10 +264,10 @@ const transformPaymentMethodsToForm = (
export const useSetPrimaryWarehouseToForm = () => {
const { setFieldValue } = useFormikContext();
const { warehouses, isWarehousesSuccess } = useInvoiceFormContext();
const { warehouses, isWarehousesSuccess, isNewMode } = useInvoiceFormContext();
React.useEffect(() => {
if (isWarehousesSuccess) {
if (isWarehousesSuccess && isNewMode) {
const primaryWarehouse =
warehouses.find((b) => b.primary) || first(warehouses);
@@ -275,22 +275,22 @@ export const useSetPrimaryWarehouseToForm = () => {
setFieldValue('warehouse_id', primaryWarehouse.id);
}
}
}, [isWarehousesSuccess, setFieldValue, warehouses]);
}, [isWarehousesSuccess, setFieldValue, warehouses, isNewMode]);
};
export const useSetPrimaryBranchToForm = () => {
const { setFieldValue } = useFormikContext();
const { branches, isBranchesSuccess } = useInvoiceFormContext();
const { branches, isBranchesSuccess, isNewMode } = useInvoiceFormContext();
React.useEffect(() => {
if (isBranchesSuccess) {
if (isBranchesSuccess && isNewMode) {
const primaryBranch = branches.find((b) => b.primary) || first(branches);
if (primaryBranch) {
setFieldValue('branch_id', primaryBranch.id);
}
}
}, [isBranchesSuccess, setFieldValue, branches]);
}, [isBranchesSuccess, setFieldValue, branches, isNewMode]);
};
/**
@@ -1,9 +1,9 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import { MenuItem } from '@blueprintjs/core';
import { MenuItem, Intent } from '@blueprintjs/core';
import { T, Choose, Icon } from '@/components';
import { T, Choose, Icon, TextStatus } from '@/components';
import { highlightText } from '@/utils';
import { RESOURCES_TYPES } from '@/constants/resourcesTypes';
@@ -39,29 +39,29 @@ function InvoiceStatus({ customer }) {
return (
<Choose>
<Choose.When condition={customer.is_fully_paid && customer.is_delivered}>
<span class="status status-success">
<TextStatus intent={Intent.SUCCESS}>
<T id={'paid'} />
</span>
</TextStatus>
</Choose.When>
<Choose.When condition={customer.is_delivered}>
<Choose>
<Choose.When condition={customer.is_overdue}>
<span className={'status status-warning'}>
<TextStatus intent={Intent.DANGER}>
{intl.get('overdue_by', { overdue: customer.overdue_days })}
</span>
</TextStatus>
</Choose.When>
<Choose.Otherwise>
<span className={'status status-warning'}>
<TextStatus intent={Intent.WARNING}>
{intl.get('due_in', { due: customer.remaining_days })}
</span>
</TextStatus>
</Choose.Otherwise>
</Choose>
</Choose.When>
<Choose.Otherwise>
<span class="status status--gray">
<TextStatus intent={Intent.NONE}>
<T id={'draft'} />
</span>
</TextStatus>
</Choose.Otherwise>
</Choose>
);
@@ -94,7 +94,6 @@ export function InvoiceUniversalSearchItem(
</>
}
onClick={handleClick}
className={'universal-search__item--invoice'}
/>
);
}
@@ -177,17 +177,17 @@ export const transformFormToRequest = (form) => {
export const useSetPrimaryBranchToForm = () => {
const { setFieldValue } = useFormikContext();
const { branches, isBranchesSuccess } = usePaymentReceiveFormContext();
const { branches, isBranchesSuccess, isNewMode } = usePaymentReceiveFormContext();
React.useEffect(() => {
if (isBranchesSuccess) {
if (isBranchesSuccess && isNewMode) {
const primaryBranch = branches.find((b) => b.primary) || first(branches);
if (primaryBranch) {
setFieldValue('branch_id', primaryBranch.id);
}
}
}, [isBranchesSuccess, setFieldValue, branches]);
}, [isBranchesSuccess, setFieldValue, branches, isNewMode]);
};
/**
@@ -19,6 +19,7 @@ import { useDrawerActions } from '@/hooks/state';
import { BrandingTemplateForm } from '@/containers/BrandingTemplates/BrandingTemplateForm';
import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider';
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
import { Box } from '@/components';
export function PaymentReceivedCustomizeContent() {
const { payload, name } = useDrawerContext();
@@ -51,7 +52,9 @@ function PaymentReceivedCustomizeFormContent() {
return (
<ElementCustomizeContent>
<ElementCustomize.PaperTemplate>
<PaymentReceivedPaperTemplateFormConnected />
<Box overflow="auto" flex="1 1" px={4} py={6}>
<PaymentReceivedPaperTemplateFormConnected />
</Box>
</ElementCustomize.PaperTemplate>
<ElementCustomize.FieldsTab id={'general'} label={'General'}>
@@ -7,7 +7,7 @@ import { PaymentReceivedMailPreviewHeader } from './PaymentReceivedMailPreviewHe
export function PaymentReceivedSendMailPreviewPdf() {
return (
<Stack flex={1}>
<Stack flex={1} spacing={0}>
<PaymentReceivedMailPreviewHeader />
<Stack px={4} py={6}>
@@ -22,7 +22,6 @@ function PaymentReceivedSendPdfPreviewIframe() {
const { data, isLoading } = useGetPaymentReceiveHtml(
payload?.paymentReceivedId,
);
if (isLoading && data) {
return <Spinner size={20} />;
}
@@ -18,7 +18,7 @@ const mailReceiptCss = css`
export function PaymentReceivedMailPreviewReceipt() {
return (
<Stack flex={1}>
<Stack flex={1} spacing={0}>
<PaymentReceivedMailPreviewHeader />
<Stack px={4} py={6}>
@@ -16,6 +16,7 @@ import { useDrawerActions } from '@/hooks/state';
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider';
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
import { Box } from '@/components';
export function ReceiptCustomizeContent() {
const { payload, name } = useDrawerContext();
@@ -44,7 +45,9 @@ function ReceiptCustomizeFormContent() {
return (
<ElementCustomizeContent>
<ElementCustomize.PaperTemplate>
<ReceiptPaperTemplateFormConnected />
<Box overflow="auto" flex="1 1" px={4} py={6}>
<ReceiptPaperTemplateFormConnected />
</Box>
</ElementCustomize.PaperTemplate>
<ElementCustomize.FieldsTab id={'general'} label={'General'}>
@@ -173,10 +173,10 @@ export const transformFormValuesToRequest = (values) => {
export const useSetPrimaryWarehouseToForm = () => {
const { setFieldValue } = useFormikContext();
const { warehouses, isWarehousesSuccess } = useReceiptFormContext();
const { warehouses, isWarehousesSuccess, isNewMode } = useReceiptFormContext();
React.useEffect(() => {
if (isWarehousesSuccess) {
if (isWarehousesSuccess && isNewMode) {
const primaryWarehouse =
warehouses.find((b) => b.primary) || first(warehouses);
@@ -184,22 +184,22 @@ export const useSetPrimaryWarehouseToForm = () => {
setFieldValue('warehouse_id', primaryWarehouse.id);
}
}
}, [isWarehousesSuccess, setFieldValue, warehouses]);
}, [isWarehousesSuccess, setFieldValue, warehouses, isNewMode]);
};
export const useSetPrimaryBranchToForm = () => {
const { setFieldValue } = useFormikContext();
const { branches, isBranchesSuccess } = useReceiptFormContext();
const { branches, isBranchesSuccess, isNewMode } = useReceiptFormContext();
React.useEffect(() => {
if (isBranchesSuccess) {
if (isBranchesSuccess && isNewMode) {
const primaryBranch = branches.find((b) => b.primary) || first(branches);
if (primaryBranch) {
setFieldValue('branch_id', primaryBranch.id);
}
}
}, [isBranchesSuccess, setFieldValue, branches]);
}, [isBranchesSuccess, setFieldValue, branches, isNewMode]);
};
/**
@@ -7,7 +7,7 @@ import { SendMailViewPreviewPdfIframe } from '../../Estimates/SendMailViewDrawer
export function ReceiptSendMailPdfPreview() {
return (
<Stack>
<Stack spacing={0}>
<ReceiptSendMailPreviewHeader />
<Stack px={4} py={6}>
@@ -26,7 +26,5 @@ function ReceiptSendPdfPreviewIframe() {
}
const iframeSrcDoc = data?.htmlContent;
console.log(data, 'data');
return <SendMailViewPreviewPdfIframe srcDoc={iframeSrcDoc} />;
}
@@ -13,7 +13,7 @@ const receiptPreviewCss = css`
export function ReceiptSendMailPreview() {
return (
<Stack>
<Stack spacing={0}>
<ReceiptSendMailPreviewHeader />
<Stack px={4} py={6}>
@@ -1,9 +1,8 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import { MenuItem } from '@blueprintjs/core';
import { Icon, Choose, T } from '@/components';
import { MenuItem, Intent } from '@blueprintjs/core';
import { Icon, Choose, T, TextStatus } from '@/components';
import { RESOURCES_TYPES } from '@/constants/resourcesTypes';
import { AbilitySubject, SaleReceiptAction } from '@/constants/abilityOption';
import { withDrawerActions } from '@/containers/Drawer/withDrawerActions';
@@ -39,15 +38,15 @@ function ReceiptStatus({ receipt }) {
return (
<Choose>
<Choose.When condition={receipt.is_closed}>
<span class="closed">
<TextStatus intent={Intent.SUCCESS}>
<T id={'closed'} />
</span>
</TextStatus>
</Choose.When>
<Choose.Otherwise>
<span class="draft">
<TextStatus intent={Intent.NONE}>
<T id={'draft'} />
</span>
</TextStatus>
</Choose.Otherwise>
</Choose>
);
@@ -1,8 +1,8 @@
// @ts-nocheck
import React from 'react';
import { Intent, Tag } from '@blueprintjs/core';
import { Intent, Tag, Classes } from '@blueprintjs/core';
import { Align } from '@/constants';
import styled from 'styled-components';
import clsx from 'classnames';
const codeAccessor = (taxRate) => {
return (
@@ -28,13 +28,17 @@ const nameAccessor = (taxRate) => {
return (
<>
<span>{taxRate.name}</span>
{!!taxRate.is_compound && <CompoundText>(Compound tax)</CompoundText>}
{!!taxRate.is_compound && (
<span className={clsx(Classes.TEXT_MUTED)}>(Compound tax)</span>
)}
</>
);
};
const DescriptionAccessor = (taxRate) => {
return <DescriptionText>{taxRate.description}</DescriptionText>;
return (
<span className={clsx(Classes.TEXT_MUTED)}>{taxRate.description}</span>
);
};
/**
@@ -72,11 +76,3 @@ export const useTaxRatesTableColumns = () => {
];
};
const CompoundText = styled('span')`
color: #738091;
margin-left: 5px;
`;
const DescriptionText = styled('span')`
color: #5f6b7c;
`;
@@ -13,7 +13,7 @@ import {
Position,
} from '@blueprintjs/core';
import * as R from 'ramda';
import { AppToaster, Can, DashboardActionsBar, Icon } from '@/components';
import { AppToaster, Can, DrawerActionsBar, Icon } from '@/components';
import { AbilitySubject, TaxRateAction } from '@/constants/abilityOption';
import { withDrawerActions } from '@/containers/Drawer/withDrawerActions';
import { withAlertActions } from '@/containers/Alert/withAlertActions';
@@ -83,7 +83,7 @@ function TaxRateDetailsContentActionsBar({
};
return (
<DashboardActionsBar>
<DrawerActionsBar>
<NavbarGroup>
<Can I={TaxRateAction.Edit} a={AbilitySubject.TaxRate}>
<Button
@@ -137,7 +137,7 @@ function TaxRateDetailsContentActionsBar({
</Popover>
</Can>
</NavbarGroup>
</DashboardActionsBar>
</DrawerActionsBar>
);
}
@@ -74,9 +74,13 @@ const TaxRateHeader = styled(`div`)`
const TaxRateAmount = styled('div')`
line-height: 1;
font-size: 30px;
color: #565b71;
font-weight: 600;
display: inline-block;
color: var(--x-color-amount-text, #565b71);
.bp4-dark & {
color: rgba(255, 255, 255, 0.9);
}
`;
const TaxRateActiveTag = styled(Tag)`
@@ -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;
`;
@@ -10,28 +10,28 @@ const Schema = Yup.object().shape({
display_name: Yup.string().trim().required().label(intl.get('display_name_')),
email: Yup.string().email().nullable(),
work_phone: Yup.number(),
personal_phone: Yup.number(),
work_phone: Yup.string().nullable(),
personal_phone: Yup.string().nullable(),
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_address1: Yup.string().trim(),
billing_address2: Yup.string().trim(),
billing_address_city: Yup.string().trim(),
billing_address_state: Yup.string().trim(),
billing_address_postcode: Yup.string().nullable(),
billing_address_phone: Yup.number(),
billing_address_phone: Yup.string().nullable(),
shipping_address_country: Yup.string().trim(),
shipping_address_1: Yup.string().trim(),
shipping_address_2: Yup.string().trim(),
shipping_address1: Yup.string().trim(),
shipping_address2: Yup.string().trim(),
shipping_address_city: Yup.string().trim(),
shipping_address_state: Yup.string().trim(),
shipping_address_postcode: Yup.string().nullable(),
shipping_address_phone: Yup.number(),
shipping_address_phone: Yup.string().nullable(),
opening_balance: Yup.number().nullable(),
currency_code: Yup.string(),
@@ -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,29 +7,24 @@ 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';
import { useVendorFormContext } from './VendorFormProvider';
import { compose, transformToForm, safeInvoke } from '@/utils';
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,
@@ -69,7 +61,10 @@ function VendorFormFormik({
// Handles the form submit.
const handleFormSubmit = (values, form) => {
const { setSubmitting, resetForm } = form;
const requestForm = { ...values };
const requestForm = {
...values,
active: parseBoolean(values.active, true),
};
setSubmitting(true);
@@ -103,51 +98,34 @@ function VendorFormFormik({
};
return (
<div
className={classNames(
CLASSES.PAGE_FORM,
CLASSES.PAGE_FORM_VENDOR,
className,
)}
>
<Formik
validationSchema={
isNewMode ? CreateVendorFormSchema : EditVendorFormSchema
}
initialValues={initialFormValues}
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>
</Formik>
);
}
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,

Some files were not shown because too many files have changed in this diff Show More