feat: add api project timesheet.

This commit is contained in:
elforjani13
2022-07-30 15:10:23 +02:00
parent 72c893c255
commit 1b13b98899
18 changed files with 334 additions and 39 deletions

View File

@@ -32,8 +32,8 @@ function ProjectTaskDeleteAlert({
closeAlert(name); closeAlert(name);
}; };
// handleConfirm delete project // handleConfirm delete project task
const handleConfirmProjectDelete = () => { const handleConfirmProjectTaskDelete = () => {
deleteProjectTaskMutate(taskId) deleteProjectTaskMutate(taskId)
.then(() => { .then(() => {
AppToaster.show({ AppToaster.show({
@@ -61,7 +61,7 @@ function ProjectTaskDeleteAlert({
intent={Intent.DANGER} intent={Intent.DANGER}
isOpen={isOpen} isOpen={isOpen}
onCancel={handleCancelDeleteAlert} onCancel={handleCancelDeleteAlert}
onConfirm={handleConfirmProjectDelete} onConfirm={handleConfirmProjectTaskDelete}
loading={isLoading} loading={isLoading}
> >
<p> <p>

View File

@@ -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 (
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'delete'} />}
icon="trash"
intent={Intent.DANGER}
isOpen={isOpen}
onCancel={handleCancelDeleteAlert}
onConfirm={handleConfirmProjectTimesheetDelete}
loading={isLoading}
>
<p>
<FormattedHTMLMessage
id={'project_time_entry.alert.once_delete_this_project'}
/>
</p>
</Alert>
);
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
)(ProjectTimesheetDeleteAlert);

View File

@@ -4,6 +4,9 @@ const ProjectDeleteAlert = React.lazy(() => import('./ProjectDeleteAlert'));
const ProjectTaskDeleteAlert = React.lazy( const ProjectTaskDeleteAlert = React.lazy(
() => import('./ProjectTaskDeleteAlert'), () => import('./ProjectTaskDeleteAlert'),
); );
const ProjectTimesheetDeleteAlert = React.lazy(
() => import('./ProjectTimesheetDeleteAlert'),
);
/** /**
* Project alerts. * Project alerts.
@@ -11,4 +14,5 @@ const ProjectTaskDeleteAlert = React.lazy(
export default [ export default [
{ name: 'project-delete', component: ProjectDeleteAlert }, { name: 'project-delete', component: ProjectDeleteAlert },
{ name: 'project-task-delete', component: ProjectTaskDeleteAlert }, { name: 'project-task-delete', component: ProjectTaskDeleteAlert },
{ name: 'project-timesheet-delete', component: ProjectTimesheetDeleteAlert },
]; ];

View File

@@ -35,7 +35,6 @@ function ProjectDetailActionsBar({
// #withSettingsActions // #withSettingsActions
addSetting, addSetting,
}) { }) {
const history = useHistory();
const { projectId } = useProjectDetailContext(); const { projectId } = useProjectDetailContext();
// Handle new transaction button click. // Handle new transaction button click.

View File

@@ -26,7 +26,7 @@ function ProjectTaskProvider({ ...props }) {
enabled: !!projectId, enabled: !!projectId,
}); });
console.log(project, 'XX');
// provider payload. // provider payload.
const provider = { const provider = {
project, project,

View File

@@ -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 <ProjectTimesheetContext.Provider value={provider} {...props} />;
}
const useProjectTimesheetContext = () =>
React.useContext(ProjectTimesheetContext);
export { ProjectTimesheetsProvider, useProjectTimesheetContext };

View File

@@ -3,6 +3,7 @@ import styled from 'styled-components';
import { ProjectTimesheetsTable } from './ProjectTimesheetsTable'; import { ProjectTimesheetsTable } from './ProjectTimesheetsTable';
import { ProjectTimesheetsHeader } from './ProjectTimesheetsHeader'; import { ProjectTimesheetsHeader } from './ProjectTimesheetsHeader';
import { ProjectTimesheetsProvider } from './ProjectTimesheetsProvider';
/** /**
* Project Timesheets. * Project Timesheets.
@@ -10,12 +11,12 @@ import { ProjectTimesheetsHeader } from './ProjectTimesheetsHeader';
*/ */
export default function ProjectTimeSheets() { export default function ProjectTimeSheets() {
return ( return (
<React.Fragment> <ProjectTimesheetsProvider>
<ProjectTimesheetsHeader /> <ProjectTimesheetsHeader />
<ProjectTimesheetTableCard> <ProjectTimesheetTableCard>
<ProjectTimesheetsTable /> <ProjectTimesheetsTable />
</ProjectTimesheetTableCard> </ProjectTimesheetTableCard>
</React.Fragment> </ProjectTimesheetsProvider>
); );
} }

View File

@@ -31,6 +31,9 @@ function ProjectTaskFormProvider({
}, },
); );
console.log(taskId, 'XX');
console.log(projectTask, 'XX');
const isNewMode = !taskId; const isNewMode = !taskId;
// State provider. // State provider.
const provider = { const provider = {

View File

@@ -6,12 +6,12 @@ const Schema = Yup.object().shape({
date: Yup.date() date: Yup.date()
.label(intl.get('project_time_entry.schema.label.date')) .label(intl.get('project_time_entry.schema.label.date'))
.required(), .required(),
projectId: Yup.string() // projectId: Yup.string()
.label(intl.get('project_time_entry.schema.label.project_name')) // .label(intl.get('project_time_entry.schema.label.project_name'))
.required(), // .required(),
taskId: Yup.string() // taskId: Yup.string()
.label(intl.get('project_time_entry.schema.label.task_name')) // .label(intl.get('project_time_entry.schema.label.task_name'))
.required(), // .required(),
description: Yup.string().nullable().max(DATATYPES_LENGTH.TEXT), description: Yup.string().nullable().max(DATATYPES_LENGTH.TEXT),
duration: Yup.string() duration: Yup.string()
.label(intl.get('project_time_entry.schema.label.duration')) .label(intl.get('project_time_entry.schema.label.duration'))

View File

@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import moment from 'moment'; import moment from 'moment';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import { Intent } from '@blueprintjs/core';
import { Formik } from 'formik'; import { Formik } from 'formik';
import { AppToaster } from '@/components'; import { AppToaster } from '@/components';
@@ -13,7 +14,7 @@ import { compose } from '@/utils';
const defaultInitialValues = { const defaultInitialValues = {
date: moment(new Date()).format('YYYY-MM-DD'), date: moment(new Date()).format('YYYY-MM-DD'),
projectId: '', // projectId: '',
taskId: '', taskId: '',
description: '', description: '',
duration: '', duration: '',
@@ -28,7 +29,11 @@ function ProjectTimeEntryForm({
closeDialog, closeDialog,
}) { }) {
// time entry form dialog context. // time entry form dialog context.
const { dialogName } = useProjectTimeEntryFormContext(); const {
dialogName,
createProjectTimeEntryMutate,
editProjectTimeEntryMutate,
} = useProjectTimeEntryFormContext();
// Initial form values // Initial form values
const initialValues = { const initialValues = {
@@ -37,11 +42,21 @@ function ProjectTimeEntryForm({
// Handles the form submit. // Handles the form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => { const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
const form = {}; const form = {
...values,
};
// Handle request response success. // Handle request response success.
const onSuccess = (response) => { 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); closeDialog(dialogName);
}; };
@@ -53,6 +68,9 @@ function ProjectTimeEntryForm({
}) => { }) => {
setSubmitting(false); setSubmitting(false);
}; };
createProjectTimeEntryMutate([values.taskId, form])
.then(onSuccess)
.catch(onError);
}; };
return ( return (

View File

@@ -9,10 +9,10 @@ import {
FInputGroup, FInputGroup,
FDateInput, FDateInput,
FTextArea, FTextArea,
FEditableText,
FieldRequiredHint, FieldRequiredHint,
FormattedMessage as T, FormattedMessage as T,
} from '@/components'; } from '@/components';
import { useProjectTimeEntryFormContext } from './ProjectTimeEntryFormProvider';
import { TaskSelect, ProjectsSelect } from '../../components'; import { TaskSelect, ProjectsSelect } from '../../components';
import { momentFormatter } from '@/utils'; import { momentFormatter } from '@/utils';
@@ -21,6 +21,9 @@ import { momentFormatter } from '@/utils';
* @returns * @returns
*/ */
function ProjectTimeEntryFormFields() { function ProjectTimeEntryFormFields() {
// time entry form dialog context.
const { projectTasks } = useProjectTimeEntryFormContext();
return ( return (
<div className={Classes.DIALOG_BODY}> <div className={Classes.DIALOG_BODY}>
{/*------------ Project -----------*/} {/*------------ Project -----------*/}
@@ -45,7 +48,7 @@ function ProjectTimeEntryFormFields() {
> >
<TaskSelect <TaskSelect
name={'taskId'} name={'taskId'}
tasks={[]} tasks={projectTasks}
popoverProps={{ minimal: true }} popoverProps={{ minimal: true }}
/> />
</FFormGroup> </FFormGroup>

View File

@@ -18,8 +18,10 @@ function ProjectTimeEntryFormFloatingActions({
const { dialogName } = useProjectTimeEntryFormContext(); const { dialogName } = useProjectTimeEntryFormContext();
// Formik context. // Formik context.
const { isSubmitting } = useFormikContext(); const { isSubmitting, values, errors } = useFormikContext();
console.log(values, 'XX');
console.log(errors, 'XX');
// Handle close button click. // Handle close button click.
const handleCancelBtnClick = () => { const handleCancelBtnClick = () => {
closeDialog(dialogName); closeDialog(dialogName);

View File

@@ -1,4 +1,9 @@
import React from 'react'; import React from 'react';
import {
useProjectTasks,
useCreateProjectTimeEntry,
useEditProjectTimeEntry,
} from '../../hooks';
import { DialogContent } from '@/components'; import { DialogContent } from '@/components';
const ProjecctTimeEntryFormContext = React.createContext(); const ProjecctTimeEntryFormContext = React.createContext();
@@ -13,12 +18,30 @@ function ProjectTimeEntryFormProvider({
projectId, projectId,
...props ...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 = { const provider = {
dialogName, dialogName,
projectId,
projectTasks,
createProjectTimeEntryMutate,
editProjectTimeEntryMutate,
}; };
return ( return (
<DialogContent> <DialogContent name={'project-time-entry-form'}>
<ProjecctTimeEntryFormContext.Provider value={provider} {...props} /> <ProjecctTimeEntryFormContext.Provider value={provider} {...props} />
</DialogContent> </DialogContent>
); );

View File

@@ -1,2 +1,3 @@
export * from './projects' export * from './projects'
export * from './projectsTask' export * from './projectsTask'
export * from './projectTimeEntry'

View File

@@ -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,
},
);
}

View File

@@ -40,18 +40,15 @@ export function useEditProjectTask(props) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const apiRequest = useApiRequest(); const apiRequest = useApiRequest();
return useMutation( return useMutation(([id, values]) => apiRequest.post(`tasks/${id}`, values), {
([id, values]) => apiRequest.post(`tasks/${id}`, values), onSuccess: (res, [id, values]) => {
{ // Invalidate specific project task.
onSuccess: (res, [id, values]) => { queryClient.invalidateQueries([t.PROJECT_TASK, id]);
// Invalidate specific project task.
queryClient.invalidateQueries([t.PROJECT_TASK, id]);
commonInvalidateQueries(queryClient); commonInvalidateQueries(queryClient);
},
...props,
}, },
); ...props,
});
} }
/** /**
@@ -83,7 +80,7 @@ export function useDeleteProjectTask(props) {
*/ */
export function useProjectTask(taskId, props, requestProps) { export function useProjectTask(taskId, props, requestProps) {
return useRequestQuery( return useRequestQuery(
[t.PROJECT, taskId], [t.PROJECT_TASK, taskId],
{ method: 'get', url: `tasks/${taskId}`, ...requestProps }, { method: 'get', url: `tasks/${taskId}`, ...requestProps },
{ {
select: (res) => res.data.task, select: (res) => res.data.task,

View File

@@ -8,9 +8,19 @@ const PROJECTS = {
PROJECTS: 'PROJECTS', PROJECTS: 'PROJECTS',
}; };
const PROJECT_TASKS ={ const PROJECT_TASKS = {
PROJECT_TASKS:'PROJECT_TASKS', PROJECT_TASKS: 'PROJECT_TASKS',
PROJECT_TASK:'PROJECT_TASK', 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,
};

View File

@@ -2124,6 +2124,10 @@
"project_time_entry.schema.label.task_name": "Task name", "project_time_entry.schema.label.task_name": "Task name",
"project_time_entry.schema.label.duration": "Duration", "project_time_entry.schema.label.duration": "Duration",
"project_time_entry.schema.label.date": "Date", "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", "find_or_choose_a_project": "Find or choose a project",
"choose_a_task": "Choose a task", "choose_a_task": "Choose a task",
"project_expense.dialog.label": "New Expense", "project_expense.dialog.label": "New Expense",