diff --git a/src/constants/tables.tsx b/src/constants/tables.tsx
index c6927fad7..e55aae3d8 100644
--- a/src/constants/tables.tsx
+++ b/src/constants/tables.tsx
@@ -19,6 +19,7 @@ export const TABLES = {
WAREHOUSE_TRANSFERS: 'warehouse_transfers',
PROJECTS: 'projects',
TIMESHEETS: 'timesheets',
+ PROJECT_TASKS: 'project_tasks',
};
export const TABLE_SIZE = {
diff --git a/src/containers/Projects/containers/ProjectAlerts/ProjectTaskDeleteAlert.tsx b/src/containers/Projects/containers/ProjectAlerts/ProjectTaskDeleteAlert.tsx
new file mode 100644
index 000000000..86d4fa9f1
--- /dev/null
+++ b/src/containers/Projects/containers/ProjectAlerts/ProjectTaskDeleteAlert.tsx
@@ -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 (
+ }
+ confirmButtonText={}
+ icon="trash"
+ intent={Intent.DANGER}
+ isOpen={isOpen}
+ onCancel={handleCancelDeleteAlert}
+ onConfirm={handleConfirmProjectDelete}
+ loading={isLoading}
+ >
+
+
+
+
+ );
+}
+
+export default compose(
+ withAlertStoreConnect(),
+ withAlertActions,
+)(ProjectTaskDeleteAlert);
diff --git a/src/containers/Projects/containers/ProjectAlerts/index.ts b/src/containers/Projects/containers/ProjectAlerts/index.ts
index 8900c1269..eecc2e6bb 100644
--- a/src/containers/Projects/containers/ProjectAlerts/index.ts
+++ b/src/containers/Projects/containers/ProjectAlerts/index.ts
@@ -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 },
+];
diff --git a/src/containers/Projects/containers/ProjectDetails/ProjectDetailActionsBar.tsx b/src/containers/Projects/containers/ProjectDetails/ProjectDetailActionsBar.tsx
index 46e0d67fa..560ddf68c 100644
--- a/src/containers/Projects/containers/ProjectDetails/ProjectDetailActionsBar.tsx
+++ b/src/containers/Projects/containers/ProjectDetails/ProjectDetailActionsBar.tsx
@@ -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 = () => {
diff --git a/src/containers/Projects/containers/ProjectDetails/ProjectDetailTabs.tsx b/src/containers/Projects/containers/ProjectDetails/ProjectDetailTabs.tsx
index 51f4c7c92..b83d9c975 100644
--- a/src/containers/Projects/containers/ProjectDetails/ProjectDetailTabs.tsx
+++ b/src/containers/Projects/containers/ProjectDetails/ProjectDetailTabs.tsx
@@ -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'}
>
+ }
+ />
;
+}
+
+const useProjectTaskContext = () => React.useContext(ProjectTaskContext);
+
+export { ProjectTaskProvider, useProjectTaskContext };
diff --git a/src/containers/Projects/containers/ProjectDetails/ProjectTasks/ProjectTasksHeader.tsx b/src/containers/Projects/containers/ProjectDetails/ProjectTasks/ProjectTasksHeader.tsx
new file mode 100644
index 000000000..057b92781
--- /dev/null
+++ b/src/containers/Projects/containers/ProjectDetails/ProjectTasks/ProjectTasksHeader.tsx
@@ -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 (
+
+
+
+ 0% of project estimate
+
+
+
+ 0% of project estimate
+
+
+
+
+ }
+ >
+ 4 days to go
+
+
+ );
+}
diff --git a/src/containers/Projects/containers/ProjectDetails/ProjectTasks/ProjectTasksTable.tsx b/src/containers/Projects/containers/ProjectDetails/ProjectTasks/ProjectTasksTable.tsx
new file mode 100644
index 000000000..921167d70
--- /dev/null
+++ b/src/containers/Projects/containers/ProjectDetails/ProjectTasks/ProjectTasksTable.tsx
@@ -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 (
+
+ );
+}
+
+export const ProjectTasksTable = compose(
+ withAlertsActions,
+ withDialogActions,
+ withSettings(({ projectTasksSettings }) => ({
+ projectTasksTableSize: projectTasksSettings?.tableSize,
+ })),
+)(ProjectTaskTableRoot);
+
+const ProjectTaksDataTable = styled(DataTable)`
+ .table {
+ .thead .tr .th {
+ .resizer {
+ display: none;
+ }
+ }
+ }
+`;
diff --git a/src/containers/Projects/containers/ProjectDetails/ProjectTasks/components.tsx b/src/containers/Projects/containers/ProjectDetails/ProjectTasks/components.tsx
new file mode 100644
index 000000000..1f5d380e5
--- /dev/null
+++ b/src/containers/Projects/containers/ProjectDetails/ProjectTasks/components.tsx
@@ -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 (
+
+ );
+}
+
+export function TaskAccessor(row) {
+ return (
+
+
+ {row.name}
+
+
+ {row.charge_type === 'hourly_rate'
+ ? row.rate + ' / hour'
+ : row.charge_type}
+ {row.estimate_minutes} estimated
+
+
+ );
+}
+
+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;
+ }
+`;
diff --git a/src/containers/Projects/containers/ProjectDetails/ProjectTasks/hooks.ts b/src/containers/Projects/containers/ProjectDetails/ProjectTasks/hooks.ts
new file mode 100644
index 000000000..74ae32ad9
--- /dev/null
+++ b/src/containers/Projects/containers/ProjectDetails/ProjectTasks/hooks.ts
@@ -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,
+ },
+ ],
+ [],
+ );
+}
diff --git a/src/containers/Projects/containers/ProjectDetails/ProjectTasks/index.tsx b/src/containers/Projects/containers/ProjectDetails/ProjectTasks/index.tsx
new file mode 100644
index 000000000..1a4f0f555
--- /dev/null
+++ b/src/containers/Projects/containers/ProjectDetails/ProjectTasks/index.tsx
@@ -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 (
+
+
+
+
+
+
+ );
+}
+
+const ProjectTasksTableCard = styled.div`
+ margin: 22px 32px;
+ border: 1px solid #c8cad0;
+ border-radius: 3px;
+ background: #fff;
+`;
diff --git a/src/containers/Projects/containers/ProjectDetails/ProjectTimeSheets/ProjectTimesheetsHeader.tsx b/src/containers/Projects/containers/ProjectDetails/ProjectTimeSheets/ProjectTimesheetsHeader.tsx
index d5d0d9f7a..609ee7908 100644
--- a/src/containers/Projects/containers/ProjectDetails/ProjectTimeSheets/ProjectTimesheetsHeader.tsx
+++ b/src/containers/Projects/containers/ProjectDetails/ProjectTimeSheets/ProjectTimesheetsHeader.tsx
@@ -1,4 +1,3 @@
-//@ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import styled from 'styled-components';
diff --git a/src/containers/Projects/containers/ProjectTaskFormDialog/ProjectTaskForm.schema.tsx b/src/containers/Projects/containers/ProjectTaskFormDialog/ProjectTaskForm.schema.tsx
index 779843ccc..939a69ce5 100644
--- a/src/containers/Projects/containers/ProjectTaskFormDialog/ProjectTaskForm.schema.tsx
+++ b/src/containers/Projects/containers/ProjectTaskFormDialog/ProjectTaskForm.schema.tsx
@@ -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;
diff --git a/src/containers/Projects/containers/ProjectTaskFormDialog/ProjectTaskForm.tsx b/src/containers/Projects/containers/ProjectTaskFormDialog/ProjectTaskForm.tsx
index cb8f261cd..6df0a988b 100644
--- a/src/containers/Projects/containers/ProjectTaskFormDialog/ProjectTaskForm.tsx
+++ b/src/containers/Projects/containers/ProjectTaskFormDialog/ProjectTaskForm.tsx
@@ -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 (
diff --git a/src/containers/Projects/containers/ProjectTaskFormDialog/ProjectTaskFormDialogContent.tsx b/src/containers/Projects/containers/ProjectTaskFormDialog/ProjectTaskFormDialogContent.tsx
index a72bff479..4b97b1278 100644
--- a/src/containers/Projects/containers/ProjectTaskFormDialog/ProjectTaskFormDialogContent.tsx
+++ b/src/containers/Projects/containers/ProjectTaskFormDialog/ProjectTaskFormDialogContent.tsx
@@ -9,9 +9,14 @@ export default function ProjectTaskFormDialogContent({
// #ownProps
dialogName,
task,
+ project,
}) {
return (
-
+
);
diff --git a/src/containers/Projects/containers/ProjectTaskFormDialog/ProjectTaskFormFields.tsx b/src/containers/Projects/containers/ProjectTaskFormDialog/ProjectTaskFormFields.tsx
index 8af1a45bd..5bbee5ed6 100644
--- a/src/containers/Projects/containers/ProjectTaskFormDialog/ProjectTaskFormFields.tsx
+++ b/src/containers/Projects/containers/ProjectTaskFormDialog/ProjectTaskFormFields.tsx
@@ -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 (
{/*------------ Task Name -----------*/}
- } name={'taskName'}>
-
+ }
+ name={'taskName'}
+ >
+
{/*------------ Estimated Hours -----------*/}
}
- name={'taskHouse'}
+ name={'estimate_minutes'}
>
-
+
{/*------------ Charge -----------*/}
}
>
{/*------------ Estimated Amount -----------*/}
-
-
-
- 0.00
-
-
+
);
}
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;
-`;
diff --git a/src/containers/Projects/containers/ProjectTaskFormDialog/ProjectTaskFormProvider.tsx b/src/containers/Projects/containers/ProjectTaskFormDialog/ProjectTaskFormProvider.tsx
index 3d8eea7ce..8f417f1f3 100644
--- a/src/containers/Projects/containers/ProjectTaskFormDialog/ProjectTaskFormProvider.tsx
+++ b/src/containers/Projects/containers/ProjectTaskFormDialog/ProjectTaskFormProvider.tsx
@@ -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 (
-
+
);
diff --git a/src/containers/Projects/containers/ProjectTaskFormDialog/index.tsx b/src/containers/Projects/containers/ProjectTaskFormDialog/index.tsx
index a278b2778..6aa50661d 100644
--- a/src/containers/Projects/containers/ProjectTaskFormDialog/index.tsx
+++ b/src/containers/Projects/containers/ProjectTaskFormDialog/index.tsx
@@ -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 (
+ ) : (
+
+ )
+ }
isOpen={isOpen}
autoFocus={true}
canEscapeKeyClose={true}
style={{ width: '500px' }}
>
-
+
);
diff --git a/src/containers/Projects/containers/ProjectTaskFormDialog/utils.tsx b/src/containers/Projects/containers/ProjectTaskFormDialog/utils.tsx
new file mode 100644
index 000000000..5c1e0416c
--- /dev/null
+++ b/src/containers/Projects/containers/ProjectTaskFormDialog/utils.tsx
@@ -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 (
+
+
+
+
+
+ {estimatedAmount}
+
+
+
+ {values.rate}
+
+
+
+ 0.00
+
+
+
+
+ );
+}
+
+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;
+`;
diff --git a/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryForm.schema.tsx b/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryForm.schema.tsx
index 1facb1c11..8177df2ad 100644
--- a/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryForm.schema.tsx
+++ b/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryForm.schema.tsx
@@ -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()
diff --git a/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryFormDialogContent.tsx b/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryFormDialogContent.tsx
index 1b48e95d8..262932d00 100644
--- a/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryFormDialogContent.tsx
+++ b/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryFormDialogContent.tsx
@@ -10,14 +10,9 @@ export default function ProjectTimeEntryFormDialogContent({
// #ownProps
dialogName,
project,
- timeEntry,
}) {
return (
-
+
);
diff --git a/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryFormProvider.tsx b/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryFormProvider.tsx
index 3568ffbd5..40c4ee5e9 100644
--- a/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryFormProvider.tsx
+++ b/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryFormProvider.tsx
@@ -11,7 +11,6 @@ function ProjectTimeEntryFormProvider({
// #ownProps
dialogName,
projectId,
- timeEntryId,
...props
}) {
const provider = {
diff --git a/src/containers/Projects/containers/ProjectTimeEntryFormDialog/index.tsx b/src/containers/Projects/containers/ProjectTimeEntryFormDialog/index.tsx
index de303cdd2..094ef8e0e 100644
--- a/src/containers/Projects/containers/ProjectTimeEntryFormDialog/index.tsx
+++ b/src/containers/Projects/containers/ProjectTimeEntryFormDialog/index.tsx
@@ -15,7 +15,7 @@ const ProjectTimeEntryFormDialogContent = React.lazy(
function ProjectTimeEntryFormDialog({
dialogName,
isOpen,
- payload: { projectId = null, timeEntryId = null },
+ payload: { projectId },
}) {
return (
diff --git a/src/containers/Projects/containers/ProjectsLanding/ProjectsDataTable.tsx b/src/containers/Projects/containers/ProjectsLanding/ProjectsDataTable.tsx
index 696bfb7b2..ecc89f3d5 100644
--- a/src/containers/Projects/containers/ProjectsLanding/ProjectsDataTable.tsx
+++ b/src/containers/Projects/containers/ProjectsLanding/ProjectsDataTable.tsx
@@ -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.
diff --git a/src/containers/Projects/containers/common/modalChargeOptions.ts b/src/containers/Projects/containers/common/modalChargeOptions.ts
index a7232554f..025175ba3 100644
--- a/src/containers/Projects/containers/common/modalChargeOptions.ts
+++ b/src/containers/Projects/containers/common/modalChargeOptions.ts
@@ -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 = [
diff --git a/src/containers/Projects/hooks/index.ts b/src/containers/Projects/hooks/index.ts
index 0ee1f1604..77427adfb 100644
--- a/src/containers/Projects/hooks/index.ts
+++ b/src/containers/Projects/hooks/index.ts
@@ -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'
\ No newline at end of file
diff --git a/src/containers/Projects/hooks/projects.ts b/src/containers/Projects/hooks/projects.ts
new file mode 100644
index 000000000..0ee1f1604
--- /dev/null
+++ b/src/containers/Projects/hooks/projects.ts
@@ -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);
+ },
+ };
+}
diff --git a/src/containers/Projects/hooks/projectsTask.tsx b/src/containers/Projects/hooks/projectsTask.tsx
new file mode 100644
index 000000000..64ee669bc
--- /dev/null
+++ b/src/containers/Projects/hooks/projectsTask.tsx
@@ -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,
+ },
+ );
+}
diff --git a/src/containers/Projects/hooks/type.ts b/src/containers/Projects/hooks/type.ts
index cd2c44196..40ccea949 100644
--- a/src/containers/Projects/hooks/type.ts
+++ b/src/containers/Projects/hooks/type.ts
@@ -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 };
diff --git a/src/containers/Settings/withSettings.tsx b/src/containers/Settings/withSettings.tsx
index 60091face..6aac1c8ed 100644
--- a/src/containers/Settings/withSettings.tsx
+++ b/src/containers/Settings/withSettings.tsx
@@ -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;
diff --git a/src/lang/en/index.json b/src/lang/en/index.json
index c0bb39ade..d70c68bfc 100644
--- a/src/lang/en/index.json
+++ b/src/lang/en/index.json
@@ -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",
diff --git a/src/store/settings/settings.reducer.tsx b/src/store/settings/settings.reducer.tsx
index 882be4392..bd7eb5b1d 100644
--- a/src/store/settings/settings.reducer.tsx
+++ b/src/store/settings/settings.reducer.tsx
@@ -61,6 +61,9 @@ const initialState = {
warehouseTransfer: {
tableSize: 'medium',
},
+ projectTasks: {
+ tableSize: 'medium',
+ },
},
};