feat: add api project tasks.

This commit is contained in:
elforjani13
2022-07-28 21:14:36 +02:00
parent a17843ddbe
commit fcf001a831
32 changed files with 813 additions and 201 deletions

View File

@@ -19,6 +19,7 @@ export const TABLES = {
WAREHOUSE_TRANSFERS: 'warehouse_transfers',
PROJECTS: 'projects',
TIMESHEETS: 'timesheets',
PROJECT_TASKS: 'project_tasks',
};
export const TABLE_SIZE = {

View File

@@ -0,0 +1,77 @@
import React from 'react';
import intl from 'react-intl-universal';
import { FormattedMessage as T, FormattedHTMLMessage } from '@/components';
import { Intent, Alert } from '@blueprintjs/core';
import { AppToaster } from '@/components';
import { useDeleteProjectTask } from '../../hooks';
import withAlertStoreConnect from '@/containers/Alert/withAlertStoreConnect';
import withAlertActions from '@/containers/Alert/withAlertActions';
import { compose } from '@/utils';
/**
* Project tasks delete alert.
* @returns
*/
function ProjectTaskDeleteAlert({
name,
// #withAlertStoreConnect
isOpen,
payload: { taskId },
// #withAlertActions
closeAlert,
}) {
const { mutateAsync: deleteProjectTaskMutate, isLoading } =
useDeleteProjectTask();
// handle cancel delete alert.
const handleCancelDeleteAlert = () => {
closeAlert(name);
};
// handleConfirm delete project
const handleConfirmProjectDelete = () => {
deleteProjectTaskMutate(taskId)
.then(() => {
AppToaster.show({
message: intl.get('projects.alert.delete_message'),
intent: Intent.SUCCESS,
});
})
.catch(
({
response: {
data: { errors },
},
}) => {},
)
.finally(() => {
closeAlert(name);
});
};
return (
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'delete'} />}
icon="trash"
intent={Intent.DANGER}
isOpen={isOpen}
onCancel={handleCancelDeleteAlert}
onConfirm={handleConfirmProjectDelete}
loading={isLoading}
>
<p>
<FormattedHTMLMessage id={'projects.alert.once_delete_this_project'} />
</p>
</Alert>
);
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
)(ProjectTaskDeleteAlert);

View File

@@ -1,8 +1,14 @@
import React from 'react';
const ProjectDeleteAlert = React.lazy(() => import('./ProjectDeleteAlert'));
const ProjectTaskDeleteAlert = React.lazy(
() => import('./ProjectTaskDeleteAlert'),
);
/**
* Project alerts.
*/
export default [{ name: 'project-delete', component: ProjectDeleteAlert }];
export default [
{ name: 'project-delete', component: ProjectDeleteAlert },
{ name: 'project-task-delete', component: ProjectTaskDeleteAlert },
];

View File

@@ -58,7 +58,8 @@ function ProjectDetailActionsBar({
const handleTableRowSizeChange = (size) => {
addSetting('timesheets', 'tableSize', size) &&
addSetting('sales', 'tableSize', size) &&
addSetting('purchases', 'tableSize', size);
addSetting('purchases', 'tableSize', size) &&
addSetting('project_tasks', 'tableSize', size);
};
const handleTimeEntryBtnClick = () => {

View File

@@ -3,6 +3,7 @@ import styled from 'styled-components';
import intl from 'react-intl-universal';
import { Tabs, Tab } from '@blueprintjs/core';
import ProjectTimeSheets from './ProjectTimeSheets';
import ProjectTasks from './ProjectTasks';
import ProjectPurchasesTable from './ProjectPurchasesTable';
import ProjectSalesTable from './ProjectSalesTable';
@@ -17,9 +18,14 @@ export default function ProjectDetailTabs() {
animate={true}
large={true}
renderActiveTabPanelOnly={true}
defaultSelectedTabId={'purchases'}
defaultSelectedTabId={'tasks'}
>
<Tab id="overview" title={intl.get('project_details.label.overview')} />
<Tab
id="tasks"
title={intl.get('project_details.label.tasks')}
panel={<ProjectTasks />}
/>
<Tab
id="timesheet"
title={intl.get('project_details.label.timesheet')}

View File

@@ -0,0 +1,36 @@
import React from 'react';
import { useParams } from 'react-router-dom';
import { useProjectTasks } from '../../../hooks';
const ProjectTaskContext = React.createContext();
/**
* Project task data provider.
* @returns
*/
function ProjectTaskProvider({ ...props }) {
const { id } = useParams();
const projectId = parseInt(id, 10);
const {
data: { projectTasks },
isFetching: isProjectTasksFetching,
isLoading: isProjectTasksLoading,
} = useProjectTasks(projectId, {
enabled: !!projectId,
});
// provider payload.
const provider = {
projectId,
projectTasks,
isProjectTasksFetching,
isProjectTasksLoading,
};
return <ProjectTaskContext.Provider value={provider} {...props} />;
}
const useProjectTaskContext = () => React.useContext(ProjectTaskContext);
export { ProjectTaskProvider, useProjectTaskContext };

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { Intent } from '@blueprintjs/core';
import { FormatDate } from '@/components';
import {
DetailFinancialCard,
DetailFinancialSection,
FinancialProgressBar,
FinancialCardText,
} from '../components';
import { calculateStatus } from '@/utils';
/**
* Project Tasks header.
* @returns
*/
export function ProjectTasksHeader() {
return (
<DetailFinancialSection>
<DetailFinancialCard label={'Project estimate'} value={'3.14'} />
<DetailFinancialCard label={'Invoiced'} value={'0.00'}>
<FinancialCardText>0% of project estimate</FinancialCardText>
<FinancialProgressBar intent={Intent.NONE} value={0} />
</DetailFinancialCard>
<DetailFinancialCard label={'Time & Expenses'} value={'0.00'}>
<FinancialCardText>0% of project estimate</FinancialCardText>
<FinancialProgressBar intent={Intent.NONE} value={0} />
</DetailFinancialCard>
<DetailFinancialCard label={'To be invoiced'} value={'3.14'} />
<DetailFinancialCard
label={'Deadline'}
value={<FormatDate value={'2022-06-08T22:00:00.000Z'} />}
>
<FinancialCardText>4 days to go</FinancialCardText>
</DetailFinancialCard>
</DetailFinancialSection>
);
}

View File

@@ -0,0 +1,87 @@
import React from 'react';
import styled from 'styled-components';
import {
DataTable,
TableSkeletonRows,
TableSkeletonHeader,
} from '@/components';
import { TABLES } from '@/constants/tables';
import { ActionsMenu } from './components';
import { useProjectTaskColumns } from './hooks';
import { useMemorizedColumnsWidths } from '@/hooks';
import { useProjectTaskContext } from './ProjectTaskProvider';
import withSettings from '@/containers/Settings/withSettings';
import withAlertsActions from '@/containers/Alert/withAlertActions';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { compose } from '@/utils';
function ProjectTaskTableRoot({
// #withSettings
projectTasksTableSize,
// #withDialog
openDialog,
// #withAlertsActions
openAlert,
}) {
const { projectTasks } = useProjectTaskContext();
// Retrieve project task table columns.
const columns = useProjectTaskColumns();
// Handle delete task.
const handleDeleteTask = ({ id }) => {
openAlert('project-task-delete', { taskId: id });
};
const handleEditTask = ({ id }) => {
openDialog('project-task-form', {
taskId: id,
action: 'edit',
});
};
// Local storage memorizing columns widths.
const [initialColumnsWidths, , handleColumnResizing] =
useMemorizedColumnsWidths(TABLES.PROJECT_TASKS);
return (
<ProjectTaksDataTable
columns={columns}
data={projectTasks}
manualSortBy={true}
noInitialFetch={true}
sticky={true}
hideTableHeader={true}
ContextMenu={ActionsMenu}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
initialColumnsWidths={initialColumnsWidths}
onColumnResizing={handleColumnResizing}
size={projectTasksTableSize}
payload={{
onDelete: handleDeleteTask,
onEdit: handleEditTask,
}}
/>
);
}
export const ProjectTasksTable = compose(
withAlertsActions,
withDialogActions,
withSettings(({ projectTasksSettings }) => ({
projectTasksTableSize: projectTasksSettings?.tableSize,
})),
)(ProjectTaskTableRoot);
const ProjectTaksDataTable = styled(DataTable)`
.table {
.thead .tr .th {
.resizer {
display: none;
}
}
}
`;

View File

@@ -0,0 +1,74 @@
import React from 'react';
import intl from 'react-intl-universal';
import styled from 'styled-components';
import { Icon } from '@/components';
import { Menu, MenuItem, Intent } from '@blueprintjs/core';
import { safeCallback } from '@/utils';
/**
* Table actions cell.
*/
export function ActionsMenu({
payload: { onEdit, onDelete },
row: { original },
}) {
return (
<Menu>
<MenuItem
icon={<Icon icon="pen-18" />}
text={intl.get('project_task.action.edit_task')}
onClick={safeCallback(onEdit, original)}
/>
<MenuItem
text={intl.get('project_task.action.delete_task')}
intent={Intent.DANGER}
onClick={safeCallback(onDelete, original)}
icon={<Icon icon="trash-16" iconSize={16} />}
/>
</Menu>
);
}
export function TaskAccessor(row) {
return (
<TaskRoot>
<TaskHeader>
<TaskTitle>{row.name}</TaskTitle>
</TaskHeader>
<TaskContent>
{row.charge_type === 'hourly_rate'
? row.rate + ' / hour'
: row.charge_type}
<TaskDescription>{row.estimate_minutes} estimated</TaskDescription>
</TaskContent>
</TaskRoot>
);
}
const TaskRoot = styled.div`
margin-left: 12px;
`;
const TaskHeader = styled.div`
display: flex;
align-items: baseline;
flex-flow: wrap;
`;
const TaskTitle = styled.span`
font-weight: 500;
/* margin-right: 12px; */
line-height: 1.5rem;
`;
const TaskContent = styled.div`
display: block;
white-space: nowrap;
font-size: 13px;
opacity: 0.75;
margin-bottom: 0.1rem;
line-height: 1.2rem;
`;
const TaskDescription = styled.span`
&::before {
content: '•';
margin: 0.3rem;
}
`;

View File

@@ -0,0 +1,22 @@
import React from 'react';
import { TaskAccessor } from './components';
/**
* Retrieve project tasks list columns.
*/
export function useProjectTaskColumns() {
return React.useMemo(
() => [
{
id: 'name',
Header: 'Header',
accessor: TaskAccessor,
width: 100,
className: 'name',
clickable: true,
textOverview: true,
},
],
[],
);
}

View File

@@ -0,0 +1,24 @@
import React from 'react';
import styled from 'styled-components';
import { ProjectTasksHeader } from './ProjectTasksHeader';
import { ProjectTasksTable } from './ProjectTasksTable';
import { ProjectTaskProvider } from './ProjectTaskProvider';
export default function ProjectTasks() {
return (
<ProjectTaskProvider>
<ProjectTasksHeader />
<ProjectTasksTableCard>
<ProjectTasksTable />
</ProjectTasksTableCard>
</ProjectTaskProvider>
);
}
const ProjectTasksTableCard = styled.div`
margin: 22px 32px;
border: 1px solid #c8cad0;
border-radius: 3px;
background: #fff;
`;

View File

@@ -1,4 +1,3 @@
//@ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import styled from 'styled-components';

View File

@@ -2,14 +2,19 @@ import * as Yup from 'yup';
import intl from 'react-intl-universal';
const Schema = Yup.object().shape({
taskName: Yup.string()
.label(intl.get('task.schema.label.task_name'))
name: Yup.string()
.label(intl.get('project_task.schema.label.task_name'))
.required(),
taskHouse: Yup.string().label(intl.get('task.schema.label.task_house')),
taskCharge: Yup.string()
.label(intl.get('task.schema.label.charge'))
charge_type: Yup.string()
.label(intl.get('project_task.schema.label.charge_type'))
.required(),
taskamount: Yup.number().label(intl.get('task.schema.label.amount')),
rate: Yup.number()
.label(intl.get('project_task.schema.label.rate'))
.required(),
cost_estimate: Yup.number().required(),
estimate_minutes: Yup.string().label(
intl.get('project_task.schema.label.task_house'),
),
});
export const CreateProjectTaskFormSchema = Schema;

View File

@@ -1,18 +1,21 @@
import React from 'react';
import intl from 'react-intl-universal';
import { Formik } from 'formik';
import { Intent } from '@blueprintjs/core';
import { AppToaster } from '@/components';
import { CreateProjectTaskFormSchema } from './ProjectTaskForm.schema';
import { useProjectTaskFormContext } from './ProjectTaskFormProvider';
import { AppToaster } from '@/components';
import ProjectTaskFormContent from './ProjectTaskFormContent';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { compose } from '@/utils';
import { compose, transformToForm } from '@/utils';
const defaultInitialValues = {
taskName: '',
taskHouse: '00:00',
taskCharge: 'hourly_rate',
taskamount: '',
name: '',
charge_type: 'fixed_price',
estimate_minutes: '',
cost_estimate: '',
rate: '0.00',
};
/**
@@ -24,19 +27,39 @@ function ProjectTaskForm({
closeDialog,
}) {
// task form dialog context.
const { dialogName } = useProjectTaskFormContext();
const {
taskId,
projectId,
isNewMode,
dialogName,
projectTask,
createProjectTaskMutate,
editProjectTaskMutate,
} = useProjectTaskFormContext();
// Initial form values
const initialValues = {
...defaultInitialValues,
...transformToForm(projectTask, defaultInitialValues),
};
// Handles the form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
const form = {};
const form = { ...values };
// Handle request response success.
const onSuccess = (response) => {};
const onSuccess = (response) => {
AppToaster.show({
message: intl.get(
isNewMode
? 'project_task.dialog.success_message'
: 'project_task.dialog.edit_success_message',
),
intent: Intent.SUCCESS,
});
closeDialog(dialogName);
};
// Handle request response errors.
const onError = ({
@@ -44,8 +67,13 @@ function ProjectTaskForm({
data: { errors },
},
}) => {
setSubmitting(false);
setSubmitting(false);
};
if (isNewMode) {
createProjectTaskMutate([projectId, form]).then(onSuccess).catch(onError);
} else {
editProjectTaskMutate([taskId, form]).then(onSuccess).catch(onError);
}
};
return (

View File

@@ -9,9 +9,14 @@ export default function ProjectTaskFormDialogContent({
// #ownProps
dialogName,
task,
project,
}) {
return (
<ProjectTaskFormProvider taskId={task} dialogName={dialogName}>
<ProjectTaskFormProvider
taskId={task}
projectId={project}
dialogName={dialogName}
>
<ProjectTaskForm />
</ProjectTaskFormProvider>
);

View File

@@ -1,5 +1,4 @@
import React from 'react';
import styled from 'styled-components';
import { useFormikContext } from 'formik';
import { Classes, ControlGroup } from '@blueprintjs/core';
import {
@@ -9,6 +8,7 @@ import {
Row,
FormattedMessage as T,
} from '@/components';
import { EstimateAmount } from './utils';
import { taskChargeOptions } from '../common/modalChargeOptions';
import { ChangeTypesSelect } from '../../components';
@@ -23,69 +23,48 @@ function ProjectTaskFormFields() {
return (
<div className={Classes.DIALOG_BODY}>
{/*------------ Task Name -----------*/}
<FFormGroup label={<T id={'project_task.dialog.task_name'} />} name={'taskName'}>
<FInputGroup name="taskName" />
<FFormGroup
label={<T id={'project_task.dialog.task_name'} />}
name={'taskName'}
>
<FInputGroup name="name" />
</FFormGroup>
{/*------------ Estimated Hours -----------*/}
<Row>
<Col xs={4}>
<FFormGroup
label={<T id={'project_task.dialog.estimated_hours'} />}
name={'taskHouse'}
name={'estimate_minutes'}
>
<FInputGroup name="taskHouse" />
<FInputGroup name="estimate_minutes" />
</FFormGroup>
</Col>
{/*------------ Charge -----------*/}
<Col xs={8}>
<FFormGroup
name={'taskCharge'}
name={'charge_type'}
className={'form-group--select-list'}
label={<T id={'project_task.dialog.charge'} />}
>
<ControlGroup>
<ChangeTypesSelect
name="taskCharge"
name="charge_type"
items={taskChargeOptions}
popoverProps={{ minimal: true }}
filterable={false}
/>
<FInputGroup
name="taskamount"
disabled={values?.taskCharge === 'Non-chargeable'}
name="rate"
disabled={values?.charge_type === 'non_chargeable'}
/>
</ControlGroup>
</FFormGroup>
</Col>
</Row>
{/*------------ Estimated Amount -----------*/}
<EstimatedAmountBase>
<EstimatedAmountContent>
<T id={'project_task.dialog.estimated_amount'} />
<EstimateAmount>0.00</EstimateAmount>
</EstimatedAmountContent>
</EstimatedAmountBase>
<EstimateAmount />
</div>
);
}
export default ProjectTaskFormFields;
const EstimatedAmountBase = styled.div`
display: flex;
justify-content: flex-end;
font-size: 14px;
line-height: 1.5rem;
opacity: 0.75;
`;
const EstimatedAmountContent = styled.span`
background-color: #fffdf5;
padding: 0.1rem 0;
`;
const EstimateAmount = styled.span`
font-size: 15px;
font-weight: 700;
margin-left: 10px;
`;

View File

@@ -1,4 +1,9 @@
import React from 'react';
import {
useCreateProjectTask,
useEditProjectTask,
useProjectTask,
} from '../../hooks';
import { DialogContent } from '@/components';
const ProjectTaskFormContext = React.createContext();
@@ -11,15 +16,34 @@ function ProjectTaskFormProvider({
// #ownProps
dialogName,
taskId,
projectId,
...props
}) {
// Create and edit project task mutations.
const { mutateAsync: createProjectTaskMutate } = useCreateProjectTask();
const { mutateAsync: editProjectTaskMutate } = useEditProjectTask();
// Handle fetch project task detail.
const { data: projectTask, isLoading: isProjectTaskLoading } = useProjectTask(
taskId,
{
enabled: !!taskId,
},
);
const isNewMode = !taskId;
// State provider.
const provider = {
dialogName,
isNewMode,
projectId,
projectTask,
createProjectTaskMutate,
editProjectTaskMutate,
};
return (
<DialogContent>
<DialogContent isLoading={isProjectTaskLoading}>
<ProjectTaskFormContext.Provider value={provider} {...props} />
</DialogContent>
);

View File

@@ -1,6 +1,6 @@
import React from 'react';
import styled from 'styled-components';
import intl from 'react-intl-universal';
import styled from 'styled-components';
import { Dialog, DialogSuspense, FormattedMessage as T } from '@/components';
import withDialogRedux from '@/components/DialogReduxConnect';
import { compose } from '@/utils';
@@ -15,20 +15,30 @@ const ProjectTaskFormDialogContent = React.lazy(
*/
function ProjectTaskFormDialog({
dialogName,
payload: { taskId = null },
payload: { taskId = null, projectId = null, action },
isOpen,
}) {
return (
<Dialog
name={dialogName}
title={intl.get('project_task.dialog.new_task')}
title={
action === 'edit' ? (
<T id="project_task.dialog.edit_task" />
) : (
<T id={'project_task.dialog.new_task'} />
)
}
isOpen={isOpen}
autoFocus={true}
canEscapeKeyClose={true}
style={{ width: '500px' }}
>
<DialogSuspense>
<ProjectTaskFormDialogContent dialogName={dialogName} task={taskId} />
<ProjectTaskFormDialogContent
dialogName={dialogName}
task={taskId}
project={projectId}
/>
</DialogSuspense>
</Dialog>
);

View File

@@ -0,0 +1,52 @@
import React from 'react';
import _ from 'lodash';
import { useFormikContext } from 'formik';
import styled from 'styled-components';
import { Choose, FormattedMessage as T } from '@/components';
export function EstimateAmount() {
const { values } = useFormikContext();
// Calculate estimate amount.
const estimatedAmount = _.multiply(values.rate, values.estimate_minutes);
return (
<EstimatedAmountBase>
<EstimatedAmountContent>
<Choose>
<Choose.When condition={values?.charge_type === 'hourly_rate'}>
<T id={'project_task.dialog.estimated_amount'} />
<EstimatedAmount>{estimatedAmount}</EstimatedAmount>
</Choose.When>
<Choose.When condition={values?.charge_type === 'fixed_price'}>
<T id={'project_task.dialog.total'} />
<EstimatedAmount>{values.rate}</EstimatedAmount>
</Choose.When>
<Choose.Otherwise>
<T id={'project_task.dialog.total'} />
<EstimatedAmount>0.00</EstimatedAmount>
</Choose.Otherwise>
</Choose>
</EstimatedAmountContent>
</EstimatedAmountBase>
);
}
const EstimatedAmountBase = styled.div`
display: flex;
justify-content: flex-end;
font-size: 14px;
line-height: 1.5rem;
opacity: 0.75;
`;
const EstimatedAmountContent = styled.span`
background-color: #fffdf5;
padding: 0.1rem 0;
`;
const EstimatedAmount = styled.span`
font-size: 15px;
font-weight: 700;
margin-left: 10px;
`;

View File

@@ -1,5 +1,6 @@
import * as Yup from 'yup';
import intl from 'react-intl-universal';
import { DATATYPES_LENGTH } from '@/constants/dataTypes';
const Schema = Yup.object().shape({
date: Yup.date()

View File

@@ -10,14 +10,9 @@ export default function ProjectTimeEntryFormDialogContent({
// #ownProps
dialogName,
project,
timeEntry,
}) {
return (
<ProjectTimeEntryFormProvider
projectId={project}
timeEntryId={timeEntry}
dialogName={dialogName}
>
<ProjectTimeEntryFormProvider projectId={project} dialogName={dialogName}>
<ProjectTimeEntryForm />
</ProjectTimeEntryFormProvider>
);

View File

@@ -11,7 +11,6 @@ function ProjectTimeEntryFormProvider({
// #ownProps
dialogName,
projectId,
timeEntryId,
...props
}) {
const provider = {

View File

@@ -15,7 +15,7 @@ const ProjectTimeEntryFormDialogContent = React.lazy(
function ProjectTimeEntryFormDialog({
dialogName,
isOpen,
payload: { projectId = null, timeEntryId = null },
payload: { projectId },
}) {
return (
<ProjectTimeEntryFormDialogRoot
@@ -30,7 +30,6 @@ function ProjectTimeEntryFormDialog({
<ProjectTimeEntryFormDialogContent
dialogName={dialogName}
project={projectId}
timeEntry={timeEntryId}
/>
</DialogSuspense>
</ProjectTimeEntryFormDialogRoot>

View File

@@ -1,7 +1,11 @@
import React from 'react';
import styled from 'styled-components';
import { useHistory } from 'react-router-dom';
import { DataTable,TableSkeletonRows ,TableSkeletonHeader } from '@/components';
import {
DataTable,
TableSkeletonRows,
TableSkeletonHeader,
} from '@/components';
import { TABLES } from '@/constants/tables';
import ProjectsEmptyStatus from './ProjectsEmptyStatus';
import { useProjectsListContext } from './ProjectsListProvider';
@@ -59,8 +63,10 @@ function ProjectsDataTable({
};
// Handle new task button click.
const handleNewTaskButtonClick = () => {
openDialog('project-task-form');
const handleNewTaskButtonClick = (project) => {
openDialog('project-task-form', {
projectId: project.id,
});
};
// Local storage memorizing columns widths.

View File

@@ -1,9 +1,9 @@
import intl from 'react-intl-universal';
export const taskChargeOptions = [
{ name: intl.get('task.dialog.hourly_rate'), value: 'hourly_rate' },
{ name: intl.get('task.dialog.fixed_price'), value: 'fixed_price' },
{ name: intl.get('task.dialog.non_chargeable'), value: 'non_chargeable' },
{ name: intl.get('project_task.dialog.hourly_rate'), value: 'hourly_rate' },
{ name: intl.get('project_task.dialog.fixed_price'), value: 'fixed_price' },
{ name: intl.get('project_task.dialog.non_chargeable'), value: 'non_chargeable' },
];
export const expenseChargeOption = [

View File

@@ -1,124 +1,2 @@
import { useQueryClient, useMutation } from 'react-query';
import { useRequestQuery } from '@/hooks/useQueryRequest';
import { transformPagination } from '@/utils';
import useApiRequest from '@/hooks/useRequest';
import t from './type';
// Common invalidate queries.
const commonInvalidateQueries = (queryClient) => {
// Invalidate projects.
queryClient.invalidateQueries(t.PROJECT);
queryClient.invalidateQueries(t.PROJECTS);
};
/**
* Create a new project
* @param props
*/
export function useCreateProject(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation((values) => apiRequest.post('projects', values), {
onSuccess: (res, values) => {
// Common invalidate queries.
commonInvalidateQueries(queryClient);
},
...props,
});
}
/**
* Edit the given project
* @param props
* @returns
*/
export function useEditProject(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation(
([id, values]) => apiRequest.post(`/projects/${id}`, values),
{
onSuccess: (res, [id, values]) => {
// Invalidate specific project.
queryClient.invalidateQueries([t.PROJECT, id]);
commonInvalidateQueries(queryClient);
},
...props,
},
);
}
/**
* Delete the given project
* @param props
*/
export function useDeleteProject(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation((id) => apiRequest.delete(`projects/${id}`), {
onSuccess: (res, id) => {
// Invalidate specific project.
queryClient.invalidateQueries([t.PROJECT, id]);
// Common invalidate queries.
commonInvalidateQueries(queryClient);
},
...props,
});
}
/**
* Retrieve the projects details.
* @param projectId The project id
* @param props
* @param requestProps
* @returns
*/
export function useProject(projectId, props, requestProps) {
return useRequestQuery(
[t.PROJECT, projectId],
{ method: 'get', url: `projects/${projectId}`, ...requestProps },
{
select: (res) => res.data.project,
defaultData: {},
...props,
},
);
}
const transformProjects = (res) => ({
projects: res.data.projects,
});
/**
* Retrieve projects list with pagination meta.
* @param query
* @param props
*/
export function useProjects(query, props) {
return useRequestQuery(
[t.PROJECTS, query],
{ method: 'get', url: 'projects', params: query },
{
select: transformProjects,
defaultData: {
projects: [],
},
...props,
},
);
}
export function useRefreshInvoices() {
const queryClient = useQueryClient();
return {
refresh: () => {
queryClient.invalidateQueries(t.PROJECTS);
},
};
}
export * from './projects'
export * from './projectsTask'

View File

@@ -0,0 +1,124 @@
import { useQueryClient, useMutation } from 'react-query';
import { useRequestQuery } from '@/hooks/useQueryRequest';
import { transformPagination } from '@/utils';
import useApiRequest from '@/hooks/useRequest';
import t from './type';
// Common invalidate queries.
const commonInvalidateQueries = (queryClient) => {
// Invalidate projects.
queryClient.invalidateQueries(t.PROJECT);
queryClient.invalidateQueries(t.PROJECTS);
};
/**
* Create a new project
* @param props
*/
export function useCreateProject(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation((values) => apiRequest.post('projects', values), {
onSuccess: (res, values) => {
// Common invalidate queries.
commonInvalidateQueries(queryClient);
},
...props,
});
}
/**
* Edit the given project
* @param props
* @returns
*/
export function useEditProject(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation(
([id, values]) => apiRequest.post(`/projects/${id}`, values),
{
onSuccess: (res, [id, values]) => {
// Invalidate specific project.
queryClient.invalidateQueries([t.PROJECT, id]);
commonInvalidateQueries(queryClient);
},
...props,
},
);
}
/**
* Delete the given project
* @param props
*/
export function useDeleteProject(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation((id) => apiRequest.delete(`projects/${id}`), {
onSuccess: (res, id) => {
// Invalidate specific project.
queryClient.invalidateQueries([t.PROJECT, id]);
// Common invalidate queries.
commonInvalidateQueries(queryClient);
},
...props,
});
}
/**
* Retrieve the projects details.
* @param projectId The project id
* @param props
* @param requestProps
* @returns
*/
export function useProject(projectId, props, requestProps) {
return useRequestQuery(
[t.PROJECT, projectId],
{ method: 'get', url: `projects/${projectId}`, ...requestProps },
{
select: (res) => res.data.project,
defaultData: {},
...props,
},
);
}
const transformProjects = (res) => ({
projects: res.data.projects,
});
/**
* Retrieve projects list with pagination meta.
* @param query
* @param props
*/
export function useProjects(query, props) {
return useRequestQuery(
[t.PROJECTS, query],
{ method: 'get', url: 'projects', params: query },
{
select: transformProjects,
defaultData: {
projects: [],
},
...props,
},
);
}
export function useRefreshInvoices() {
const queryClient = useQueryClient();
return {
refresh: () => {
queryClient.invalidateQueries(t.PROJECTS);
},
};
}

View File

@@ -0,0 +1,119 @@
import { useQueryClient, useMutation } from 'react-query';
import { useRequestQuery } from '@/hooks/useQueryRequest';
import useApiRequest from '@/hooks/useRequest';
import t from './type';
// Common invalidate queries.
const commonInvalidateQueries = (queryClient) => {
// Invalidate projects.
queryClient.invalidateQueries(t.PROJECTS);
// Invalidate project tasks.
queryClient.invalidateQueries(t.PROJECT_TASKS);
};
/**
* Create a new project task.
* @param props
*/
export function useCreateProjectTask(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation(
([id, values]) => apiRequest.post(`/projects/${id}/tasks`, values),
{
onSuccess: (res, [id, values]) => {
// Common invalidate queries.
commonInvalidateQueries(queryClient);
},
...props,
},
);
}
/**
* Edit the given project task.
* @param props
* @returns
*/
export function useEditProjectTask(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation(
([id, values]) => apiRequest.post(`tasks/${id}`, values),
{
onSuccess: (res, [id, values]) => {
// Invalidate specific project task.
queryClient.invalidateQueries([t.PROJECT_TASK, id]);
commonInvalidateQueries(queryClient);
},
...props,
},
);
}
/**
* Delete the given project task.
* @param props
*/
export function useDeleteProjectTask(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation((id) => apiRequest.delete(`tasks/${id}`), {
onSuccess: (res, id) => {
// Invalidate specific project task.
queryClient.invalidateQueries([t.PROJECT_TASK, id]);
// Common invalidate queries.
commonInvalidateQueries(queryClient);
},
...props,
});
}
/**
* Retrive the given project task.
* @param taskId
* @param props
* @param requestProps
* @returns
*/
export function useProjectTask(taskId, props, requestProps) {
return useRequestQuery(
[t.PROJECT, taskId],
{ method: 'get', url: `tasks/${taskId}`, ...requestProps },
{
select: (res) => res.data.task,
defaultData: {},
...props,
},
);
}
const transformProjectTasks = (res) => ({
projectTasks: res.data.tasks,
});
/**
*
* @param projectId - Project id.
* @param query
* @param requestProps
* @returns
*/
export function useProjectTasks(projectId, props, requestProps) {
return useRequestQuery(
[t.PROJECT_TASKS, projectId],
{ method: 'get', url: `projects/${projectId}/tasks`, ...requestProps },
{
select: transformProjectTasks,
defaultData: {
projectTasks: [],
},
...props,
},
);
}

View File

@@ -8,4 +8,9 @@ const PROJECTS = {
PROJECTS: 'PROJECTS',
};
export default { ...PROJECTS, ...CUSTOMERS };
const PROJECT_TASKS ={
PROJECT_TASKS:'PROJECT_TASKS',
PROJECT_TASK:'PROJECT_TASK',
}
export default { ...PROJECTS, ...CUSTOMERS,...PROJECT_TASKS };

View File

@@ -23,6 +23,7 @@ export default (mapState) => {
vendorsCreditNoteSetting: state.settings.data.vendorCredit,
warehouseTransferSettings: state.settings.data.warehouseTransfers,
projectSettings:state.settings.data.projects,
projectTasksSettings:state.settings.data.projectTasks,
timesheetsSettings:state.settings.data.timesheets
};
return mapState ? mapState(mapped, state, props) : mapped;

View File

@@ -2067,13 +2067,19 @@
"projects.empty_status.description":"",
"projects.empty_status.action":"New Project",
"project_task.dialog.new_task": "New Task",
"project_task.dialog.edit_task": "Edit Task",
"project_task.dialog.task_name": "Task Name",
"project_task.dialog.estimated_hours": "Estimate Hours",
"project_task.dialog.charge": "Charge",
"project_task.dialog.total": "Total",
"project_task.dialog.estimated_amount": "Estimated Amount",
"project_task.dialog.hourly_rate": "Hourly rate",
"project_task.dialog.fixed_price": "Fixed price",
"project_task.dialog.non_chargeable": "Non-chargeable",
"project_task.dialog.success_message":"The task has been created successfully.",
"project_task.dialog.edit_success_message":"The task has been created successfully.",
"project_task.action.edit_task": "Edit Task",
"project_task.action.delete_task": "Delete Task",
"project.schema.label.contact": "Contact",
"project.schema.label.project_name": "Project name",
"project.schema.label.deadline": "Deadline",
@@ -2081,12 +2087,14 @@
"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.charge": "Charge",
"project_task.schema.label.charge_type": "Charge type",
"project_task.schema.label.rate": "Rate",
"project_task.schema.label.amount": "Amount",
"projcet_details.action.new_transaction": "New Transaction",
"projcet_details.action.time_entry": "Time",
"projcet_details.action.edit_project": "Edit Project",
"project_details.label.overview": "Overview",
"project_details.label.tasks": "Tasks",
"project_details.label.timesheet": "Timesheet",
"project_details.label.purchases": "Purchases",
"project_details.label.sales": "Sales",

View File

@@ -61,6 +61,9 @@ const initialState = {
warehouseTransfer: {
tableSize: 'medium',
},
projectTasks: {
tableSize: 'medium',
},
},
};