mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-21 07:10:33 +00:00
feat: project detail.
This commit is contained in:
@@ -31,7 +31,7 @@ const chargeTypeSelectProps = {
|
|||||||
* @param param0
|
* @param param0
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function ChangeTypesSelect({ items, ...rest }) {
|
export function ProjectTaskChargeTypeSelect({ items, ...rest }) {
|
||||||
return (
|
return (
|
||||||
<FSelect
|
<FSelect
|
||||||
{...chargeTypeSelectProps}
|
{...chargeTypeSelectProps}
|
||||||
@@ -48,7 +48,12 @@ const taskSelectProps = {
|
|||||||
labelAccessor: 'name',
|
labelAccessor: 'name',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function TaskSelect({ tasks, ...rest }) {
|
/**
|
||||||
|
*
|
||||||
|
* @param param0
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function ProjectTaskSelect({ tasks, ...rest }) {
|
||||||
return (
|
return (
|
||||||
<FSelect
|
<FSelect
|
||||||
items={tasks}
|
items={tasks}
|
||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
ExpenseSelect,
|
ExpenseSelect,
|
||||||
FInputGroupComponent,
|
FInputGroupComponent,
|
||||||
ChangeTypesSelect,
|
ProjectTaskChargeTypeSelect,
|
||||||
} from '../../components';
|
} from '../../components';
|
||||||
import { useEstimatedExpenseFormContext } from './EstimatedExpenseFormProvider';
|
import { useEstimatedExpenseFormContext } from './EstimatedExpenseFormProvider';
|
||||||
import EstimatedExpenseFormChargeFields from './EstimatedExpenseFormChargeFields';
|
import EstimatedExpenseFormChargeFields from './EstimatedExpenseFormChargeFields';
|
||||||
@@ -75,7 +75,7 @@ export default function EstimatedExpenseFormFields() {
|
|||||||
label={<T id={'estimated_expenses.dialog.charge'} />}
|
label={<T id={'estimated_expenses.dialog.charge'} />}
|
||||||
className={classNames('form-group--select-list', Classes.FILL)}
|
className={classNames('form-group--select-list', Classes.FILL)}
|
||||||
>
|
>
|
||||||
<ChangeTypesSelect
|
<ProjectTaskChargeTypeSelect
|
||||||
name="charge"
|
name="charge"
|
||||||
items={expenseChargeOption}
|
items={expenseChargeOption}
|
||||||
popoverProps={{ minimal: true }}
|
popoverProps={{ minimal: true }}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { DashboardInsider } from '@/components';
|
import { DashboardInsider } from '@/components';
|
||||||
|
import { useProject } from '../../hooks';
|
||||||
|
|
||||||
const ProjectDetailContext = React.createContext();
|
const ProjectDetailContext = React.createContext();
|
||||||
|
|
||||||
@@ -13,12 +14,18 @@ function ProjectDetailProvider({
|
|||||||
// #ownProps
|
// #ownProps
|
||||||
...props
|
...props
|
||||||
}) {
|
}) {
|
||||||
|
// Handle fetch project detail.
|
||||||
|
const { data: project, isLoading: isProjectLoading } = useProject(projectId, {
|
||||||
|
enabled: !!projectId,
|
||||||
|
});
|
||||||
|
|
||||||
// State provider.
|
// State provider.
|
||||||
const provider = {
|
const provider = {
|
||||||
|
project,
|
||||||
projectId,
|
projectId,
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<DashboardInsider className="timesheets">
|
<DashboardInsider loading={isProjectLoading}>
|
||||||
<ProjectDetailContext.Provider value={provider} {...props} />
|
<ProjectDetailContext.Provider value={provider} {...props} />
|
||||||
</DashboardInsider>
|
</DashboardInsider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React from 'react';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
import moment from 'moment';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { Intent } from '@blueprintjs/core';
|
||||||
|
import { FormatDate } from '@/components';
|
||||||
|
import {
|
||||||
|
DetailFinancialCard,
|
||||||
|
DetailFinancialSection,
|
||||||
|
FinancialProgressBar,
|
||||||
|
FinancialCardText,
|
||||||
|
} from './components';
|
||||||
|
import { calculateStatus } from '@/utils';
|
||||||
|
import { useCalculateProject } from './utils';
|
||||||
|
|
||||||
|
import { useProjectDetailContext } from './ProjectDetailProvider';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project details header.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function ProjectDetailHeader() {
|
||||||
|
const { project } = useProjectDetailContext();
|
||||||
|
|
||||||
|
const { percentageOfInvoice, percentageOfExpense } = useCalculateProject();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DetailFinancialSection>
|
||||||
|
<DetailFinancialCard
|
||||||
|
label={intl.get('project_details.label.project_estimate')}
|
||||||
|
value={project.cost_estimate_formatted}
|
||||||
|
/>
|
||||||
|
<DetailFinancialCard
|
||||||
|
label={intl.get('project_details.label.invoiced')}
|
||||||
|
value={project.total_invoiced_formatted}
|
||||||
|
>
|
||||||
|
<FinancialCardText>
|
||||||
|
{intl.get('project_details.label.of_project_estimate', {
|
||||||
|
value: percentageOfInvoice,
|
||||||
|
})}
|
||||||
|
</FinancialCardText>
|
||||||
|
<FinancialProgressBar
|
||||||
|
intent={Intent.NONE}
|
||||||
|
value={calculateStatus(project.total_invoiced, project.cost_estimate)}
|
||||||
|
/>
|
||||||
|
</DetailFinancialCard>
|
||||||
|
<DetailFinancialCard
|
||||||
|
label={intl.get('project_details.label.time_expenses')}
|
||||||
|
value={project.total_expenses_formatted}
|
||||||
|
>
|
||||||
|
<FinancialCardText>
|
||||||
|
{intl.get('project_details.label.of_project_estimate', {
|
||||||
|
value: percentageOfExpense,
|
||||||
|
})}
|
||||||
|
</FinancialCardText>
|
||||||
|
<FinancialProgressBar
|
||||||
|
intent={Intent.NONE}
|
||||||
|
value={calculateStatus(project.total_expenses, project.cost_estimate)}
|
||||||
|
/>
|
||||||
|
</DetailFinancialCard>
|
||||||
|
|
||||||
|
<DetailFinancialCard
|
||||||
|
label={intl.get('project_details.label.to_be_invoiced')}
|
||||||
|
value={project.total_billable_formatted}
|
||||||
|
/>
|
||||||
|
<DetailFinancialCard
|
||||||
|
label={'Deadline'}
|
||||||
|
value={<FormatDate value={project.deadline_formatted} />}
|
||||||
|
>
|
||||||
|
<FinancialCardText>4 days to go</FinancialCardText>
|
||||||
|
</DetailFinancialCard>
|
||||||
|
</DetailFinancialSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Icon } from '@/components';
|
import { Icon, If, Choose, FormattedMessage as T } from '@/components';
|
||||||
import { Menu, MenuItem, Intent } from '@blueprintjs/core';
|
import { Menu, MenuItem, Intent } from '@blueprintjs/core';
|
||||||
import { safeCallback } from '@/utils';
|
import { safeCallback } from '@/utils';
|
||||||
|
|
||||||
@@ -30,22 +30,39 @@ export function ActionsMenu({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TaskAccessor(row) {
|
/**
|
||||||
|
*
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function TaskChrageType({ values: { charge_type, rate } }) {
|
||||||
|
return (
|
||||||
|
<Choose>
|
||||||
|
<Choose.When condition={charge_type === 'TIME'}>
|
||||||
|
<T id={'project_task.rate'} values={{ rate: rate }} />
|
||||||
|
</Choose.When>
|
||||||
|
<Choose.When condition={charge_type === 'FIXED'}>
|
||||||
|
<T id={'project_task.fixed_price'} />
|
||||||
|
</Choose.When>
|
||||||
|
<Choose.When condition={charge_type === 'NON_CHARGABLE'}>
|
||||||
|
<T id={'project_task.non_chargable'} />
|
||||||
|
</Choose.When>
|
||||||
|
</Choose>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TaskAccessor(task) {
|
||||||
return (
|
return (
|
||||||
<TaskRoot>
|
<TaskRoot>
|
||||||
<TaskHeader>
|
<TaskHeader>
|
||||||
<TaskTitle>{row.name}</TaskTitle>
|
<TaskTitle>{task.name}</TaskTitle>
|
||||||
</TaskHeader>
|
</TaskHeader>
|
||||||
<TaskContent>
|
<TaskContent>
|
||||||
{row.charge_type === 'TIME'
|
<TaskChrageType values={task} />
|
||||||
? intl.get('project_task.rate', {
|
|
||||||
rate: row.rate,
|
|
||||||
})
|
|
||||||
: row.charge_type}
|
|
||||||
<TaskDescription>
|
<TaskDescription>
|
||||||
{row.estimate_minutes &&
|
{task.estimate_hours &&
|
||||||
intl.get('project_task.estimate_minutes', {
|
intl.get('project_task.estimate_hours', {
|
||||||
estimate_minutes: row.estimate_minutes,
|
estimate_hours: task.estimate_hours,
|
||||||
})}
|
})}
|
||||||
</TaskDescription>
|
</TaskDescription>
|
||||||
</TaskContent>
|
</TaskContent>
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import { ProjectTasksHeader } from './ProjectTasksHeader';
|
import { ProjectDetailHeader } from '../ProjectDetailsHeader';
|
||||||
import { ProjectTasksTable } from './ProjectTasksTable';
|
import { ProjectTasksTable } from './ProjectTasksTable';
|
||||||
import { ProjectTaskProvider } from './ProjectTaskProvider';
|
import { ProjectTaskProvider } from './ProjectTaskProvider';
|
||||||
|
|
||||||
export default function ProjectTasks() {
|
export default function ProjectTasks() {
|
||||||
return (
|
return (
|
||||||
<ProjectTaskProvider>
|
<ProjectTaskProvider>
|
||||||
<ProjectTasksHeader />
|
<ProjectDetailHeader />
|
||||||
<ProjectTasksTableCard>
|
<ProjectTasksTableCard>
|
||||||
<ProjectTasksTable />
|
<ProjectTasksTable />
|
||||||
</ProjectTasksTableCard>
|
</ProjectTasksTableCard>
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import { ProjectDetailHeader } from '../ProjectDetailsHeader';
|
||||||
import { ProjectTimesheetsTable } from './ProjectTimesheetsTable';
|
import { ProjectTimesheetsTable } from './ProjectTimesheetsTable';
|
||||||
import { ProjectTimesheetsHeader } from './ProjectTimesheetsHeader';
|
|
||||||
import { ProjectTimesheetsProvider } from './ProjectTimesheetsProvider';
|
import { ProjectTimesheetsProvider } from './ProjectTimesheetsProvider';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -13,7 +13,7 @@ import { ProjectTimesheetsProvider } from './ProjectTimesheetsProvider';
|
|||||||
export default function ProjectTimeSheets() {
|
export default function ProjectTimeSheets() {
|
||||||
return (
|
return (
|
||||||
<ProjectTimesheetsProvider>
|
<ProjectTimesheetsProvider>
|
||||||
<ProjectTimesheetsHeader />
|
<ProjectDetailHeader />
|
||||||
<ProjectTimesheetTableCard>
|
<ProjectTimesheetTableCard>
|
||||||
<ProjectTimesheetsTable />
|
<ProjectTimesheetsTable />
|
||||||
</ProjectTimesheetTableCard>
|
</ProjectTimesheetTableCard>
|
||||||
|
|||||||
30
src/containers/Projects/containers/ProjectDetails/utils.tsx
Normal file
30
src/containers/Projects/containers/ProjectDetails/utils.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
//@ts-nocheck
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { subtract } from 'lodash';
|
||||||
|
import { calculateStatus } from '@/utils';
|
||||||
|
import { useProjectDetailContext } from './ProjectDetailProvider';
|
||||||
|
|
||||||
|
function calculateProject(costEstiate, totalAmount) {
|
||||||
|
return (costEstiate / totalAmount) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCalculateProject = () => {
|
||||||
|
const { project } = useProjectDetailContext();
|
||||||
|
const percentageOfInvoice = calculateProject(
|
||||||
|
project?.total_invoiced,
|
||||||
|
project?.cost_estimate,
|
||||||
|
);
|
||||||
|
|
||||||
|
const percentageOfExpense = calculateProject(
|
||||||
|
project?.total_expenses,
|
||||||
|
project?.cost_estimate,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
percentageOfInvoice,
|
||||||
|
percentageOfExpense,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
ExpenseSelect,
|
ExpenseSelect,
|
||||||
FInputGroupComponent,
|
FInputGroupComponent,
|
||||||
ChangeTypesSelect,
|
ProjectTaskChargeTypeSelect,
|
||||||
} from '../../components';
|
} from '../../components';
|
||||||
import ExpenseFormChargeFields from './ProjectExpenseFormChargeFields';
|
import ExpenseFormChargeFields from './ProjectExpenseFormChargeFields';
|
||||||
import { momentFormatter } from '@/utils';
|
import { momentFormatter } from '@/utils';
|
||||||
@@ -100,7 +100,7 @@ export default function ProjectExpenseFormFields() {
|
|||||||
label={<T id={'project_expense.dialog.charge'} />}
|
label={<T id={'project_expense.dialog.charge'} />}
|
||||||
className={classNames('form-group--select-list', Classes.FILL)}
|
className={classNames('form-group--select-list', Classes.FILL)}
|
||||||
>
|
>
|
||||||
<ChangeTypesSelect
|
<ProjectTaskChargeTypeSelect
|
||||||
name="expenseCharge"
|
name="expenseCharge"
|
||||||
items={expenseChargeOption}
|
items={expenseChargeOption}
|
||||||
popoverProps={{ minimal: true }}
|
popoverProps={{ minimal: true }}
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import {
|
|||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { useProjectTimeEntryFormContext } from './ProjectTimeEntryFormProvider';
|
import { useProjectTimeEntryFormContext } from './ProjectTimeEntryFormProvider';
|
||||||
import {
|
import {
|
||||||
TaskSelect,
|
|
||||||
ProjectsSelect,
|
ProjectsSelect,
|
||||||
|
ProjectTaskSelect,
|
||||||
ProjectSelectButton,
|
ProjectSelectButton,
|
||||||
} from '../../components';
|
} from '../../components';
|
||||||
import { momentFormatter } from '@/utils';
|
import { momentFormatter } from '@/utils';
|
||||||
@@ -59,7 +59,7 @@ function ProjectTimeEntryFormFields() {
|
|||||||
labelInfo={<FieldRequiredHint />}
|
labelInfo={<FieldRequiredHint />}
|
||||||
className={classNames('form-group--select-list', Classes.FILL)}
|
className={classNames('form-group--select-list', Classes.FILL)}
|
||||||
>
|
>
|
||||||
<TaskSelect
|
<ProjectTaskSelect
|
||||||
name={'task_id'}
|
name={'task_id'}
|
||||||
tasks={projectTasks}
|
tasks={projectTasks}
|
||||||
popoverProps={{ minimal: true }}
|
popoverProps={{ minimal: true }}
|
||||||
|
|||||||
@@ -49,14 +49,18 @@ export const StatusAccessor = (row) => {
|
|||||||
<ProjectStatus row={row} />
|
<ProjectStatus row={row} />
|
||||||
</Choose.When>
|
</Choose.When>
|
||||||
<Choose.When condition={row.status_formatted === 'Closed'}>
|
<Choose.When condition={row.status_formatted === 'Closed'}>
|
||||||
<StatusTag minimal={true} intent={Intent.SUCCESS} round={true}>
|
<StatusTagWrap>
|
||||||
{row.status_formatted}
|
<Tag minimal={true} intent={Intent.SUCCESS} round={true}>
|
||||||
</StatusTag>
|
{row.status_formatted}
|
||||||
|
</Tag>
|
||||||
|
</StatusTagWrap>
|
||||||
</Choose.When>
|
</Choose.When>
|
||||||
<Choose.Otherwise>
|
<Choose.Otherwise>
|
||||||
<StatusTag minimal={true} round={true}>
|
<StatusTagWrap>
|
||||||
<T id={'draft'} />
|
<Tag minimal={true} round={true}>
|
||||||
</StatusTag>
|
<T id={'draft'} />
|
||||||
|
</Tag>
|
||||||
|
</StatusTagWrap>
|
||||||
</Choose.Otherwise>
|
</Choose.Otherwise>
|
||||||
</Choose>
|
</Choose>
|
||||||
);
|
);
|
||||||
@@ -167,7 +171,7 @@ export const useProjectsListColumns = () => {
|
|||||||
id: 'name',
|
id: 'name',
|
||||||
Header: '',
|
Header: '',
|
||||||
accessor: ProjectsAccessor,
|
accessor: ProjectsAccessor,
|
||||||
width: 240,
|
width: 140,
|
||||||
className: 'name',
|
className: 'name',
|
||||||
clickable: true,
|
clickable: true,
|
||||||
},
|
},
|
||||||
@@ -232,7 +236,11 @@ const ProjectProgressBar = styled(ProgressBar)`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StatusTag = styled(Tag)`
|
const StatusTagWrap = styled.div`
|
||||||
min-width: 65px;
|
display: flex;
|
||||||
text-align: center;
|
justify-content: center;
|
||||||
|
.tag {
|
||||||
|
min-width: 65px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -2083,8 +2083,10 @@
|
|||||||
"project_task.dialog.edit_success_message": "The task has been edited successfully.",
|
"project_task.dialog.edit_success_message": "The task has been edited successfully.",
|
||||||
"project_task.action.edit_task": "Edit Task",
|
"project_task.action.edit_task": "Edit Task",
|
||||||
"project_task.action.delete_task": "Delete Task",
|
"project_task.action.delete_task": "Delete Task",
|
||||||
"project_task.rate": "{rate} /hour",
|
"project_task.rate": "{rate} / hour",
|
||||||
"project_task.estimate_minutes": "• {estimate_minutes}h 0m estimated",
|
"project_task.fixed_price": "Fixed price",
|
||||||
|
"project_task.non_chargable": "Non-chargeable",
|
||||||
|
"project_task.estimate_hours": "• {estimate_hours}h 0m estimated",
|
||||||
"project_task.alert.delete_message": "The deleted task has been deleted successfully.",
|
"project_task.alert.delete_message": "The deleted task has been deleted successfully.",
|
||||||
"project_task.alert.once_delete_this_project": "Once you delete this task, you won't be able to restore it later. Are you sure you want to delete this task?",
|
"project_task.alert.once_delete_this_project": "Once you delete this task, you won't be able to restore it later. Are you sure you want to delete this task?",
|
||||||
"fixed_price": "Fixed price",
|
"fixed_price": "Fixed price",
|
||||||
@@ -2095,7 +2097,7 @@
|
|||||||
"project.schema.label.project_state": "Project state",
|
"project.schema.label.project_state": "Project state",
|
||||||
"project.schema.label.project_cost": "Project cost",
|
"project.schema.label.project_cost": "Project cost",
|
||||||
"project_task.schema.label.task_name": "Task name",
|
"project_task.schema.label.task_name": "Task name",
|
||||||
"project_task.schema.label.task_house": "Task house",
|
"project_task.schema.label.estimate_hours": "Estimate hours",
|
||||||
"project_task.schema.label.charge_type": "Charge type",
|
"project_task.schema.label.charge_type": "Charge type",
|
||||||
"project_task.schema.label.rate": "Rate",
|
"project_task.schema.label.rate": "Rate",
|
||||||
"project_task.schema.label.amount": "Amount",
|
"project_task.schema.label.amount": "Amount",
|
||||||
@@ -2112,6 +2114,11 @@
|
|||||||
"project_details.new_invoicing": "New Invoicing",
|
"project_details.new_invoicing": "New Invoicing",
|
||||||
"project_details.new_expense": "New Expense",
|
"project_details.new_expense": "New Expense",
|
||||||
"project_details.new_estimated_expense": "New Estimated Expense",
|
"project_details.new_estimated_expense": "New Estimated Expense",
|
||||||
|
"project_details.label.project_estimate": "Project estimate",
|
||||||
|
"project_details.label.invoiced": "Invoiced",
|
||||||
|
"project_details.label.time_expenses": "Time & Expenses",
|
||||||
|
"project_details.label.to_be_invoiced": "To be invoiced",
|
||||||
|
"project_details.label.of_project_estimate": "{value}% of project estimate",
|
||||||
"timesheets.action.delete_timesheet": "Delete",
|
"timesheets.action.delete_timesheet": "Delete",
|
||||||
"timesheets.action.edit_timesheet": "Edit Timesheet",
|
"timesheets.action.edit_timesheet": "Edit Timesheet",
|
||||||
"timesheets.column.date": "Date",
|
"timesheets.column.date": "Date",
|
||||||
@@ -2215,5 +2222,10 @@
|
|||||||
"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.expense": "Expense",
|
||||||
"project_billable_entries.dialog.task": "Task",
|
"project_billable_entries.dialog.task": "Task",
|
||||||
"project_billable_entries.dialog.bill": "Bill"
|
"project_billable_entries.dialog.bill": "Bill",
|
||||||
|
"project_billable_entries.dialog.add": "Add",
|
||||||
|
"project_billable_entries.dialog.show": "Show",
|
||||||
|
"project_billable_entries.alert.there_is_no_billable_entries": "There is no billable entries for that project.",
|
||||||
|
"project_billable_entries.billable_type": "Billable {value}",
|
||||||
|
"add_billable_entries": "Add Billable Entries"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user