mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 06:40:31 +00:00
feat: project billable entries dialog
This commit is contained in:
@@ -0,0 +1,132 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React from 'react';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
import { Menu, MenuItem } from '@blueprintjs/core';
|
||||||
|
import { Suggest } from '@blueprintjs/select';
|
||||||
|
import { FormattedMessage as T } from '@/components';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { CLASSES } from '@/constants/classes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {*} billableType
|
||||||
|
* @param {*} param1
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
*/
|
||||||
|
const billableTypeItemRenderer = (
|
||||||
|
billableType,
|
||||||
|
{ handleClick, modifiers, query },
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
disabled={modifiers.disabled}
|
||||||
|
key={billableType.id}
|
||||||
|
text={billableType.name}
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} query
|
||||||
|
* @param {*} billableType
|
||||||
|
* @param {*} _index
|
||||||
|
* @param {*} exactMatch
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const billableTypeItemPredicate = (query, billableType, _index, exactMatch) => {
|
||||||
|
const normalizedTitle = billableType.name.toLowerCase();
|
||||||
|
const normalizedQuery = query.toLowerCase();
|
||||||
|
|
||||||
|
if (exactMatch) {
|
||||||
|
return normalizedTitle === normalizedQuery;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
`${billableType.name}. ${normalizedTitle}`.indexOf(normalizedQuery) >= 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param inputValue
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const billableTypeInputValueRenderer = (inputValue) => {
|
||||||
|
if (inputValue) {
|
||||||
|
return inputValue.name.toString();
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project billable suggest field.
|
||||||
|
* @param
|
||||||
|
*/
|
||||||
|
export function ProjectBillableSuggestField({
|
||||||
|
billableType,
|
||||||
|
initialBillableTypeId,
|
||||||
|
selectedBillableTypeId,
|
||||||
|
|
||||||
|
defautlSelectText = 'Placeholder Type...',
|
||||||
|
onBillableTypeSelected,
|
||||||
|
popoverFill = false,
|
||||||
|
|
||||||
|
...suggestProps
|
||||||
|
}) {
|
||||||
|
const initialBillableType = React.useMemo(
|
||||||
|
() => billableType.find((b) => b.id === initialBillableTypeId),
|
||||||
|
[initialBillableTypeId, billableType],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [selectedBillableType, setSelectedBillableType] = React.useState(
|
||||||
|
initialBillableType || null,
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (typeof selectedBillableTypeId !== 'undefined') {
|
||||||
|
const billableType = selectedBillableTypeId
|
||||||
|
? billableType.find((a) => a.id === selectedBillableTypeId)
|
||||||
|
: null;
|
||||||
|
setSelectedBillableType(billableType);
|
||||||
|
}
|
||||||
|
}, [selectedBillableTypeId, billableType, setSelectedBillableType]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} billableType
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const billableTypetemSelect = React.useCallback(
|
||||||
|
(billableType) => {
|
||||||
|
if (billableType.id) {
|
||||||
|
setSelectedBillableType({ ...billableType });
|
||||||
|
onBillableTypeSelected && onBillableTypeSelected(billableType);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setSelectedBillableType, onBillableTypeSelected],
|
||||||
|
);
|
||||||
|
|
||||||
|
const billableTypeSelectProps = {
|
||||||
|
itemRenderer: billableTypeItemRenderer,
|
||||||
|
itemPredicate: billableTypeItemPredicate,
|
||||||
|
inputValueRenderer: billableTypeInputValueRenderer,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Suggest
|
||||||
|
items={billableType}
|
||||||
|
selectedItem={selectedBillableType}
|
||||||
|
onItemSelect={billableTypetemSelect}
|
||||||
|
inputProps={{ placeholder: defautlSelectText }}
|
||||||
|
fill={true}
|
||||||
|
resetOnClose={true}
|
||||||
|
popoverProps={{ minimal: true, boundary: 'window' }}
|
||||||
|
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
|
||||||
|
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
|
||||||
|
})}
|
||||||
|
{...billableTypeSelectProps}
|
||||||
|
{...suggestProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -48,15 +48,13 @@ export function ProjectSuggestField({
|
|||||||
*/
|
*/
|
||||||
const projectsItemRenderer = (project, { handleClick, modifiers, query }) => {
|
const projectsItemRenderer = (project, { handleClick, modifiers, query }) => {
|
||||||
return (
|
return (
|
||||||
<MenuContent>
|
<MenuItem
|
||||||
<MenuItem
|
icon={<AvatarSelect text={project} />}
|
||||||
icon={<AvatarSelect text={project} />}
|
disabled={modifiers.disabled}
|
||||||
disabled={modifiers.disabled}
|
key={project.id}
|
||||||
key={project.id}
|
text={project.name}
|
||||||
text={project.name}
|
onClick={handleClick}
|
||||||
onClick={handleClick}
|
/>
|
||||||
/>
|
|
||||||
</MenuContent>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -133,20 +131,14 @@ const AvatarSelect = ({ text }) => {
|
|||||||
return <AvaterContent>{firstLettersArgs(text?.name)}</AvaterContent>;
|
return <AvaterContent>{firstLettersArgs(text?.name)}</AvaterContent>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MenuContent = styled(Menu)`
|
|
||||||
.bp3-menu {
|
|
||||||
margin: 5px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const AvaterContent = styled.div`
|
const AvaterContent = styled.div`
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background: #adbcc9;
|
background: #adbcc9;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
height: 25px;
|
height: 22px;
|
||||||
width: 25px;
|
width: 22px;
|
||||||
line-height: 25px;
|
line-height: 22px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ export * from './TaskSelect';
|
|||||||
export * from './ProjectsSelect';
|
export * from './ProjectsSelect';
|
||||||
export * from './ProjectMultiSelect'
|
export * from './ProjectMultiSelect'
|
||||||
export * from './FInputGroupComponent';
|
export * from './FInputGroupComponent';
|
||||||
export * from './ProjectSuggestField'
|
export * from './ProjectSuggestField'
|
||||||
|
export * from './ProjectBillableTypeSuggestField'
|
||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
FieldRequiredHint,
|
FieldRequiredHint,
|
||||||
FormattedMessage as T,
|
FormattedMessage as T,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
|
import { ProjectBillableSuggestField } from '../../components';
|
||||||
|
import { billableTypeOption } from '../common';
|
||||||
import { ProjectRowDivider, ProjectEntiresBox } from './components';
|
import { ProjectRowDivider, ProjectEntiresBox } from './components';
|
||||||
import { useProjectBillableEntriesFormContext } from './ProjectBillableEntriesFormProvider';
|
import { useProjectBillableEntriesFormContext } from './ProjectBillableEntriesFormProvider';
|
||||||
|
|
||||||
@@ -20,8 +22,6 @@ export default function ProjectBillableEntriesFormFields() {
|
|||||||
// Formik context.
|
// Formik context.
|
||||||
const { values } = useFormikContext();
|
const { values } = useFormikContext();
|
||||||
|
|
||||||
const { billableEntries } = useProjectBillableEntriesFormContext();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={Classes.DIALOG_BODY}>
|
<div className={Classes.DIALOG_BODY}>
|
||||||
{/*------------ Filter by Date -----------*/}
|
{/*------------ Filter by Date -----------*/}
|
||||||
@@ -37,14 +37,14 @@ export default function ProjectBillableEntriesFormFields() {
|
|||||||
|
|
||||||
{/*------------ Filter by Type -----------*/}
|
{/*------------ Filter by Type -----------*/}
|
||||||
<FFormGroup
|
<FFormGroup
|
||||||
name={'type'}
|
name={'billableType'}
|
||||||
label={<T id={'project_billable_entries.dialog.filter_by_type'} />}
|
label={<T id={'project_billable_entries.dialog.filter_by_type'} />}
|
||||||
labelInfo={<FieldRequiredHint />}
|
labelInfo={<FieldRequiredHint />}
|
||||||
>
|
>
|
||||||
<FInputGroup name="type" placeholder={'Placeholder Type'} />
|
<ProjectBillableSuggestField billableType={billableTypeOption} />
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
|
|
||||||
<ProjectEntiresBox billableEntries={billableEntries} />
|
<ProjectEntiresBox billableEntries={[]} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useProjectBillableEntries } from '../../hooks';
|
|
||||||
import { DialogContent } from '@/components';
|
import { DialogContent } from '@/components';
|
||||||
|
|
||||||
const ProjectBillableEntriesFormContext = React.createContext();
|
const ProjectBillableEntriesFormContext = React.createContext();
|
||||||
@@ -16,31 +15,14 @@ function ProjectBillableEntriesFormProvider({
|
|||||||
projectId,
|
projectId,
|
||||||
...props
|
...props
|
||||||
}) {
|
}) {
|
||||||
// Handle fetch project billable entries.
|
|
||||||
const {
|
|
||||||
data: { billableEntries },
|
|
||||||
isLoading: isProjectBillableEntriesLoading,
|
|
||||||
} = useProjectBillableEntries(
|
|
||||||
projectId,
|
|
||||||
{
|
|
||||||
billable_type: 'expense',
|
|
||||||
to_date: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!projectId,
|
|
||||||
keepPreviousData: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
//state provider.
|
//state provider.
|
||||||
const provider = {
|
const provider = {
|
||||||
dialogName,
|
dialogName,
|
||||||
projectId,
|
projectId,
|
||||||
billableEntries,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogContent isLoading={isProjectBillableEntriesLoading}>
|
<DialogContent>
|
||||||
<ProjectBillableEntriesFormContext.Provider value={provider} {...props} />
|
<ProjectBillableEntriesFormContext.Provider value={provider} {...props} />
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import intl from 'react-intl-universal';
|
|||||||
export const taskChargeOptions = [
|
export const taskChargeOptions = [
|
||||||
{ name: intl.get('project_task.dialog.hourly_rate'), value: 'TIME' },
|
{ name: intl.get('project_task.dialog.hourly_rate'), value: 'TIME' },
|
||||||
{ name: intl.get('project_task.dialog.fixed_price'), value: 'FIXED' },
|
{ name: intl.get('project_task.dialog.fixed_price'), value: 'FIXED' },
|
||||||
{ name: intl.get('project_task.dialog.non_chargeable'), value: 'NON_CHARGABLE' },
|
{
|
||||||
|
name: intl.get('project_task.dialog.non_chargeable'),
|
||||||
|
value: 'NON_CHARGABLE',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const expenseChargeOption = [
|
export const expenseChargeOption = [
|
||||||
@@ -16,3 +19,12 @@ export const expenseChargeOption = [
|
|||||||
{ name: intl.get('expenses.dialog.custom_pirce'), value: 'custom_pirce' },
|
{ name: intl.get('expenses.dialog.custom_pirce'), value: 'custom_pirce' },
|
||||||
{ name: intl.get('expenses.dialog.non_chargeable'), value: 'non_chargeable' },
|
{ name: intl.get('expenses.dialog.non_chargeable'), value: 'non_chargeable' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const billableTypeOption = [
|
||||||
|
{ name: intl.get('project_billable_entries.dialog.task'), value: 'Task' },
|
||||||
|
{ name: intl.get('project_billable_entries.dialog.bill'), value: 'Bill' },
|
||||||
|
{
|
||||||
|
name: intl.get('project_billable_entries.dialog.expense'),
|
||||||
|
value: 'Expense',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import t from './type';
|
|||||||
*/
|
*/
|
||||||
export function useProjectBillableEntries(projectId, query, props) {
|
export function useProjectBillableEntries(projectId, query, props) {
|
||||||
return useRequestQuery(
|
return useRequestQuery(
|
||||||
[t.PROJECT_BILLABLE_ENTRIES, projectId],
|
[t.PROJECT_BILLABLE_ENTRIES, projectId, query],
|
||||||
{
|
{
|
||||||
method: 'get',
|
method: 'get',
|
||||||
url: `projects/${projectId}/billable/entries`,
|
url: `projects/${projectId}/billable/entries`,
|
||||||
@@ -20,9 +20,7 @@ export function useProjectBillableEntries(projectId, query, props) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
select: (res) => res.data.billable_entries,
|
select: (res) => res.data.billable_entries,
|
||||||
defaultData: {
|
defaultData: {},
|
||||||
billableEntries: [],
|
|
||||||
},
|
|
||||||
...props,
|
...props,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2212,5 +2212,8 @@
|
|||||||
"project_invoicing.dialog.bill_to": "Bill To",
|
"project_invoicing.dialog.bill_to": "Bill To",
|
||||||
"project_billable_entries.dialog.label": "Add Project Entries",
|
"project_billable_entries.dialog.label": "Add Project Entries",
|
||||||
"project_billable_entries.dialog.filter_by_date": "Filter by Date",
|
"project_billable_entries.dialog.filter_by_date": "Filter by Date",
|
||||||
"project_billable_entries.dialog.filter_by_type":"Filter by Type"
|
"project_billable_entries.dialog.filter_by_type": "Filter by Type",
|
||||||
|
"project_billable_entries.dialog.expense": "Expense",
|
||||||
|
"project_billable_entries.dialog.task": "Task",
|
||||||
|
"project_billable_entries.dialog.bill": "Bill"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user