feat: add project api

This commit is contained in:
elforjani13
2022-07-14 18:29:22 +02:00
parent 8826d2bc5b
commit 709e06a646
14 changed files with 341 additions and 73 deletions

View File

@@ -23,6 +23,7 @@ import TransactionsLockingAlerts from '../TransactionsLocking/TransactionsLockin
import WarehousesAlerts from '../Preferences/Warehouses/WarehousesAlerts';
import WarehousesTransfersAlerts from '../WarehouseTransfers/WarehousesTransfersAlerts';
import BranchesAlerts from '../Preferences/Branches/BranchesAlerts';
import ProjectAlerts from '../../containers/Projects/containers/ProjectAlerts';
export default [
...AccountsAlerts,
@@ -50,4 +51,5 @@ export default [
...WarehousesAlerts,
...WarehousesTransfersAlerts,
...BranchesAlerts,
...ProjectAlerts,
];

View File

@@ -0,0 +1,79 @@
//@ts-nocheck
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 { useDeleteProject } from '../../hooks';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
import { compose } from 'utils';
/**
* Project delete alert.
*/
function ProjectDeleteAlert({
name,
// #withAlertStoreConnect
isOpen,
payload: { projectId },
// #withAlertActions
closeAlert,
// #withDrawerActions
closeDrawer,
}) {
const { mutateAsync: deleteProjectMutate, isLoading } = useDeleteProject();
// handle cancel delete project alert.
const handleCancelDeleteAlert = () => {
closeAlert(name);
};
// handleConfirm delete project
const handleConfirmProjectDelete = () => {
deleteProjectMutate(projectId)
.then(() => {
AppToaster.show({
message: intl.get('projects.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={handleConfirmProjectDelete}
loading={isLoading}
>
<p>
<FormattedHTMLMessage id={'projects.alert.once_delete_this_project'} />
</p>
</Alert>
);
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
)(ProjectDeleteAlert);

View File

@@ -0,0 +1,8 @@
import React from 'react';
const ProjectDeleteAlert = React.lazy(() => import('./ProjectDeleteAlert'));
/**
* Project alerts.
*/
export default [{ name: 'project-delete', component: ProjectDeleteAlert }];

View File

@@ -1,19 +1,18 @@
import * as Yup from 'yup';
import intl from 'react-intl-universal';
import { DATATYPES_LENGTH } from 'common/dataTypes';
const Schema = Yup.object().shape({
contact: Yup.string().label(intl.get('project.schema.label.contact')),
projectName: Yup.string()
contact_id: Yup.string().label(intl.get('project.schema.label.contact')),
name: Yup.string()
.label(intl.get('project.schema.label.project_name'))
.required(),
projectDeadline: Yup.date()
deadline: Yup.date()
.label(intl.get('project.schema.label.deadline'))
.required(),
published: Yup.boolean().label(
published: Yup.boolean().label(
intl.get('project.schema.label.project_state'),
),
projectCost: Yup.number().label(
cost_estimate: Yup.number().label(
intl.get('project.schema.label.project_cost'),
),
});

View File

@@ -9,14 +9,14 @@ import { CreateProjectFormSchema } from './ProjectForm.schema';
import { useProjectFormContext } from './ProjectFormProvider';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
import { compose, transformToForm } from 'utils';
const defaultInitialValues = {
contact: '',
projectName: '',
projectDeadline: moment(new Date()).format('YYYY-MM-DD'),
contact_id: '',
name: '',
deadline: moment(new Date()).format('YYYY-MM-DD'),
published: false,
projectCost: '',
cost_estimate: '',
};
/**
@@ -28,20 +28,37 @@ function ProjectForm({
closeDialog,
}) {
// project form dialog context.
const { dialogName } = useProjectFormContext();
const {
dialogName,
project,
isNewMode,
projectId,
createProjectMutate,
editProjectMutate,
} = useProjectFormContext();
// Initial form values
const initialValues = {
...defaultInitialValues,
...transformToForm(project, defaultInitialValues),
};
// Handles the form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
const form = {};
setSubmitting(true);
const form = { ...values };
// Handle request response success.
const onSuccess = (response) => {
AppToaster.show({});
AppToaster.show({
message: intl.get(
isNewMode
? 'projects.dialog.success_message'
: 'projects.dialog.edit_success_message',
),
intent: Intent.SUCCESS,
});
closeDialog(dialogName);
};
@@ -53,6 +70,12 @@ function ProjectForm({
}) => {
setSubmitting(false);
};
if (isNewMode) {
createProjectMutate(form).then(onSuccess).catch(onError);
} else {
editProjectMutate([projectId, form]).then(onSuccess).catch(onError);
}
};
return (

View File

@@ -40,7 +40,7 @@ function ProjectFormFields() {
return (
<div className={Classes.DIALOG_BODY}>
{/*------------ Contact -----------*/}
<FastField name={'contact'}>
<FastField name={'contact_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={intl.get('projects.dialog.contact')}
@@ -53,7 +53,7 @@ function ProjectFormFields() {
selectedContactId={value}
defaultSelectText={'Select Contact Account'}
onContactSelected={(customer) => {
form.setFieldValue('contact', customer.id);
form.setFieldValue('contact_id', customer.id);
}}
allowCreate={true}
popoverFill={true}
@@ -64,19 +64,19 @@ function ProjectFormFields() {
{/*------------ Project Name -----------*/}
<FFormGroup
label={intl.get('projects.dialog.project_name')}
name={'projectName'}
name={'name'}
>
<FInputGroup name="projectName" />
<FInputGroup name="name" />
</FFormGroup>
{/*------------ DeadLine -----------*/}
<FFormGroup
label={intl.get('projects.dialog.deadline')}
name={'projectDeadline'}
name={'deadline'}
className={classNames(CLASSES.FILL, 'form-group--date')}
>
<FDateInput
{...momentFormatter('YYYY/MM/DD')}
name="projectDeadline"
name="deadline"
formatDate={(date) => date.toLocaleString()}
popoverProps={{
position: Position.BOTTOM,
@@ -94,14 +94,14 @@ function ProjectFormFields() {
</FFormGroup>
{/*------------ Cost Estimate -----------*/}
<FFormGroup
name={'projectCost'}
name={'cost_estimate'}
label={intl.get('projects.dialog.cost_estimate')}
>
<ControlGroup>
<InputPrependText text={'USD'} />
<FMoneyInputGroup
disabled={values.published}
name={'project_cost'}
name={'cost_estimate'}
allowDecimals={true}
allowNegativeValue={true}
/>

View File

@@ -15,12 +15,12 @@ function ProjectFormFloatingActions({
// #withDialogActions
closeDialog,
}) {
// project form dialog context.
const { dialogName } = useProjectFormContext();
// Formik context.
const { isSubmitting } = useFormikContext();
// project form dialog context.
const { dialogName } = useProjectFormContext();
// Handle close button click.
const handleCancelBtnClick = () => {
closeDialog(dialogName);
@@ -29,7 +29,7 @@ function ProjectFormFloatingActions({
return (
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button onClick={handleCancelBtnClick} style={{ minWidth: '75px' }}>
<Button onClick={handleCancelBtnClick} style={{ minWidth: '85px' }}>
<T id={'cancel'} />
</Button>
<Button

View File

@@ -1,6 +1,7 @@
// @ts-nocheck
import React from 'react';
import { useCustomers } from 'hooks/query';
import { useCreateProject, useEditProject, useProject } from '../../hooks';
import { DialogContent } from 'components';
const ProjectFormContext = React.createContext();
@@ -15,20 +16,36 @@ function ProjectFormProvider({
projectId,
...props
}) {
// Create and edit project mutations.
const { mutateAsync: createProjectMutate } = useCreateProject();
const { mutateAsync: editProjectMutate } = useEditProject();
// Handle fetch project detail.
const { data: project, isLoading: isProjectLoading } = useProject(projectId, {
enabled: !!projectId,
});
// Handle fetch customers data table or list
const {
data: { customers },
isLoading: isCustomersLoading,
} = useCustomers({ page_size: 10000 });
const isNewMode = !projectId;
// State provider.
const provider = {
customers,
dialogName,
project,
projectId,
isNewMode,
createProjectMutate,
editProjectMutate,
};
return (
<DialogContent isLoading={isCustomersLoading}>
<DialogContent isLoading={isCustomersLoading || isProjectLoading}>
<ProjectFormContext.Provider value={provider} {...props} />
</DialogContent>
);

View File

@@ -1,3 +1,4 @@
//@ts-nocheck
import React from 'react';
import styled from 'styled-components';
import { useHistory } from 'react-router-dom';
@@ -9,47 +10,12 @@ import { useProjectsListContext } from './ProjectsListProvider';
import { useMemorizedColumnsWidths } from 'hooks';
import { useProjectsListColumns, ActionsMenu } from './components';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withAlertsActions from 'containers/Alert/withAlertActions';
import withProjectsActions from './withProjectsActions';
import withSettings from '../../../Settings/withSettings';
import { compose } from 'utils';
const projects = [
{
id: 1,
name: 'Maroon Bronze',
deadline: '2022-06-08T22:00:00.000Z',
display_name: 'Kyrie Rearden',
cost_estimate: '40000',
task_amount: '0',
is_process: true,
is_closed: false,
is_draft: false,
},
{
id: 2,
name: 'Project Sherwood',
deadline: '2022-06-08T22:00:00.000Z',
display_name: 'Ella-Grace Miller',
cost_estimate: '700',
task_amount: '300',
is_process: false,
is_closed: false,
is_draft: true,
},
{
id: 3,
name: 'Tax Compliance',
deadline: '2022-06-23T22:00:00.000Z',
display_name: 'Abby & Wells',
cost_estimate: '3000',
task_amount: '0',
is_process: true,
is_closed: false,
is_draft: false,
},
];
/**
* Projects list datatable.
* @returns
@@ -58,11 +24,23 @@ function ProjectsDataTable({
// #withDial
openDialog,
// #withAlertsActions
openAlert,
// #withSettings
projectsTableSize,
}) {
const history = useHistory();
// Projects list context.
const { projects, isEmptyStatus, isProjectsLoading, isProjectsFetching } =
useProjectsListContext();
// Handle delete project.
const handleDeleteProject = ({ id }) => {
openAlert('project-delete', { projectId: id });
};
// Retrieve projects table columns.
const columns = useProjectsListColumns();
@@ -102,9 +80,9 @@ function ProjectsDataTable({
<ProjectsTable
columns={columns}
data={projects}
// loading={}
// headerLoading={}
// progressBarLoading={}
loading={isProjectsLoading}
headerLoading={isProjectsLoading}
progressBarLoading={isProjectsFetching}
manualSortBy={true}
noInitialFetch={true}
sticky={true}
@@ -119,6 +97,7 @@ function ProjectsDataTable({
payload={{
onViewDetails: handleViewDetailProject,
onEdit: handleEditProject,
onDelete:handleDeleteProject,
onNewTask: handleNewTaskButtonClick,
}}
/>
@@ -127,6 +106,7 @@ function ProjectsDataTable({
export default compose(
withDialogActions,
withAlertsActions,
withProjectsActions,
withSettings(({ projectSettings }) => ({
projectsTableSize: projectSettings?.tableSize,

View File

@@ -1,7 +1,9 @@
//@ts-nocheck
import React from 'react';
import { isEmpty } from 'lodash';
import { useResourceViews, useResourceMeta } from 'hooks/query';
import DashboardInsider from '../../../../components/Dashboard/DashboardInsider';
import { useProjects } from '../../hooks';
const ProjectsListContext = React.createContext();
@@ -14,16 +16,32 @@ function ProjectsListProvider({ query, tableStateChanged, ...props }) {
const { data: projectsViews, isLoading: isViewsLoading } =
useResourceViews('projects');
// Fetch accounts list according to the given custom view id.
const {
data: { projects },
isFetching: isProjectsFetching,
isLoading: isProjectsLoading,
} = useProjects(query, { keepPreviousData: true });
// Detarmines the datatable empty status.
const isEmptyStatus =
isEmpty(projects) && !tableStateChanged && !isProjectsLoading;
// provider payload.
const provider = {
projects,
projectsViews,
isProjectsLoading,
isProjectsFetching,
isViewsLoading,
isEmptyStatus,
};
return (
<DashboardInsider
// loading={isViewsLoading}
name={'projects'}
>
<DashboardInsider loading={isViewsLoading} name={'projects'}>
<ProjectsListContext.Provider value={provider} {...props} />
</DashboardInsider>
);

View File

@@ -102,13 +102,15 @@ export const ActionsMenu = ({
export const ProjectsAccessor = (row) => (
<ProjectItemsWrap>
<ProjectItemsHeader>
<ProjectItemContactName>{row.display_name}</ProjectItemContactName>
<ProjectItemContactName>
{row.contact_display_name}
</ProjectItemContactName>
<ProjectItemProjectName>{row.name}</ProjectItemProjectName>
</ProjectItemsHeader>
<ProjectItemDescription>
<FormatDate value={row.deadline} />
<FormatDate value={row.deadline_formatted} />
{intl.get('projects.label.cost_estimate', {
value: row.cost_estimate,
value: row.cost_estimate_formatted,
})}
</ProjectItemDescription>
</ProjectItemsWrap>

View File

@@ -0,0 +1,125 @@
//@ts-nocheck
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.projects,
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);
},
};
}

View File

@@ -0,0 +1,11 @@
const CUSTOMERS = {
CUSTOMERS: 'CUSTOMERS',
CUSTOMER: 'CUSTOMER',
};
const PROJECTS = {
PROJECT: 'PROJECT',
PROJECTS: 'PROJECTS',
};
export default { ...PROJECTS, ...CUSTOMERS };

View File

@@ -2057,6 +2057,10 @@
"projects.dialog.cost_estimate": "Cost Estimate",
"projects.label.create": "Create",
"projects.label.cost_estimate": " • Estimate {value}",
"projects.dialog.success_message": "The project has been created successfully.",
"projects.dialog.edit_success_message": "The project has been edited successfully.",
"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?",
"project_task.dialog.new_task": "New Task",
"project_task.dialog.task_name": "Task Name",
"project_task.dialog.estimated_hours": "Estimate Hours",