mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 13:50:31 +00:00
feat: project detail.
This commit is contained in:
@@ -31,7 +31,7 @@ const chargeTypeSelectProps = {
|
||||
* @param param0
|
||||
* @returns
|
||||
*/
|
||||
export function ChangeTypesSelect({ items, ...rest }) {
|
||||
export function ProjectTaskChargeTypeSelect({ items, ...rest }) {
|
||||
return (
|
||||
<FSelect
|
||||
{...chargeTypeSelectProps}
|
||||
@@ -48,7 +48,12 @@ const taskSelectProps = {
|
||||
labelAccessor: 'name',
|
||||
};
|
||||
|
||||
export function TaskSelect({ tasks, ...rest }) {
|
||||
/**
|
||||
*
|
||||
* @param param0
|
||||
* @returns
|
||||
*/
|
||||
export function ProjectTaskSelect({ tasks, ...rest }) {
|
||||
return (
|
||||
<FSelect
|
||||
items={tasks}
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import {
|
||||
ExpenseSelect,
|
||||
FInputGroupComponent,
|
||||
ChangeTypesSelect,
|
||||
ProjectTaskChargeTypeSelect,
|
||||
} from '../../components';
|
||||
import { useEstimatedExpenseFormContext } from './EstimatedExpenseFormProvider';
|
||||
import EstimatedExpenseFormChargeFields from './EstimatedExpenseFormChargeFields';
|
||||
@@ -75,7 +75,7 @@ export default function EstimatedExpenseFormFields() {
|
||||
label={<T id={'estimated_expenses.dialog.charge'} />}
|
||||
className={classNames('form-group--select-list', Classes.FILL)}
|
||||
>
|
||||
<ChangeTypesSelect
|
||||
<ProjectTaskChargeTypeSelect
|
||||
name="charge"
|
||||
items={expenseChargeOption}
|
||||
popoverProps={{ minimal: true }}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { DashboardInsider } from '@/components';
|
||||
import { useProject } from '../../hooks';
|
||||
|
||||
const ProjectDetailContext = React.createContext();
|
||||
|
||||
@@ -13,12 +14,18 @@ function ProjectDetailProvider({
|
||||
// #ownProps
|
||||
...props
|
||||
}) {
|
||||
// Handle fetch project detail.
|
||||
const { data: project, isLoading: isProjectLoading } = useProject(projectId, {
|
||||
enabled: !!projectId,
|
||||
});
|
||||
|
||||
// State provider.
|
||||
const provider = {
|
||||
project,
|
||||
projectId,
|
||||
};
|
||||
return (
|
||||
<DashboardInsider className="timesheets">
|
||||
<DashboardInsider loading={isProjectLoading}>
|
||||
<ProjectDetailContext.Provider value={provider} {...props} />
|
||||
</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 intl from 'react-intl-universal';
|
||||
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 { 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 (
|
||||
<TaskRoot>
|
||||
<TaskHeader>
|
||||
<TaskTitle>{row.name}</TaskTitle>
|
||||
<TaskTitle>{task.name}</TaskTitle>
|
||||
</TaskHeader>
|
||||
<TaskContent>
|
||||
{row.charge_type === 'TIME'
|
||||
? intl.get('project_task.rate', {
|
||||
rate: row.rate,
|
||||
})
|
||||
: row.charge_type}
|
||||
<TaskChrageType values={task} />
|
||||
|
||||
<TaskDescription>
|
||||
{row.estimate_minutes &&
|
||||
intl.get('project_task.estimate_minutes', {
|
||||
estimate_minutes: row.estimate_minutes,
|
||||
{task.estimate_hours &&
|
||||
intl.get('project_task.estimate_hours', {
|
||||
estimate_hours: task.estimate_hours,
|
||||
})}
|
||||
</TaskDescription>
|
||||
</TaskContent>
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { ProjectTasksHeader } from './ProjectTasksHeader';
|
||||
import { ProjectDetailHeader } from '../ProjectDetailsHeader';
|
||||
import { ProjectTasksTable } from './ProjectTasksTable';
|
||||
import { ProjectTaskProvider } from './ProjectTaskProvider';
|
||||
|
||||
export default function ProjectTasks() {
|
||||
return (
|
||||
<ProjectTaskProvider>
|
||||
<ProjectTasksHeader />
|
||||
<ProjectDetailHeader />
|
||||
<ProjectTasksTableCard>
|
||||
<ProjectTasksTable />
|
||||
</ProjectTasksTableCard>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { ProjectDetailHeader } from '../ProjectDetailsHeader';
|
||||
import { ProjectTimesheetsTable } from './ProjectTimesheetsTable';
|
||||
import { ProjectTimesheetsHeader } from './ProjectTimesheetsHeader';
|
||||
import { ProjectTimesheetsProvider } from './ProjectTimesheetsProvider';
|
||||
|
||||
/**
|
||||
@@ -13,7 +13,7 @@ import { ProjectTimesheetsProvider } from './ProjectTimesheetsProvider';
|
||||
export default function ProjectTimeSheets() {
|
||||
return (
|
||||
<ProjectTimesheetsProvider>
|
||||
<ProjectTimesheetsHeader />
|
||||
<ProjectDetailHeader />
|
||||
<ProjectTimesheetTableCard>
|
||||
<ProjectTimesheetsTable />
|
||||
</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 {
|
||||
ExpenseSelect,
|
||||
FInputGroupComponent,
|
||||
ChangeTypesSelect,
|
||||
ProjectTaskChargeTypeSelect,
|
||||
} from '../../components';
|
||||
import ExpenseFormChargeFields from './ProjectExpenseFormChargeFields';
|
||||
import { momentFormatter } from '@/utils';
|
||||
@@ -100,7 +100,7 @@ export default function ProjectExpenseFormFields() {
|
||||
label={<T id={'project_expense.dialog.charge'} />}
|
||||
className={classNames('form-group--select-list', Classes.FILL)}
|
||||
>
|
||||
<ChangeTypesSelect
|
||||
<ProjectTaskChargeTypeSelect
|
||||
name="expenseCharge"
|
||||
items={expenseChargeOption}
|
||||
popoverProps={{ minimal: true }}
|
||||
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
} from '@/components';
|
||||
import { useProjectTimeEntryFormContext } from './ProjectTimeEntryFormProvider';
|
||||
import {
|
||||
TaskSelect,
|
||||
ProjectsSelect,
|
||||
ProjectTaskSelect,
|
||||
ProjectSelectButton,
|
||||
} from '../../components';
|
||||
import { momentFormatter } from '@/utils';
|
||||
@@ -59,7 +59,7 @@ function ProjectTimeEntryFormFields() {
|
||||
labelInfo={<FieldRequiredHint />}
|
||||
className={classNames('form-group--select-list', Classes.FILL)}
|
||||
>
|
||||
<TaskSelect
|
||||
<ProjectTaskSelect
|
||||
name={'task_id'}
|
||||
tasks={projectTasks}
|
||||
popoverProps={{ minimal: true }}
|
||||
|
||||
@@ -49,14 +49,18 @@ export const StatusAccessor = (row) => {
|
||||
<ProjectStatus row={row} />
|
||||
</Choose.When>
|
||||
<Choose.When condition={row.status_formatted === 'Closed'}>
|
||||
<StatusTag minimal={true} intent={Intent.SUCCESS} round={true}>
|
||||
{row.status_formatted}
|
||||
</StatusTag>
|
||||
<StatusTagWrap>
|
||||
<Tag minimal={true} intent={Intent.SUCCESS} round={true}>
|
||||
{row.status_formatted}
|
||||
</Tag>
|
||||
</StatusTagWrap>
|
||||
</Choose.When>
|
||||
<Choose.Otherwise>
|
||||
<StatusTag minimal={true} round={true}>
|
||||
<T id={'draft'} />
|
||||
</StatusTag>
|
||||
<StatusTagWrap>
|
||||
<Tag minimal={true} round={true}>
|
||||
<T id={'draft'} />
|
||||
</Tag>
|
||||
</StatusTagWrap>
|
||||
</Choose.Otherwise>
|
||||
</Choose>
|
||||
);
|
||||
@@ -167,7 +171,7 @@ export const useProjectsListColumns = () => {
|
||||
id: 'name',
|
||||
Header: '',
|
||||
accessor: ProjectsAccessor,
|
||||
width: 240,
|
||||
width: 140,
|
||||
className: 'name',
|
||||
clickable: true,
|
||||
},
|
||||
@@ -232,7 +236,11 @@ const ProjectProgressBar = styled(ProgressBar)`
|
||||
}
|
||||
`;
|
||||
|
||||
const StatusTag = styled(Tag)`
|
||||
min-width: 65px;
|
||||
text-align: center;
|
||||
const StatusTagWrap = styled.div`
|
||||
display: flex;
|
||||
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.action.edit_task": "Edit Task",
|
||||
"project_task.action.delete_task": "Delete Task",
|
||||
"project_task.rate": "{rate} /hour",
|
||||
"project_task.estimate_minutes": "• {estimate_minutes}h 0m estimated",
|
||||
"project_task.rate": "{rate} / hour",
|
||||
"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.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",
|
||||
@@ -2095,7 +2097,7 @@
|
||||
"project.schema.label.project_state": "Project state",
|
||||
"project.schema.label.project_cost": "Project cost",
|
||||
"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.rate": "Rate",
|
||||
"project_task.schema.label.amount": "Amount",
|
||||
@@ -2112,6 +2114,11 @@
|
||||
"project_details.new_invoicing": "New Invoicing",
|
||||
"project_details.new_expense": "New 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.edit_timesheet": "Edit Timesheet",
|
||||
"timesheets.column.date": "Date",
|
||||
@@ -2215,5 +2222,10 @@
|
||||
"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"
|
||||
"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