diff --git a/src/containers/Projects/containers/ProjectAlerts/ProjectTaskDeleteAlert.tsx b/src/containers/Projects/containers/ProjectAlerts/ProjectTaskDeleteAlert.tsx index 9a320da76..4cbfa3b21 100644 --- a/src/containers/Projects/containers/ProjectAlerts/ProjectTaskDeleteAlert.tsx +++ b/src/containers/Projects/containers/ProjectAlerts/ProjectTaskDeleteAlert.tsx @@ -32,8 +32,8 @@ function ProjectTaskDeleteAlert({ closeAlert(name); }; - // handleConfirm delete project - const handleConfirmProjectDelete = () => { + // handleConfirm delete project task + const handleConfirmProjectTaskDelete = () => { deleteProjectTaskMutate(taskId) .then(() => { AppToaster.show({ @@ -61,7 +61,7 @@ function ProjectTaskDeleteAlert({ intent={Intent.DANGER} isOpen={isOpen} onCancel={handleCancelDeleteAlert} - onConfirm={handleConfirmProjectDelete} + onConfirm={handleConfirmProjectTaskDelete} loading={isLoading} >

diff --git a/src/containers/Projects/containers/ProjectAlerts/ProjectTimesheetDeleteAlert.tsx b/src/containers/Projects/containers/ProjectAlerts/ProjectTimesheetDeleteAlert.tsx new file mode 100644 index 000000000..7d9789270 --- /dev/null +++ b/src/containers/Projects/containers/ProjectAlerts/ProjectTimesheetDeleteAlert.tsx @@ -0,0 +1,78 @@ +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 { useDeleteProjectTimeEntry } from '../../hooks'; + +import withAlertStoreConnect from '@/containers/Alert/withAlertStoreConnect'; +import withAlertActions from '@/containers/Alert/withAlertActions'; + +import { compose } from '@/utils'; + +/** + * Project timesheet delete alert. + * @returns + */ +function ProjectTimesheetDeleteAlert({ + name, + + // #withAlertStoreConnect + isOpen, + payload: { timesheetId }, + + // #withAlertActions + closeAlert, +}) { + const { mutateAsync: deleteProjectTimeEntryMutate, isLoading } = + useDeleteProjectTimeEntry(); + + // handle cancel delete alert. + const handleCancelDeleteAlert = () => { + closeAlert(name); + }; + + // handleConfirm delete project time sheet. + const handleConfirmProjectTimesheetDelete = () => { + deleteProjectTimeEntryMutate(timesheetId) + .then(() => { + AppToaster.show({ + message: intl.get('project_time_entry.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={handleConfirmProjectTimesheetDelete} + loading={isLoading} + > +

+ +

+ + ); +} +export default compose( + withAlertStoreConnect(), + withAlertActions, +)(ProjectTimesheetDeleteAlert); diff --git a/src/containers/Projects/containers/ProjectAlerts/index.ts b/src/containers/Projects/containers/ProjectAlerts/index.ts index eecc2e6bb..17a6acf14 100644 --- a/src/containers/Projects/containers/ProjectAlerts/index.ts +++ b/src/containers/Projects/containers/ProjectAlerts/index.ts @@ -4,6 +4,9 @@ const ProjectDeleteAlert = React.lazy(() => import('./ProjectDeleteAlert')); const ProjectTaskDeleteAlert = React.lazy( () => import('./ProjectTaskDeleteAlert'), ); +const ProjectTimesheetDeleteAlert = React.lazy( + () => import('./ProjectTimesheetDeleteAlert'), +); /** * Project alerts. @@ -11,4 +14,5 @@ const ProjectTaskDeleteAlert = React.lazy( export default [ { name: 'project-delete', component: ProjectDeleteAlert }, { name: 'project-task-delete', component: ProjectTaskDeleteAlert }, + { name: 'project-timesheet-delete', component: ProjectTimesheetDeleteAlert }, ]; diff --git a/src/containers/Projects/containers/ProjectDetails/ProjectDetailActionsBar.tsx b/src/containers/Projects/containers/ProjectDetails/ProjectDetailActionsBar.tsx index 560ddf68c..be33fc8a3 100644 --- a/src/containers/Projects/containers/ProjectDetails/ProjectDetailActionsBar.tsx +++ b/src/containers/Projects/containers/ProjectDetails/ProjectDetailActionsBar.tsx @@ -35,7 +35,6 @@ function ProjectDetailActionsBar({ // #withSettingsActions addSetting, }) { - const history = useHistory(); const { projectId } = useProjectDetailContext(); // Handle new transaction button click. diff --git a/src/containers/Projects/containers/ProjectDetails/ProjectTasks/ProjectTaskProvider.tsx b/src/containers/Projects/containers/ProjectDetails/ProjectTasks/ProjectTaskProvider.tsx index 8b30aba57..840ca0804 100644 --- a/src/containers/Projects/containers/ProjectDetails/ProjectTasks/ProjectTaskProvider.tsx +++ b/src/containers/Projects/containers/ProjectDetails/ProjectTasks/ProjectTaskProvider.tsx @@ -26,7 +26,7 @@ function ProjectTaskProvider({ ...props }) { enabled: !!projectId, }); - console.log(project, 'XX'); + // provider payload. const provider = { project, diff --git a/src/containers/Projects/containers/ProjectDetails/ProjectTimeSheets/ProjectTimesheetsProvider.tsx b/src/containers/Projects/containers/ProjectDetails/ProjectTimeSheets/ProjectTimesheetsProvider.tsx new file mode 100644 index 000000000..c047782b7 --- /dev/null +++ b/src/containers/Projects/containers/ProjectDetails/ProjectTimeSheets/ProjectTimesheetsProvider.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { useParams } from 'react-router-dom'; +import { useProject } from '../../../hooks'; + +const ProjectTimesheetContext = React.createContext(); + +/** + * Project timesheets data provider. + * @returns + */ +function ProjectTimesheetsProvider({ ...props }) { + const { id } = useParams(); + const projectId = parseInt(id, 10); + + // Handle fetch project detail. + const { data: project } = useProject(projectId, { + enabled: !!projectId, + }); + + // provider payload. + const provider = { + projectId, + project, + }; + + return ; +} + +const useProjectTimesheetContext = () => + React.useContext(ProjectTimesheetContext); + +export { ProjectTimesheetsProvider, useProjectTimesheetContext }; diff --git a/src/containers/Projects/containers/ProjectDetails/ProjectTimeSheets/index.tsx b/src/containers/Projects/containers/ProjectDetails/ProjectTimeSheets/index.tsx index 1e443e5a8..62b1872e1 100644 --- a/src/containers/Projects/containers/ProjectDetails/ProjectTimeSheets/index.tsx +++ b/src/containers/Projects/containers/ProjectDetails/ProjectTimeSheets/index.tsx @@ -3,6 +3,7 @@ import styled from 'styled-components'; import { ProjectTimesheetsTable } from './ProjectTimesheetsTable'; import { ProjectTimesheetsHeader } from './ProjectTimesheetsHeader'; +import { ProjectTimesheetsProvider } from './ProjectTimesheetsProvider'; /** * Project Timesheets. @@ -10,12 +11,12 @@ import { ProjectTimesheetsHeader } from './ProjectTimesheetsHeader'; */ export default function ProjectTimeSheets() { return ( - + - + ); } diff --git a/src/containers/Projects/containers/ProjectTaskFormDialog/ProjectTaskFormProvider.tsx b/src/containers/Projects/containers/ProjectTaskFormDialog/ProjectTaskFormProvider.tsx index 8f417f1f3..355c2efd1 100644 --- a/src/containers/Projects/containers/ProjectTaskFormDialog/ProjectTaskFormProvider.tsx +++ b/src/containers/Projects/containers/ProjectTaskFormDialog/ProjectTaskFormProvider.tsx @@ -31,6 +31,9 @@ function ProjectTaskFormProvider({ }, ); + console.log(taskId, 'XX'); + console.log(projectTask, 'XX'); + const isNewMode = !taskId; // State provider. const provider = { diff --git a/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryForm.schema.tsx b/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryForm.schema.tsx index 8177df2ad..c75f29c08 100644 --- a/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryForm.schema.tsx +++ b/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryForm.schema.tsx @@ -6,12 +6,12 @@ const Schema = Yup.object().shape({ date: Yup.date() .label(intl.get('project_time_entry.schema.label.date')) .required(), - projectId: Yup.string() - .label(intl.get('project_time_entry.schema.label.project_name')) - .required(), - taskId: Yup.string() - .label(intl.get('project_time_entry.schema.label.task_name')) - .required(), + // projectId: Yup.string() + // .label(intl.get('project_time_entry.schema.label.project_name')) + // .required(), + // taskId: Yup.string() + // .label(intl.get('project_time_entry.schema.label.task_name')) + // .required(), description: Yup.string().nullable().max(DATATYPES_LENGTH.TEXT), duration: Yup.string() .label(intl.get('project_time_entry.schema.label.duration')) diff --git a/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryForm.tsx b/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryForm.tsx index f81b719e4..679ce35f2 100644 --- a/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryForm.tsx +++ b/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryForm.tsx @@ -1,6 +1,7 @@ import React from 'react'; import moment from 'moment'; import intl from 'react-intl-universal'; +import { Intent } from '@blueprintjs/core'; import { Formik } from 'formik'; import { AppToaster } from '@/components'; @@ -13,7 +14,7 @@ import { compose } from '@/utils'; const defaultInitialValues = { date: moment(new Date()).format('YYYY-MM-DD'), - projectId: '', + // projectId: '', taskId: '', description: '', duration: '', @@ -28,7 +29,11 @@ function ProjectTimeEntryForm({ closeDialog, }) { // time entry form dialog context. - const { dialogName } = useProjectTimeEntryFormContext(); + const { + dialogName, + createProjectTimeEntryMutate, + editProjectTimeEntryMutate, + } = useProjectTimeEntryFormContext(); // Initial form values const initialValues = { @@ -37,11 +42,21 @@ function ProjectTimeEntryForm({ // Handles the form submit. const handleFormSubmit = (values, { setSubmitting, setErrors }) => { - const form = {}; + const form = { + ...values, + }; // Handle request response success. const onSuccess = (response) => { - AppToaster.show({}); + AppToaster.show({ + message: intl.get( + true + ? 'project_time_entry.success_message' + : 'project_time_entry.dialog.edit_success_message', + ), + + intent: Intent.SUCCESS, + }); closeDialog(dialogName); }; @@ -53,6 +68,9 @@ function ProjectTimeEntryForm({ }) => { setSubmitting(false); }; + createProjectTimeEntryMutate([values.taskId, form]) + .then(onSuccess) + .catch(onError); }; return ( diff --git a/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryFormFields.tsx b/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryFormFields.tsx index ad12187b1..0ec18a5d4 100644 --- a/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryFormFields.tsx +++ b/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryFormFields.tsx @@ -9,10 +9,10 @@ import { FInputGroup, FDateInput, FTextArea, - FEditableText, FieldRequiredHint, FormattedMessage as T, } from '@/components'; +import { useProjectTimeEntryFormContext } from './ProjectTimeEntryFormProvider'; import { TaskSelect, ProjectsSelect } from '../../components'; import { momentFormatter } from '@/utils'; @@ -21,6 +21,9 @@ import { momentFormatter } from '@/utils'; * @returns */ function ProjectTimeEntryFormFields() { + // time entry form dialog context. + const { projectTasks } = useProjectTimeEntryFormContext(); + return (
{/*------------ Project -----------*/} @@ -45,7 +48,7 @@ function ProjectTimeEntryFormFields() { > diff --git a/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryFormFloatingActions.tsx b/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryFormFloatingActions.tsx index 7589d406b..cf0cf6bca 100644 --- a/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryFormFloatingActions.tsx +++ b/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryFormFloatingActions.tsx @@ -18,8 +18,10 @@ function ProjectTimeEntryFormFloatingActions({ const { dialogName } = useProjectTimeEntryFormContext(); // Formik context. - const { isSubmitting } = useFormikContext(); - + const { isSubmitting, values, errors } = useFormikContext(); + console.log(values, 'XX'); + console.log(errors, 'XX'); + // Handle close button click. const handleCancelBtnClick = () => { closeDialog(dialogName); diff --git a/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryFormProvider.tsx b/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryFormProvider.tsx index 40c4ee5e9..0ce82d96e 100644 --- a/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryFormProvider.tsx +++ b/src/containers/Projects/containers/ProjectTimeEntryFormDialog/ProjectTimeEntryFormProvider.tsx @@ -1,4 +1,9 @@ import React from 'react'; +import { + useProjectTasks, + useCreateProjectTimeEntry, + useEditProjectTimeEntry, +} from '../../hooks'; import { DialogContent } from '@/components'; const ProjecctTimeEntryFormContext = React.createContext(); @@ -13,12 +18,30 @@ function ProjectTimeEntryFormProvider({ projectId, ...props }) { + // Create and edit project time entry mutations. + const { mutateAsync: createProjectTimeEntryMutate } = + useCreateProjectTimeEntry(); + const { mutateAsync: editProjectTimeEntryMutate } = useEditProjectTimeEntry(); + + // Handle fetch project tasks. + const { + data: { projectTasks }, + isLoading: isProjectTasksLoading, + } = useProjectTasks(projectId, { + enabled: !!projectId, + }); + + // provider payload. const provider = { dialogName, + projectId, + projectTasks, + createProjectTimeEntryMutate, + editProjectTimeEntryMutate, }; return ( - + ); diff --git a/src/containers/Projects/hooks/index.ts b/src/containers/Projects/hooks/index.ts index 77427adfb..544277616 100644 --- a/src/containers/Projects/hooks/index.ts +++ b/src/containers/Projects/hooks/index.ts @@ -1,2 +1,3 @@ export * from './projects' -export * from './projectsTask' \ No newline at end of file +export * from './projectsTask' +export * from './projectTimeEntry' \ No newline at end of file diff --git a/src/containers/Projects/hooks/projectTimeEntry.tsx b/src/containers/Projects/hooks/projectTimeEntry.tsx new file mode 100644 index 000000000..6499c19ad --- /dev/null +++ b/src/containers/Projects/hooks/projectTimeEntry.tsx @@ -0,0 +1,120 @@ +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 entries. + queryClient.invalidateQueries(t.PROJECT_TIME_ENTRIES); +}; + +/** + * Create a new project time entry. + * @param props + * @returns + */ +export function useCreateProjectTimeEntry(props) { + const queryClient = useQueryClient(); + const apiRequest = useApiRequest(); + + return useMutation( + ([id, values]) => apiRequest.post(`/projects/tasks/${id}/times`, values), + { + onSuccess: () => { + // Common invalidate queries. + commonInvalidateQueries(queryClient); + }, + ...props, + }, + ); +} + +/** + * Edit the given project time entry. + * @param props + * @returns + */ +export function useEditProjectTimeEntry(props) { + const queryClient = useQueryClient(); + const apiRequest = useApiRequest(); + + return useMutation( + ([id, values]) => apiRequest.post(`projects/times/${id}`, values), + { + onSuccess: (res, [id, values]) => { + // Invalidate specific project time entry. + queryClient.invalidateQueries([t.PROJECT_TIME_ENTRY, id]); + + commonInvalidateQueries(queryClient); + }, + ...props, + }, + ); +} + +/** + * Delete the given project time entry + * @param props + */ +export function useDeleteProjectTimeEntry(props) { + const queryClient = useQueryClient(); + const apiRequest = useApiRequest(); + + return useMutation((id) => apiRequest.delete(`projects/times/${id}`), { + onSuccess: (res, id) => { + // Invalidate specific project task. + queryClient.invalidateQueries([t.PROJECT_TASK, id]); + + // Common invalidate queries. + commonInvalidateQueries(queryClient); + }, + ...props, + }); +} + +/** + * Retrive the given project time entry. + * @param timeId + * @param props + * @param requestProps + * @returns + */ +export function useProjectTimeEntry(timeId, props, requestProps) { + return useRequestQuery( + [t.PROJECT_TIME_ENTRY, timeId], + { method: 'get', url: `projects/times/${timeId}`, ...requestProps }, + { + select: (res) => res.data.time, + defaultData: {}, + ...props, + }, + ); +} + +const transformProjectTimeEntries = (res) => ({ + projectTasks: res.data.times, +}); + +/** + * + * @param taskId - Task id. + * @param props + * @param requestProps + * @returns + */ +export function useProjectTimeEntries(taskId, props, requestProps) { + return useRequestQuery( + [t.PROJECT_TIME_ENTRIES, taskId], + { method: 'get', url: `projects/tasks/${taskId}/times`, ...requestProps }, + { + select: transformProjectTimeEntries, + defaultData: { + projectTimeEntries: [], + }, + ...props, + }, + ); +} diff --git a/src/containers/Projects/hooks/projectsTask.tsx b/src/containers/Projects/hooks/projectsTask.tsx index 64ee669bc..1acfcbc5e 100644 --- a/src/containers/Projects/hooks/projectsTask.tsx +++ b/src/containers/Projects/hooks/projectsTask.tsx @@ -40,18 +40,15 @@ 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]); + 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, + commonInvalidateQueries(queryClient); }, - ); + ...props, + }); } /** @@ -83,7 +80,7 @@ export function useDeleteProjectTask(props) { */ export function useProjectTask(taskId, props, requestProps) { return useRequestQuery( - [t.PROJECT, taskId], + [t.PROJECT_TASK, taskId], { method: 'get', url: `tasks/${taskId}`, ...requestProps }, { select: (res) => res.data.task, diff --git a/src/containers/Projects/hooks/type.ts b/src/containers/Projects/hooks/type.ts index 40ccea949..72f12914c 100644 --- a/src/containers/Projects/hooks/type.ts +++ b/src/containers/Projects/hooks/type.ts @@ -8,9 +8,19 @@ const PROJECTS = { PROJECTS: 'PROJECTS', }; -const PROJECT_TASKS ={ - PROJECT_TASKS:'PROJECT_TASKS', - PROJECT_TASK:'PROJECT_TASK', -} +const PROJECT_TASKS = { + PROJECT_TASKS: 'PROJECT_TASKS', + PROJECT_TASK: 'PROJECT_TASK', +}; -export default { ...PROJECTS, ...CUSTOMERS,...PROJECT_TASKS }; +const PROJECT_TIME_ENTRIES = { + PROJECT_TIME_ENTRIES: 'PROJECT_TIME_ENTRIES', + PROJECT_TIME_ENTRY: 'PROJECT_TIME_ENTRY', +}; + +export default { + ...PROJECTS, + ...CUSTOMERS, + ...PROJECT_TASKS, + ...PROJECT_TIME_ENTRIES, +}; diff --git a/src/lang/en/index.json b/src/lang/en/index.json index 2ecb5a4dc..84e9c54aa 100644 --- a/src/lang/en/index.json +++ b/src/lang/en/index.json @@ -2124,6 +2124,10 @@ "project_time_entry.schema.label.task_name": "Task name", "project_time_entry.schema.label.duration": "Duration", "project_time_entry.schema.label.date": "Date", + "project_time_entry.success_message": "The time entry has been created successfully.", + "project_time_entry.edit_success_message": "The time entry has been edited successfully.", + "project_time_entry.alert.delete_message": "The deleted time entry has been deleted successfully.", + "project_time_entry.alert.once_delete_this_project": "Once you delete this time entry, you won't be able to restore it later. Are you sure you want to delete this time entry?", "find_or_choose_a_project": "Find or choose a project", "choose_a_task": "Choose a task", "project_expense.dialog.label": "New Expense",