mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-21 15:20:34 +00:00
feat: add project billable entries dialog.
This commit is contained in:
@@ -46,7 +46,7 @@ import ProjectTimeEntryFormDialog from '@/containers/Projects/containers/Project
|
|||||||
import ProjectExpenseForm from '@/containers/Projects/containers/ProjectExpenseForm';
|
import ProjectExpenseForm from '@/containers/Projects/containers/ProjectExpenseForm';
|
||||||
import EstimatedExpenseFormDialog from '@/containers/Projects/containers/EstimatedExpenseFormDialog';
|
import EstimatedExpenseFormDialog from '@/containers/Projects/containers/EstimatedExpenseFormDialog';
|
||||||
import ProjectInvoicingFormDialog from '@/containers/Projects/containers/ProjectInvoicingFormDialog';
|
import ProjectInvoicingFormDialog from '@/containers/Projects/containers/ProjectInvoicingFormDialog';
|
||||||
|
import ProjectBillableEntriesFormDialog from '@/containers/Projects/containers/ProjectBillableEntriesFormDialog';
|
||||||
import { DialogsName } from '@/constants/dialogs';
|
import { DialogsName } from '@/constants/dialogs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -134,6 +134,7 @@ export default function DialogsContainer() {
|
|||||||
<ProjectInvoicingFormDialog
|
<ProjectInvoicingFormDialog
|
||||||
dialogName={DialogsName.ProjectInvoicingForm}
|
dialogName={DialogsName.ProjectInvoicingForm}
|
||||||
/>
|
/>
|
||||||
|
<ProjectBillableEntriesFormDialog dialogName={DialogsName.ProjectBillableEntriesForm}/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,4 +44,5 @@ export enum DialogsName {
|
|||||||
ProjectExpenseForm = 'project-expense-form',
|
ProjectExpenseForm = 'project-expense-form',
|
||||||
EstimateExpenseForm = 'estimate-expense-form',
|
EstimateExpenseForm = 'estimate-expense-form',
|
||||||
ProjectInvoicingForm = 'project-invoicing-form',
|
ProjectInvoicingForm = 'project-invoicing-form',
|
||||||
|
ProjectBillableEntriesForm = 'project-billable-entries',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import * as Yup from 'yup';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
|
||||||
|
const Schema = Yup.object().shape({});
|
||||||
|
|
||||||
|
export const ProjectBillableEntriesFormSchema = Schema;
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
import { Formik } from 'formik';
|
||||||
|
import { AppToaster } from '@/components';
|
||||||
|
import { ProjectBillableEntriesFormSchema } from './ProjectBillableEntriesForm.schema';
|
||||||
|
import ProjectBillableEntriesFormContent from './ProjectBillableEntriesFormContent';
|
||||||
|
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||||
|
|
||||||
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
|
const defaultInitialValues = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* project billable entries form.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function ProjectBillableEntriesForm({
|
||||||
|
//#withDialogActions
|
||||||
|
closeDialog,
|
||||||
|
}) {
|
||||||
|
const initialValues = {
|
||||||
|
...defaultInitialValues,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handles the form submit.
|
||||||
|
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
|
||||||
|
const form = {};
|
||||||
|
|
||||||
|
// Handle request response success.
|
||||||
|
const onSuccess = (response) => {
|
||||||
|
AppToaster.show({});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle request response errors.
|
||||||
|
const onError = ({
|
||||||
|
response: {
|
||||||
|
data: { errors },
|
||||||
|
},
|
||||||
|
}) => {
|
||||||
|
setSubmitting(false);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
validationSchema={ProjectBillableEntriesFormSchema}
|
||||||
|
initialValues={initialValues}
|
||||||
|
onSubmit={handleFormSubmit}
|
||||||
|
component={ProjectBillableEntriesFormContent}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(withDialogActions)(ProjectBillableEntriesForm);
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Form } from 'formik';
|
||||||
|
import ProjectBillableEntriesFormFields from './ProjectBillableEntriesFormFields';
|
||||||
|
import ProjectBillableEntriesFormFloatingActions from './ProjectBillableEntriesFormFloatingActions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project billable entries form content.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function ProjectBillableEntriesFormContent() {
|
||||||
|
return (
|
||||||
|
<Form>
|
||||||
|
<ProjectBillableEntriesFormFields />
|
||||||
|
<ProjectBillableEntriesFormFloatingActions />
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ProjectBillableEntriesFormProvider } from './ProjectBillableEntriesFormProvider';
|
||||||
|
import ProjectBillableEntriesForm from './ProjectBillableEntriesForm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project billable entries form dialog content.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function ProjectEntriesFormDialogContent({
|
||||||
|
// #ownProps
|
||||||
|
dialogName,
|
||||||
|
projectId,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ProjectBillableEntriesFormProvider
|
||||||
|
dialogName={dialogName}
|
||||||
|
projectId={projectId}
|
||||||
|
>
|
||||||
|
<ProjectBillableEntriesForm />
|
||||||
|
</ProjectBillableEntriesFormProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
import { Classes } from '@blueprintjs/core';
|
||||||
|
import {
|
||||||
|
FFormGroup,
|
||||||
|
FInputGroup,
|
||||||
|
FieldRequiredHint,
|
||||||
|
FormattedMessage as T,
|
||||||
|
} from '@/components';
|
||||||
|
import { ProjectRowDivider, ProjectEntiresBox } from './components';
|
||||||
|
import { useProjectBillableEntriesFormContext } from './ProjectBillableEntriesFormProvider';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project billable entries form fields.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function ProjectBillableEntriesFormFields() {
|
||||||
|
// Formik context.
|
||||||
|
const { values } = useFormikContext();
|
||||||
|
|
||||||
|
const { billableEntries } = useProjectBillableEntriesFormContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={Classes.DIALOG_BODY}>
|
||||||
|
{/*------------ Filter by Date -----------*/}
|
||||||
|
<FFormGroup
|
||||||
|
name={'date'}
|
||||||
|
label={<T id={'project_billable_entries.dialog.filter_by_date'} />}
|
||||||
|
labelInfo={<FieldRequiredHint />}
|
||||||
|
>
|
||||||
|
<FInputGroup name="date" placeholder={'Placeholder text'} />
|
||||||
|
</FFormGroup>
|
||||||
|
|
||||||
|
<ProjectRowDivider />
|
||||||
|
|
||||||
|
{/*------------ Filter by Type -----------*/}
|
||||||
|
<FFormGroup
|
||||||
|
name={'type'}
|
||||||
|
label={<T id={'project_billable_entries.dialog.filter_by_type'} />}
|
||||||
|
labelInfo={<FieldRequiredHint />}
|
||||||
|
>
|
||||||
|
<FInputGroup name="type" placeholder={'Placeholder Type'} />
|
||||||
|
</FFormGroup>
|
||||||
|
|
||||||
|
<ProjectEntiresBox billableEntries={billableEntries} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
import { Intent, Button, Classes } from '@blueprintjs/core';
|
||||||
|
import { FormattedMessage as T } from '@/components';
|
||||||
|
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||||
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* project entries from floating actions.
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
function ProjectEntriesFormFloatingActions({
|
||||||
|
// #withDialogActions
|
||||||
|
closeDialog,
|
||||||
|
}) {
|
||||||
|
// Formik context.
|
||||||
|
const { isSubmitting } = useFormikContext();
|
||||||
|
|
||||||
|
// Handle close button click.
|
||||||
|
const handleCancelBtnClick = () => {
|
||||||
|
closeDialog(dialogName);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={Classes.DIALOG_FOOTER}>
|
||||||
|
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||||
|
<SaveButton
|
||||||
|
intent={Intent.PRIMARY}
|
||||||
|
loading={isSubmitting}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</SaveButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(withDialogActions)(ProjectEntriesFormFloatingActions);
|
||||||
|
|
||||||
|
const SaveButton = styled(Button)`
|
||||||
|
&.bp3-button {
|
||||||
|
min-width: 80px;
|
||||||
|
border-radius: 16px;
|
||||||
|
margin-left: 0px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useProjectBillableEntries } from '../../hooks';
|
||||||
|
import { DialogContent } from '@/components';
|
||||||
|
|
||||||
|
const ProjectBillableEntriesFormContext = React.createContext();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project billable entries form provider.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function ProjectBillableEntriesFormProvider({
|
||||||
|
// #ownProps
|
||||||
|
dialogName,
|
||||||
|
projectId,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
// Handle fetch project billable entries.
|
||||||
|
const {
|
||||||
|
data: { billableEntries },
|
||||||
|
isLoading: isProjectBillableEntriesLoading,
|
||||||
|
} = useProjectBillableEntries(
|
||||||
|
projectId,
|
||||||
|
{
|
||||||
|
billable_type: 'expense',
|
||||||
|
to_date: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!projectId,
|
||||||
|
keepPreviousData: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
//state provider.
|
||||||
|
const provider = {
|
||||||
|
dialogName,
|
||||||
|
projectId,
|
||||||
|
billableEntries,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogContent isLoading={isProjectBillableEntriesLoading}>
|
||||||
|
<ProjectBillableEntriesFormContext.Provider value={provider} {...props} />
|
||||||
|
</DialogContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const useProjectBillableEntriesFormContext = () =>
|
||||||
|
React.useContext(ProjectBillableEntriesFormContext);
|
||||||
|
|
||||||
|
export {
|
||||||
|
ProjectBillableEntriesFormProvider,
|
||||||
|
useProjectBillableEntriesFormContext,
|
||||||
|
};
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Button } from '@blueprintjs/core';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Projec billable entries item box.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function ProjectBillableEntriesItemBox({ projectBillableEntry }) {
|
||||||
|
return (
|
||||||
|
<ProjectEntryBox>
|
||||||
|
<ProjectEntryHeader>
|
||||||
|
<ProjectEntryTitle>{projectBillableEntry.title}</ProjectEntryTitle>
|
||||||
|
<ProjectEntrtyItemContent>
|
||||||
|
<ProjectEntryItem>{projectBillableEntry.date}</ProjectEntryItem>
|
||||||
|
<ProjectEntryItem>{projectBillableEntry.time}</ProjectEntryItem>
|
||||||
|
</ProjectEntrtyItemContent>
|
||||||
|
</ProjectEntryHeader>
|
||||||
|
<ProjectEntryContent>
|
||||||
|
<ProjectEntryAmount>{projectBillableEntry.billable_amount}</ProjectEntryAmount>
|
||||||
|
</ProjectEntryContent>
|
||||||
|
<ProjectEntryFoorer>
|
||||||
|
<ProjectEntryButton small={true}>Add</ProjectEntryButton>
|
||||||
|
<ProjectEntryButton small={true}>Show</ProjectEntryButton>
|
||||||
|
</ProjectEntryFoorer>
|
||||||
|
</ProjectEntryBox>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project billable entries box.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function ProjectEntiresBox({ billableEntries }) {
|
||||||
|
return billableEntries.map((entries) => (
|
||||||
|
<ProjectBillableEntriesItemBox projectBillableEntry={entries} />
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProjectEntryBox = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: 5px;
|
||||||
|
width: 360px;
|
||||||
|
height: 121px;
|
||||||
|
border: 1px solid #d4d9df;
|
||||||
|
padding: 15px 12px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
const ProjectEntryHeader = styled.div``;
|
||||||
|
const ProjectEntryTitle = styled.div`
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #444444;
|
||||||
|
`;
|
||||||
|
const ProjectEntrtyItemContent = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
`;
|
||||||
|
const ProjectEntryItem = styled.div`
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #666666;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ProjectEntryContent = styled.div`
|
||||||
|
flex: 1 0 auto;
|
||||||
|
line-height: 2rem;
|
||||||
|
border-bottom: 1px solid #e3e3e3;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ProjectEntryAmount = styled.div`
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #111111;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ProjectRowDivider = styled.div`
|
||||||
|
height: 1px;
|
||||||
|
background: #e3e3e3;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
margin-top: 15px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ProjectEntryFoorer = styled.div`
|
||||||
|
padding: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ProjectEntryButton = styled(Button)`
|
||||||
|
&.bp3-button.bp3-small,
|
||||||
|
&.bp3-button:not([class*='bp3-intent-']):not(.bp3-minimal).bp3-small {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #2172ed;
|
||||||
|
background: transparent;
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { Dialog, DialogSuspense, FormattedMessage as T } from '@/components';
|
||||||
|
import withDialogRedux from '@/components/DialogReduxConnect';
|
||||||
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
|
const ProjectBillableEntriesFormDialogContent = React.lazy(
|
||||||
|
() => import('./ProjectBillableEntriesFormDialogContent'),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project billable entries form dialog.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function ProjectBillableEntriesFormDialog({
|
||||||
|
dialogName,
|
||||||
|
payload: { projectId },
|
||||||
|
isOpen,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ProjectBillableEntriesFormDialogRoot
|
||||||
|
name={dialogName}
|
||||||
|
title={<T id={'project_billable_entries.dialog.label'} />}
|
||||||
|
isOpen={isOpen}
|
||||||
|
autoFocus={true}
|
||||||
|
canEscapeKeyClose={true}
|
||||||
|
style={{ width: '400px' }}
|
||||||
|
>
|
||||||
|
<DialogSuspense>
|
||||||
|
<ProjectBillableEntriesFormDialogContent
|
||||||
|
dialogName={dialogName}
|
||||||
|
projectId={projectId}
|
||||||
|
/>
|
||||||
|
</DialogSuspense>
|
||||||
|
</ProjectBillableEntriesFormDialogRoot>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(withDialogRedux())(ProjectBillableEntriesFormDialog);
|
||||||
|
|
||||||
|
const ProjectBillableEntriesFormDialogRoot = styled(Dialog)`
|
||||||
|
.bp3-dialog-body {
|
||||||
|
.bp3-form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
label.bp3-label {
|
||||||
|
margin-bottom: 3px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.bp3-dialog-footer {
|
||||||
|
.bp3-dialog-footer-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './projects'
|
export * from './projects'
|
||||||
export * from './projectsTask'
|
export * from './projectsTask'
|
||||||
export * from './projectTimeEntry'
|
export * from './projectTimeEntry'
|
||||||
|
export * from './projectBillableEntries'
|
||||||
27
src/containers/Projects/hooks/projectBillableEntries.tsx
Normal file
27
src/containers/Projects/hooks/projectBillableEntries.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { useRequestQuery } from '@/hooks/useQueryRequest';
|
||||||
|
import t from './type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param projectId - Project id.
|
||||||
|
* @param query
|
||||||
|
* @param props
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function useProjectBillableEntries(projectId, query, props) {
|
||||||
|
return useRequestQuery(
|
||||||
|
[t.PROJECT_BILLABLE_ENTRIES, projectId],
|
||||||
|
{
|
||||||
|
method: 'get',
|
||||||
|
url: `projects/${projectId}/billable/entries`,
|
||||||
|
params: query,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
select: (res) => res.data.billable_entries,
|
||||||
|
defaultData: {
|
||||||
|
billableEntries: [],
|
||||||
|
},
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -18,9 +18,14 @@ const PROJECT_TIME_ENTRIES = {
|
|||||||
PROJECT_TIME_ENTRY: 'PROJECT_TIME_ENTRY',
|
PROJECT_TIME_ENTRY: 'PROJECT_TIME_ENTRY',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PROJECT_BILLABLE_ENTRIES = {
|
||||||
|
PROJECT_BILLABLE_ENTRIES: 'PROJECT_BILLABLE_ENTRIES',
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
...PROJECTS,
|
...PROJECTS,
|
||||||
...CUSTOMERS,
|
...CUSTOMERS,
|
||||||
...PROJECT_TASKS,
|
...PROJECT_TASKS,
|
||||||
...PROJECT_TIME_ENTRIES,
|
...PROJECT_TIME_ENTRIES,
|
||||||
|
...PROJECT_BILLABLE_ENTRIES,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user