feat: optimize status tags (#829)

This commit is contained in:
Ahmed Bouhuolia
2025-10-23 12:42:16 +02:00
committed by GitHub
9 changed files with 153 additions and 165 deletions

View File

@@ -1,5 +1,6 @@
import { Button, Classes } from '@blueprintjs/core'; import { Button, Classes } from '@blueprintjs/core';
import clsx from 'classnames'; import clsx from 'classnames';
import { useIsDarkMode } from '@/hooks/useDarkMode';
import { Box, BoxProps, Group } from '../Layout'; import { Box, BoxProps, Group } from '../Layout';
import { Icon } from '../Icon'; import { Icon } from '../Icon';
import styles from './Aside.module.scss'; import styles from './Aside.module.scss';
@@ -21,6 +22,7 @@ export function Aside({
classNames, classNames,
className className
}: AsideProps) { }: AsideProps) {
const isDarkMode = useIsDarkMode();
const handleClose = () => { const handleClose = () => {
onClose && onClose(); onClose && onClose();
}; };
@@ -32,7 +34,7 @@ export function Aside({
<Button <Button
aria-label="Close" aria-label="Close"
className={Classes.DIALOG_CLOSE_BUTTON} className={Classes.DIALOG_CLOSE_BUTTON}
icon={<Icon icon={'smallCross'} color={'#000'} />} icon={<Icon icon={'smallCross'} color={isDarkMode ? '#fff' : '#000'} />}
minimal={true} minimal={true}
onClick={handleClose} onClick={handleClose}
/> />

View File

@@ -4,12 +4,14 @@
--item-active-border: #88abdb; --item-active-border: #88abdb;
--item-label-text: #252a33; --item-label-text: #252a33;
--item-date-text: #5c7080; --item-date-text: #5c7080;
--item-border-hover: #c0c0c0;
:global(.bp4-dark) & { :global(.bp4-dark) & {
--item-background: var(--color-dark-gray4); --item-background: var(--color-dark-gray3);
--item-border: rgba(255, 255, 255, 0.2); --item-border: rgba(255, 255, 255, 0.2);
--item-date-text: var(--color-light-gray1); --item-date-text: var(--color-light-gray1);
--item-label-text: var(--color-light-gray4); --item-label-text: var(--color-light-gray4);
--item-border-hover: rgba(255, 255, 255, 0.45);
} }
background: var(--item-background); background: var(--item-background);
border-radius: 5px; border-radius: 5px;
@@ -27,7 +29,7 @@
} }
} }
&:hover:not(.active) { &:hover:not(.active) {
border-color: #c0c0c0; border-color: var(--item-border-hover);
} }
} }

View File

@@ -27,6 +27,7 @@ import {
} from '../withBankingActions'; } from '../withBankingActions';
import { withBanking } from '../withBanking'; import { withBanking } from '../withBanking';
import { MatchingReconcileTransactionForm } from './MatchingReconcileTransactionAside/MatchingReconcileTransactionForm'; import { MatchingReconcileTransactionForm } from './MatchingReconcileTransactionAside/MatchingReconcileTransactionForm';
import { useIsDarkMode } from '@/hooks/useDarkMode';
import styles from './CategorizeTransactionAside.module.scss'; import styles from './CategorizeTransactionAside.module.scss';
const initialValues = { const initialValues = {
@@ -119,67 +120,65 @@ const MatchingBankTransactionFormContent = R.compose(
withBanking(({ openReconcileMatchingTransaction }) => ({ withBanking(({ openReconcileMatchingTransaction }) => ({
openReconcileMatchingTransaction, openReconcileMatchingTransaction,
})), })),
)( )(({
({ // #withBanking
// #withBanking openReconcileMatchingTransaction,
openReconcileMatchingTransaction, }) => {
}) => { const {
const { isMatchingTransactionsFetching,
isMatchingTransactionsFetching, isMatchingTransactionsSuccess,
isMatchingTransactionsSuccess, matches,
matches, } = useMatchingTransactionBoot();
} = useMatchingTransactionBoot(); const [pending, setPending] = useState<null | {
const [pending, setPending] = useState<null | { refId: number;
refId: number; refType: string;
refType: string; }>(null);
}>(null);
const { setFieldValue } = useFormikContext(); const { setFieldValue } = useFormikContext();
// This effect is responsible for automatically marking a transaction as matched // This effect is responsible for automatically marking a transaction as matched
// when the matching process is successful and not currently fetching. // when the matching process is successful and not currently fetching.
useEffect(() => { useEffect(() => {
if ( if (
pending && pending &&
isMatchingTransactionsSuccess && isMatchingTransactionsSuccess &&
!isMatchingTransactionsFetching !isMatchingTransactionsFetching
) { ) {
const foundMatch = matches?.find( const foundMatch = matches?.find(
(m) => (m) =>
m.referenceType === pending?.refType && m.referenceType === pending?.refType &&
m.referenceId === pending?.refId, m.referenceId === pending?.refId,
); );
if (foundMatch) { if (foundMatch) {
setFieldValue(`matched.${pending.refType}-${pending.refId}`, true); setFieldValue(`matched.${pending.refType}-${pending.refId}`, true);
}
setPending(null);
} }
}, [ setPending(null);
isMatchingTransactionsFetching, }
isMatchingTransactionsSuccess, }, [
matches, isMatchingTransactionsFetching,
pending, isMatchingTransactionsSuccess,
setFieldValue, matches,
]); pending,
setFieldValue,
]);
const handleReconcileFormSubmitSuccess = (payload) => { const handleReconcileFormSubmitSuccess = (payload) => {
setPending({ refId: payload.id, refType: payload.type }); setPending({ refId: payload.id, refType: payload.type });
}; };
return ( return (
<> <>
<MatchingBankTransactionContent /> <MatchingBankTransactionContent />
{openReconcileMatchingTransaction && ( {openReconcileMatchingTransaction && (
<MatchingReconcileTransactionForm <MatchingReconcileTransactionForm
onSubmitSuccess={handleReconcileFormSubmitSuccess} onSubmitSuccess={handleReconcileFormSubmitSuccess}
/> />
)} )}
{!openReconcileMatchingTransaction && <MatchTransactionFooter />} {!openReconcileMatchingTransaction && <MatchTransactionFooter />}
</> </>
); );
}, });
);
function MatchingBankTransactionContent() { function MatchingBankTransactionContent() {
return ( return (
@@ -296,73 +295,76 @@ function MatchTransactionField({
); );
} }
interface MatchTransctionFooterProps extends WithBankingActionsProps {} interface MatchTransctionFooterProps extends WithBankingActionsProps { }
/** /**
* Renders the match transactions footer. * Renders the match transactions footer.
* @returns {React.ReactNode} * @returns {React.ReactNode}
*/ */
const MatchTransactionFooter = R.compose(withBankingActions)( const MatchTransactionFooter = R.compose(withBankingActions)(({
({ closeMatchingTransactionAside,
closeMatchingTransactionAside, openReconcileMatchingTransaction,
openReconcileMatchingTransaction, }: MatchTransctionFooterProps) => {
}: MatchTransctionFooterProps) => { const { submitForm, isSubmitting } = useFormikContext();
const { submitForm, isSubmitting } = useFormikContext(); const totalPending = useGetPendingAmountMatched();
const totalPending = useGetPendingAmountMatched(); const showReconcileLink = useIsShowReconcileTransactionLink();
const showReconcileLink = useIsShowReconcileTransactionLink(); const submitDisabled = totalPending !== 0;
const submitDisabled = totalPending !== 0; const isDarkMode = useIsDarkMode();
const handleCancelBtnClick = () => { const handleCancelBtnClick = () => {
closeMatchingTransactionAside(); closeMatchingTransactionAside();
}; };
const handleSubmitBtnClick = () => { const handleSubmitBtnClick = () => {
submitForm(); submitForm();
}; };
const handleReconcileTransaction = () => { const handleReconcileTransaction = () => {
openReconcileMatchingTransaction(totalPending); openReconcileMatchingTransaction(totalPending);
}; };
return ( return (
<Box className={styles.footer}> <Box className={styles.footer}>
<Box className={styles.footerTotal}> <Box className={styles.footerTotal}>
<Group position={'apart'}> <Group position={'apart'}>
{showReconcileLink && ( {showReconcileLink && (
<AnchorButton <AnchorButton
small small
minimal minimal
intent={Intent.PRIMARY}
onClick={handleReconcileTransaction}
>
Add Reconcile Transaction +
</AnchorButton>
)}
<Text
style={{ fontSize: 14, marginLeft: 'auto', color: '#404854' }}
tagName="span"
>
Pending <FormatNumber value={totalPending} currency={'USD'} />
</Text>
</Group>
</Box>
<Box className={styles.footerActions}>
<Group spacing={10}>
<Button
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
style={{ minWidth: 85 }} onClick={handleReconcileTransaction}
onClick={handleSubmitBtnClick}
loading={isSubmitting}
disabled={submitDisabled}
> >
Match Add Reconcile Transaction +
</Button> </AnchorButton>
)}
<Button onClick={handleCancelBtnClick}>Cancel</Button> <Text
</Group> style={{
</Box> fontSize: 14,
marginLeft: 'auto',
color: isDarkMode ? 'var(--color-light-gray1)' : '#404854',
}}
tagName="span"
>
Pending <FormatNumber value={totalPending} currency={'USD'} />
</Text>
</Group>
</Box> </Box>
);
}, <Box className={styles.footerActions}>
); <Group spacing={10}>
<Button
intent={Intent.PRIMARY}
style={{ minWidth: 85 }}
onClick={handleSubmitBtnClick}
loading={isSubmitting}
disabled={submitDisabled}
>
Match
</Button>
<Button onClick={handleCancelBtnClick}>Cancel</Button>
</Group>
</Box>
</Box>
);
});
MatchTransactionFooter.displayName = 'MatchTransactionFooter'; MatchTransactionFooter.displayName = 'MatchTransactionFooter';

View File

@@ -7,7 +7,6 @@ import {
MenuItem, MenuItem,
MenuDivider, MenuDivider,
Tag, Tag,
ProgressBar,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import clsx from 'classnames'; import clsx from 'classnames';
import { import {
@@ -21,7 +20,6 @@ import {
import { import {
formattedAmount, formattedAmount,
safeCallback, safeCallback,
calculateStatus,
} from '@/utils'; } from '@/utils';
import { import {
BillAction, BillAction,
@@ -108,42 +106,35 @@ export function StatusAccessor(bill) {
<div className={'status-accessor'}> <div className={'status-accessor'}>
<Choose> <Choose>
<Choose.When condition={bill.is_fully_paid && bill.is_open}> <Choose.When condition={bill.is_fully_paid && bill.is_open}>
<span className={'fully-paid-icon'}> <Tag round intent={Intent.SUCCESS}>
<Icon icon="small-tick" iconSize={18} />
</span>
<span class="fully-paid-text">
<T id={'paid'} /> <T id={'paid'} />
</span> </Tag>
</Choose.When> </Choose.When>
<Choose.When condition={bill.is_open}> <Choose.When condition={bill.is_open}>
<Choose> <Choose>
<Choose.When condition={bill.is_overdue}> <Choose.When condition={bill.is_overdue}>
<span className={'overdue-status'}> <Tag round intent={Intent.DANGER}>
{intl.get('overdue_by', { overdue: bill.overdue_days })} {intl.get('overdue_by', { overdue: bill.overdue_days })}
</span> </Tag>
</Choose.When> </Choose.When>
<Choose.Otherwise> <Choose.Otherwise>
<span className={'due-status'}> <Tag round intent={Intent.WARNING}>
{intl.get('due_in', { due: bill.remaining_days })} {intl.get('due_in', { due: bill.remaining_days })}
</span> </Tag>
</Choose.Otherwise> </Choose.Otherwise>
</Choose> </Choose>
<If condition={bill.is_partially_paid}> <If condition={bill.is_partially_paid}>
<span className="partial-paid"> <Tag round intent={Intent.PRIMARY}>
{intl.get('day_partially_paid', { {intl.get('day_partially_paid', {
due: formattedAmount(bill.due_amount, bill.currency_code), due: formattedAmount(bill.due_amount, bill.currency_code),
})} })}
</span> </Tag>
<ProgressBar
animate={false}
stripes={false}
intent={Intent.PRIMARY}
value={calculateStatus(bill.balance, bill.amount)}
/>
</If> </If>
</Choose.When> </Choose.When>
<Choose.Otherwise> <Choose.Otherwise>
<Tag minimal={true} round={true}> <Tag round>
<T id={'draft'} /> <T id={'draft'} />
</Tag> </Tag>
</Choose.Otherwise> </Choose.Otherwise>

View File

@@ -81,19 +81,19 @@ export function StatusAccessor(creditNote) {
<div> <div>
<Choose> <Choose>
<Choose.When condition={creditNote.is_open}> <Choose.When condition={creditNote.is_open}>
<Tag minimal={true} intent={Intent.WARNING} round={true}> <Tag intent={Intent.WARNING} round>
<T id={'open'} /> <T id={'open'} />
</Tag> </Tag>
</Choose.When> </Choose.When>
<Choose.When condition={creditNote.is_closed}> <Choose.When condition={creditNote.is_closed}>
<Tag minimal={true} intent={Intent.SUCCESS} round={true}> <Tag intent={Intent.SUCCESS} round>
<T id={'closed'} /> <T id={'closed'} />
</Tag> </Tag>
</Choose.When> </Choose.When>
<Choose.When condition={creditNote.is_draft}> <Choose.When condition={creditNote.is_draft}>
<Tag minimal={true} round={true}> <Tag round>
<T id={'draft'} /> <T id={'draft'} />
</Tag> </Tag>
</Choose.When> </Choose.When>

View File

@@ -80,19 +80,19 @@ export function StatusAccessor(creditNote) {
<div> <div>
<Choose> <Choose>
<Choose.When condition={creditNote.is_open}> <Choose.When condition={creditNote.is_open}>
<Tag intent={Intent.WARNING} minimal={true} round={true}> <Tag intent={Intent.WARNING} round>
<T id={'open'} /> <T id={'open'} />
</Tag> </Tag>
</Choose.When> </Choose.When>
<Choose.When condition={creditNote.is_closed}> <Choose.When condition={creditNote.is_closed}>
<Tag intent={Intent.SUCCESS} minimal={true} round={true}> <Tag intent={Intent.SUCCESS} round>
<T id={'closed'} /> <T id={'closed'} />
</Tag> </Tag>
</Choose.When> </Choose.When>
<Choose.When condition={creditNote.is_draft}> <Choose.When condition={creditNote.is_draft}>
<Tag intent={Intent.NONE} minimal={true} round={true}> <Tag intent={Intent.NONE} round>
<T id={'draft'} /> <T id={'draft'} />
</Tag> </Tag>
</Choose.When> </Choose.When>

View File

@@ -6,7 +6,6 @@ import {
Menu, Menu,
MenuItem, MenuItem,
MenuDivider, MenuDivider,
ProgressBar,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import clsx from 'classnames'; import clsx from 'classnames';
@@ -20,7 +19,7 @@ import {
Icon, Icon,
Can, Can,
} from '@/components'; } from '@/components';
import { formattedAmount, safeCallback, calculateStatus } from '@/utils'; import { formattedAmount, safeCallback } from '@/utils';
import { import {
SaleInvoiceAction, SaleInvoiceAction,
PaymentReceiveAction, PaymentReceiveAction,
@@ -31,44 +30,36 @@ export function InvoiceStatus({ invoice }) {
return ( return (
<Choose> <Choose>
<Choose.When condition={invoice.is_fully_paid && invoice.is_delivered}> <Choose.When condition={invoice.is_fully_paid && invoice.is_delivered}>
<span className={'fully-paid-icon'}> <Tag intent={Intent.SUCCESS} round>
<Icon icon="small-tick" iconSize={18} />
</span>
<span class="fully-paid-text">
<T id={'paid'} /> <T id={'paid'} />
</span> </Tag>
</Choose.When> </Choose.When>
<Choose.When condition={invoice.is_delivered}> <Choose.When condition={invoice.is_delivered}>
<Choose> <Choose>
<Choose.When condition={invoice.is_overdue}> <Choose.When condition={invoice.is_overdue}>
<span className={'overdue-status'}> <Tag intent={Intent.DANGER} round>
{intl.get('overdue_by', { overdue: invoice.overdue_days })} {intl.get('overdue_by', { overdue: invoice.overdue_days })}
</span> </Tag>
</Choose.When> </Choose.When>
<Choose.Otherwise> <Choose.Otherwise>
<span className={'due-status'}> <Tag intent={Intent.WARNING} round>
{intl.get('due_in', { due: invoice.remaining_days })} {intl.get('due_in', { due: invoice.remaining_days })}
</span> </Tag>
</Choose.Otherwise> </Choose.Otherwise>
</Choose> </Choose>
<If condition={invoice.is_partially_paid}> <If condition={invoice.is_partially_paid}>
<span class="partial-paid"> <Tag intent={Intent.PRIMARY} round>
{intl.get('day_partially_paid', { {intl.get('day_partially_paid', {
due: formattedAmount(invoice.due_amount, invoice.currency_code), due: formattedAmount(invoice.due_amount, invoice.currency_code),
})} })}
</span> </Tag>
<ProgressBar
animate={false}
stripes={false}
intent={Intent.PRIMARY}
value={calculateStatus(invoice.balance_amount, invoice.balance)}
/>
</If> </If>
</Choose.When> </Choose.When>
<Choose.Otherwise> <Choose.Otherwise>
<Tag minimal={true} round={true}> <Tag round>
<T id={'draft'} /> <T id={'draft'} />
</Tag> </Tag>
</Choose.Otherwise> </Choose.Otherwise>

View File

@@ -96,13 +96,13 @@ export function StatusAccessor(receipt) {
return ( return (
<Choose> <Choose>
<Choose.When condition={receipt.is_closed}> <Choose.When condition={receipt.is_closed}>
<Tag minimal={true} intent={Intent.SUCCESS} round={true}> <Tag intent={Intent.SUCCESS} round>
<T id={'closed'} /> <T id={'closed'} />
</Tag> </Tag>
</Choose.When> </Choose.When>
<Choose.Otherwise> <Choose.Otherwise>
<Tag minimal={true} intent={Intent.WARNING} round={true}> <Tag intent={Intent.WARNING} round>
<T id={'draft'} /> <T id={'draft'} />
</Tag> </Tag>
</Choose.Otherwise> </Choose.Otherwise>

View File

@@ -572,7 +572,7 @@ body.bp4-dark {
// Aside // Aside
--color-aside-background: #fff; --color-aside-background: #fff;
--color-aside-background: var(--color-dark-gray2); --color-aside-background: var(--color-dark-gray1);
--color-aside-title-background: #fff; --color-aside-title-background: #fff;
--color-aside-title-background: var(--color-dark-gray1); --color-aside-title-background: var(--color-dark-gray1);