mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 04:40:32 +00:00
feat: add project api
This commit is contained in:
@@ -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,
|
||||
];
|
||||
|
||||
@@ -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);
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
|
||||
const ProjectDeleteAlert = React.lazy(() => import('./ProjectDeleteAlert'));
|
||||
|
||||
/**
|
||||
* Project alerts.
|
||||
*/
|
||||
export default [{ name: 'project-delete', component: ProjectDeleteAlert }];
|
||||
@@ -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'),
|
||||
),
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
125
src/containers/Projects/hooks/index.ts
Normal file
125
src/containers/Projects/hooks/index.ts
Normal 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);
|
||||
},
|
||||
};
|
||||
}
|
||||
11
src/containers/Projects/hooks/type.ts
Normal file
11
src/containers/Projects/hooks/type.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
const CUSTOMERS = {
|
||||
CUSTOMERS: 'CUSTOMERS',
|
||||
CUSTOMER: 'CUSTOMER',
|
||||
};
|
||||
|
||||
const PROJECTS = {
|
||||
PROJECT: 'PROJECT',
|
||||
PROJECTS: 'PROJECTS',
|
||||
};
|
||||
|
||||
export default { ...PROJECTS, ...CUSTOMERS };
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user