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 ( + + } + text={intl.get('project_task.action.edit_task')} + onClick={safeCallback(onEdit, original)} + /> + } + /> + + ); +} + +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', + }, }, };