diff --git a/src/containers/Projects/containers/ProjectAlerts/ProjectStatusAlert.tsx b/src/containers/Projects/containers/ProjectAlerts/ProjectStatusAlert.tsx new file mode 100644 index 000000000..886c65070 --- /dev/null +++ b/src/containers/Projects/containers/ProjectAlerts/ProjectStatusAlert.tsx @@ -0,0 +1,75 @@ +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 { useProjectStatus } from '../../hooks'; + +import withAlertStoreConnect from '@/containers/Alert/withAlertStoreConnect'; +import withAlertActions from '@/containers/Alert/withAlertActions'; + +import { compose } from '@/utils'; + +/** + * Project status alert. + * @returns + */ +function ProjectStatusAlert({ + name, + isOpen, + payload: { projectId, status }, + + // #withAlertActions + closeAlert, +}) { + const { mutateAsync: statusProjectMutate, isLoading } = useProjectStatus(); + + // handle cancel alert. + const handleCancelAlert = () => { + closeAlert(name); + }; + + // handle confirm alert. + const handleConfirmAlert = () => { + const values = { + status: status !== 'InProgress' ? 'InProgress' : 'Closed', + }; + + statusProjectMutate([projectId, values]) + .then(() => { + AppToaster.show({ + message: intl.get('projects.alert.status_message'), + intent: Intent.SUCCESS, + }); + }) + .catch( + ({ + response: { + data: { errors }, + }, + }) => {}, + ) + .finally(() => { + closeAlert(name); + }); + }; + + return ( + } + confirmButtonText={} + intent={Intent.WARNING} + isOpen={isOpen} + onCancel={handleCancelAlert} + onConfirm={handleConfirmAlert} + loading={isLoading} + > + + + ); +} + +export default compose( + withAlertStoreConnect(), + withAlertActions, +)(ProjectStatusAlert); diff --git a/src/containers/Projects/containers/ProjectAlerts/index.ts b/src/containers/Projects/containers/ProjectAlerts/index.ts index 17a6acf14..f5c255e5b 100644 --- a/src/containers/Projects/containers/ProjectAlerts/index.ts +++ b/src/containers/Projects/containers/ProjectAlerts/index.ts @@ -8,6 +8,8 @@ const ProjectTimesheetDeleteAlert = React.lazy( () => import('./ProjectTimesheetDeleteAlert'), ); +const ProjectStatusAlert = React.lazy(() => import('./ProjectStatusAlert')); + /** * Project alerts. */ @@ -15,4 +17,5 @@ export default [ { name: 'project-delete', component: ProjectDeleteAlert }, { name: 'project-task-delete', component: ProjectTaskDeleteAlert }, { name: 'project-timesheet-delete', component: ProjectTimesheetDeleteAlert }, + { name: 'project-status', component: ProjectStatusAlert }, ]; diff --git a/src/containers/Projects/containers/ProjectDetails/ProjectTasks/ProjectTasksHeader.tsx b/src/containers/Projects/containers/ProjectDetails/ProjectTasks/ProjectTasksHeader.tsx index 2acf0531e..f6e86bd66 100644 --- a/src/containers/Projects/containers/ProjectDetails/ProjectTasks/ProjectTasksHeader.tsx +++ b/src/containers/Projects/containers/ProjectDetails/ProjectTasks/ProjectTasksHeader.tsx @@ -35,7 +35,7 @@ export function ProjectTasksHeader() { } + value={} > 4 days to go diff --git a/src/containers/Projects/containers/ProjectDetails/common.ts b/src/containers/Projects/containers/ProjectDetails/common.ts index aa811c2f0..d734ddbcb 100644 --- a/src/containers/Projects/containers/ProjectDetails/common.ts +++ b/src/containers/Projects/containers/ProjectDetails/common.ts @@ -1,9 +1,9 @@ import intl from 'react-intl-universal'; export const projectTranslations = [ - { name: intl.get('project_details.new_expenses'), path: 'expense' }, + { name: intl.get('project_details.new_expense'), path: 'expense' }, { - name: intl.get('project_details.new_estimated_expenses'), + name: intl.get('project_details.new_estimated_expense'), path: 'estimated_expense', }, ]; diff --git a/src/containers/Projects/containers/ProjectsLanding/ProjectsDataTable.tsx b/src/containers/Projects/containers/ProjectsLanding/ProjectsDataTable.tsx index ecc89f3d5..9ed376a98 100644 --- a/src/containers/Projects/containers/ProjectsLanding/ProjectsDataTable.tsx +++ b/src/containers/Projects/containers/ProjectsLanding/ProjectsDataTable.tsx @@ -43,6 +43,10 @@ function ProjectsDataTable({ openAlert('project-delete', { projectId: id }); }; + const handleProjectStatus = ({ id, status_formatted }) => { + openAlert('project-status', { projectId: id, status: status_formatted }); + }; + // Retrieve projects table columns. const columns = useProjectsListColumns(); @@ -109,6 +113,7 @@ function ProjectsDataTable({ onEdit: handleEditProject, onDelete: handleDeleteProject, onNewTask: handleNewTaskButtonClick, + onStatus: handleProjectStatus, }} /> ); diff --git a/src/containers/Projects/containers/ProjectsLanding/components.tsx b/src/containers/Projects/containers/ProjectsLanding/components.tsx index 5a6fccea2..0eb8e1929 100644 --- a/src/containers/Projects/containers/ProjectsLanding/components.tsx +++ b/src/containers/Projects/containers/ProjectsLanding/components.tsx @@ -10,21 +10,27 @@ import { Intent, ProgressBar, } from '@blueprintjs/core'; -import { Icon, FormatDate, Choose, FormattedMessage as T } from '@/components'; +import { + Icon, + FormatDate, + Choose, + If, + FormattedMessage as T, +} from '@/components'; import { safeCallback, firstLettersArgs, calculateStatus } from '@/utils'; /** * project status. */ -export function ProjectStatus({ project }) { +export function ProjectStatus({ row }) { return ( - {project.task_amount} + {row.cost_estimate} ); @@ -33,22 +39,22 @@ export function ProjectStatus({ project }) { /** * status accessor. */ -export const StatusAccessor = (project) => { +export const StatusAccessor = (row) => { return ( - - + + - + - + {row.status_formatted} - - + + - + ); }; @@ -67,7 +73,7 @@ export const AvatarCell = ({ row: { original }, size }) => ( */ export const ActionsMenu = ({ row: { original }, - payload: { onEdit, onDelete, onViewDetails, onNewTask }, + payload: { onEdit, onDelete, onViewDetails, onNewTask, onStatus }, }) => ( + + }> + + + + + + + + apiRequest.patch(`projects/${id}/status`, values), + { + onSuccess: (res, [id, values]) => { + // Invalidate specific project. + queryClient.invalidateQueries([t.PROJECT, id]); + + commonInvalidateQueries(queryClient); + }, + ...props, + }, + ); +} + +export function useRefreshProjects() { const queryClient = useQueryClient(); return { diff --git a/src/hooks/useRequest.tsx b/src/hooks/useRequest.tsx index aae07bb13..7f8ecb2ea 100644 --- a/src/hooks/useRequest.tsx +++ b/src/hooks/useRequest.tsx @@ -66,7 +66,7 @@ export default function useApiRequest() { if (lockedError) { setGlobalErrors({ transactionsLocked: { ...lockedError.data } }); } - if (data.errors.find(e => e.type === 'USER_INACTIVE')) { + if (data.errors.find((e) => e.type === 'USER_INACTIVE')) { setGlobalErrors({ userInactive: true }); setLogout(); } @@ -97,6 +97,10 @@ export default function useApiRequest() { return http.put(`/api/${resource}`, params); }, + patch(resource, params, config) { + return http.patch(`/api/${resource}`, params, config); + }, + delete(resource, params) { return http.delete(`/api/${resource}`, params); }, diff --git a/src/lang/en/index.json b/src/lang/en/index.json index b46137395..043c02d88 100644 --- a/src/lang/en/index.json +++ b/src/lang/en/index.json @@ -2064,6 +2064,8 @@ "projects.dialog.edit_project": "Edit Project", "projects.alert.delete_message": "The deleted project has been deleted successfully.", "projects.alert.once_delete_this_project": "Once you delete this project, you won't be able to restore it later. Are you sure you want to delete this project?", + "projects.alert.status_message": "The project has been edited successfully.", + "projects.alert.are_you_sure_you_want":"Are you sure you want to edit this project?", "projects.empty_status.title": "", "projects.empty_status.description": "", "projects.empty_status.action": "New Project", @@ -2078,7 +2080,7 @@ "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.dialog.edit_success_message": "The task has been edited successfully.", "project_task.action.edit_task": "Edit Task", "project_task.action.delete_task": "Delete Task", "project_task.rate": "{rate} /hour", @@ -2106,15 +2108,17 @@ "project_details.label.purchases": "Purchases", "project_details.label.sales": "Sales", "project_details.label.journals": "Journals", - "project_details.new_expenses": "New Expenses", - "project_details.new_estimated_expenses": "New Estimated Expenses", - "timesheets.actions.delete_timesheet": "Delete", + "project_details.new_expense": "New Expense", + "project_details.new_estimated_expense": "New Estimated Expense", + "timesheets.action.delete_timesheet": "Delete", + "timesheets.action.edit_timesheet": "Edit Timesheet", "timesheets.column.date": "Date", "timesheets.column.task": "Task", "timesheets.column.user": "User", "timesheets.column.time": "Time", "timesheets.column.billing_status": "Billing Status", - "project_time_entry.dialog.label": "New Time Entry", + "project_time_entry.dialog.new_time_entry": "New Time Entry", + "project_time_entry.dialog.edit_time_entry": "Edit Time Entry", "project_time_entry.dialog.project": "Project", "project_time_entry.dialog.task": "Task", "project_time_entry.dialog.description": "Description",