Compare commits

...

11 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
a2160c0595 Merge pull request #939 from bigcapitalhq/fix/ahmedbouhuolia/docker-healthcheck-endpoint
fix(server): fix Docker healthcheck endpoint
2026-02-10 00:25:25 +02:00
Ahmed Bouhuolia
956a9b58dd fix(server): register SystemDatabaseController and add PublicRoute decorator
- Register SystemDatabaseController in SystemDatabaseModule to expose /api/system_db endpoint
- Add PublicRoute decorator to bypass authentication for healthcheck endpoint
- Update ping() method to return { status: 'ok' } with HTTP 200

This fixes the Docker healthcheck that was failing with 404 Not Found errors.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-10 00:22:40 +02:00
Ahmed Bouhuolia
acb701d618 Merge pull request #938 from bigcapitalhq/fix/universal-search-api-endpoints
refactor: update UniversalSearch components with TypeScript and TextStatus
2026-02-09 19:55:37 +02:00
Ahmed Bouhuolia
09ff72d302 fix: add TypeScript types to If component 2026-02-09 19:52:17 +02:00
Ahmed Bouhuolia
7375512fec refactor: update UniversalSearch components with TypeScript and TextStatus 2026-02-09 19:26:26 +02:00
Ahmed Bouhuolia
77e65389a4 Merge pull request #937 from bigcapitalhq/fix/universal-search-api-endpoints
fix: universal search API endpoint errors
2026-02-09 13:42:53 +02:00
Ahmed Bouhuolia
1972861c97 fix(server): add missing searchRoles to Item model
Add searchRoles static property to enable searching items by name and code.
This fixes the 500 Internal Server Error when searching items via
/api/items?search_keyword=...

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-09 13:40:34 +02:00
Ahmed Bouhuolia
c47acdee03 fix(webapp): correct API endpoint URLs for universal search
Update resource URL mappings to match backend NestJS controller routes:
- /sales/invoices -> /sale-invoices
- /sales/estimates -> /sale-estimates
- /sales/receipts -> /sale-receipts
- /purchases/bills -> /bills
- /sales/payment_receives -> /payments-received
- /purchases/bill_payments -> /bill-payments
- /sales/credit_notes -> /credit-notes
- /purchases/vendor-credit -> /vendor-credits

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-09 13:40:18 +02:00
Ahmed Bouhuolia
8689962bf3 Merge pull request #935 from bigcapitalhq/feat/abouolia/add-throttle-organization-build-job
feat: expand rate limiting of getting org build job endpoint
2026-02-09 13:23:49 +02:00
Ahmed Bouhuolia
3258159474 feat: add rate limiting to organization build job endpoint
Add @Throttle decorator to GET /build/:buildJobId endpoint to limit
to 300 requests per minute to prevent abuse.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-08 09:39:55 +02:00
Ahmed Bouhuolia
36bfa573ad 🐛 fix(manual-journal): fix race condition in form submission handlers
Fix the order of setSubmitPayload and submitForm calls in all six
button handlers to prevent race condition where submitForm reads
stale state before setSubmitPayload updates it.

Changes:
- handleSubmitPublishBtnClick
- handleSubmitPublishAndNewBtnClick
- handleSubmitPublishContinueEditingBtnClick
- handleSubmitDraftBtnClick
- handleSubmitDraftAndNewBtnClick
- handleSubmitDraftContinueEditingBtnClick

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-02-07 16:58:57 +02:00
17 changed files with 528 additions and 346 deletions

View File

@@ -70,6 +70,16 @@ export class Item extends TenantBaseModel {
};
}
/**
* Model search roles.
*/
static get searchRoles() {
return [
{ condition: 'or', fieldKey: 'name', comparator: 'contains' },
{ condition: 'or', fieldKey: 'code', comparator: 'like' },
];
}
/**
* Relationship mapping.
*/

View File

@@ -17,6 +17,7 @@ import {
HttpCode,
Param,
} from '@nestjs/common';
import { Throttle } from '@nestjs/throttler';
import { BuildOrganizationService } from './commands/BuildOrganization.service';
import {
BuildOrganizationDto,
@@ -50,7 +51,7 @@ export class OrganizationController {
private readonly updateOrganizationService: UpdateOrganizationService,
private readonly getBuildOrganizationJobService: GetBuildOrganizationBuildJob,
private readonly orgBaseCurrencyLockingService: OrganizationBaseCurrencyLocking,
) { }
) {}
@Post('build')
@HttpCode(200)
@@ -77,6 +78,7 @@ export class OrganizationController {
}
@Get('build/:buildJobId')
@Throttle({ default: { limit: 300, ttl: 60000 } }) // 300 req/min
@ApiParam({
name: 'buildJobId',
required: true,

View File

@@ -1,12 +1,14 @@
import { Controller, Get, Post } from '@nestjs/common';
import { Controller, Get, HttpCode } from '@nestjs/common';
import { PublicRoute } from '@/modules/Auth/guards/jwt.guard';
@Controller('/system_db')
@Controller('system_db')
@PublicRoute()
export class SystemDatabaseController {
constructor() {}
@Post()
@Get()
ping(){
@HttpCode(200)
ping() {
return { status: 'ok' };
}
}

View File

@@ -6,6 +6,7 @@ import {
SystemKnexConnectionConfigure,
} from './SystemDB.constants';
import { knexSnakeCaseMappers } from 'objection';
import { SystemDatabaseController } from './SystemDB.controller';
const providers = [
{
@@ -42,6 +43,7 @@ const providers = [
@Global()
@Module({
controllers: [SystemDatabaseController],
providers: [...providers],
exports: [...providers],
})

View File

@@ -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' &&

View File

@@ -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>
);

View File

@@ -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;
};

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;

View File

@@ -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.

View File

@@ -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>
);

View File

@@ -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>
);

View File

@@ -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'}
/>
);
}

View File

@@ -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>
);

View File

@@ -32,19 +32,19 @@ export function useResourceData(type, query, props) {
*/
function getResourceUrlFromType(type) {
const config = {
[RESOURCES_TYPES.INVOICE]: '/sales/invoices',
[RESOURCES_TYPES.ESTIMATE]: '/sales/estimates',
[RESOURCES_TYPES.INVOICE]: '/sale-invoices',
[RESOURCES_TYPES.ESTIMATE]: '/sale-estimates',
[RESOURCES_TYPES.ITEM]: '/items',
[RESOURCES_TYPES.RECEIPT]: '/sales/receipts',
[RESOURCES_TYPES.BILL]: '/purchases/bills',
[RESOURCES_TYPES.PAYMENT_RECEIVE]: '/sales/payment_receives',
[RESOURCES_TYPES.PAYMENT_MADE]: '/purchases/bill_payments',
[RESOURCES_TYPES.RECEIPT]: '/sale-receipts',
[RESOURCES_TYPES.BILL]: '/bills',
[RESOURCES_TYPES.PAYMENT_RECEIVE]: '/payments-received',
[RESOURCES_TYPES.PAYMENT_MADE]: '/bill-payments',
[RESOURCES_TYPES.CUSTOMER]: '/customers',
[RESOURCES_TYPES.VENDOR]: '/vendors',
[RESOURCES_TYPES.MANUAL_JOURNAL]: '/manual-journals',
[RESOURCES_TYPES.ACCOUNT]: '/accounts',
[RESOURCES_TYPES.CREDIT_NOTE]: '/sales/credit_notes',
[RESOURCES_TYPES.VENDOR_CREDIT]: '/purchases/vendor-credit',
[RESOURCES_TYPES.CREDIT_NOTE]: '/credit-notes',
[RESOURCES_TYPES.VENDOR_CREDIT]: '/vendor-credits',
};
return config[type] || '';
}

View File

@@ -31,7 +31,6 @@
@import 'components/Overlay';
@import 'components/Menu';
@import 'components/SidebarOverlay';
@import 'components/UniversalSearch';
// Pages
@import 'pages/view-form';

View File

@@ -7,6 +7,27 @@ $ns: bp4;
--color-primary: #8abbff;
--color-danger: red;
// Green colors
--color-green-500: #165a36;
--color-green-400: #1c6e42;
--color-green-300: #238551;
--color-green-200: #32a467;
--color-green-100: #72ca9b;
// Red colors
--color-red-500: #8e292c;
--color-red-400: #ac2f33;
--color-red-300: #cd4246;
--color-red-200: #e76a6e;
--color-red-100: #fa999c;
// Orange colors
--color-orange-500: #77450d;
--color-orange-400: #935610;
--color-orange-300: #c87619;
--color-orange-200: #ec9a3c;
--color-orange-100: #fbb360;
--color-dark-gray5: #404854;
--color-dark-gray4: #383e47;
--color-dark-gray3: #2f343c;
@@ -301,6 +322,27 @@ body.bp4-dark {
--color-primary: #8abbff;
--color-danger: rgb(213, 103, 103);
// Green colors (dark mode - lighter variants)
--color-green-500: #72ca9b;
--color-green-400: #32a467;
--color-green-300: #238551;
--color-green-200: #1c6e42;
--color-green-100: #165a36;
// Red colors (dark mode - lighter variants)
--color-red-500: #fa999c;
--color-red-400: #e76a6e;
--color-red-300: #cd4246;
--color-red-200: #ac2f33;
--color-red-100: #8e292c;
// Orange colors (dark mode - lighter variants)
--color-orange-500: #fbb360;
--color-orange-400: #ec9a3c;
--color-orange-300: #c87619;
--color-orange-200: #935610;
--color-orange-100: #77450d;
--color-dark-gray5: #404854;
--color-dark-gray4: #383e47;
--color-dark-gray3: #2f343c;

View File

@@ -1,200 +0,0 @@
.universal-search {
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-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-delay: 0;
transition-duration: 0.2s;
transition-property: filter, opacity;
transition-timing-function: 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-delay: 0;
transition-duration: 0.2s;
transition-property: filter, opacity;
transition-timing-function: cubic-bezier(0.4, 1, 0.75, 0.9);
}
&__omnibar {
.bp4-input-group {
.bp4-icon {
svg {
stroke: currentColor;
fill: none;
fill-rule: evenodd;
stroke-linecap: round;
stroke-linejoin: round;
}
}
}
.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-input-group {
.bp4-icon {
margin: 16px;
color: var(--color-universal-search-icon);
svg {
stroke-width: 2;
--text-opacity: 1;
}
}
}
.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: #8499a7;
}
}
&.bp4-intent-primary {
&.bp4-active {
background-color: rgb(235, 241, 246);
color: #252b30;
.bp4-menu-item-label {
color: #5c7080;
}
}
}
&-label {
flex-direction: row;
text-align: right;
}
}
}
.bp4-input-action {
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
}
}
&__type-select-overlay {
.bp4-button {
margin: 0 !important;
}
}
&__footer {
padding: 12px 12px;
border-top: 1px solid var(--color-universal-search-footer-divider);
}
&__actions {
display: flex;
}
&__action {
&:not(:first-of-type) {
margin-left: 14px;
}
.bp4-tag {
background: var(--color-universal-search-tag-background);
color: var(--color-universal-search-tag-text);
}
&--arrows {
.bp4-tag {
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;
}
}
}
.text {
margin-left: 6px;
}
}
&__footer {
}
&-input-right-elements {
display: flex;
margin: 10px;
.bp4-spinner {
margin-right: 6px;
}
}
&__item {
&--invoice,
&--estimate,
&--bill,
&--receipt {
.amount {
color: #252b30;
}
.status {
font-size: 13px;
&.status-warning {
color: rgb(236, 91, 10);
}
&.status-success {
color: #249017;
}
}
}
}
}