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);
};
// 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}
>
<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(
() => 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 },
];

View File

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

View File

@@ -26,7 +26,7 @@ function ProjectTaskProvider({ ...props }) {
enabled: !!projectId,
});
console.log(project, 'XX');
// provider payload.
const provider = {
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 { ProjectTimesheetsHeader } from './ProjectTimesheetsHeader';
import { ProjectTimesheetsProvider } from './ProjectTimesheetsProvider';
/**
* Project Timesheets.
@@ -10,12 +11,12 @@ import { ProjectTimesheetsHeader } from './ProjectTimesheetsHeader';
*/
export default function ProjectTimeSheets() {
return (
<React.Fragment>
<ProjectTimesheetsProvider>
<ProjectTimesheetsHeader />
<ProjectTimesheetTableCard>
<ProjectTimesheetsTable />
</ProjectTimesheetTableCard>
</React.Fragment>
</ProjectTimesheetsProvider>
);
}

View File

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

View File

@@ -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'))

View File

@@ -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 (

View File

@@ -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 (
<div className={Classes.DIALOG_BODY}>
{/*------------ Project -----------*/}
@@ -45,7 +48,7 @@ function ProjectTimeEntryFormFields() {
>
<TaskSelect
name={'taskId'}
tasks={[]}
tasks={projectTasks}
popoverProps={{ minimal: true }}
/>
</FFormGroup>

View File

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

View File

@@ -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 (
<DialogContent>
<DialogContent name={'project-time-entry-form'}>
<ProjecctTimeEntryFormContext.Provider value={provider} {...props} />
</DialogContent>
);

View File

@@ -1,2 +1,3 @@
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 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,

View File

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

View File

@@ -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",