Skip to content

Commit 60268ac

Browse files
committed
verified invoices vs projectMonth sync with separate backend endpoint
1 parent f1e80cf commit 60268ac

File tree

13 files changed

+119
-87
lines changed

13 files changed

+119
-87
lines changed

backend/src/controllers/invoices.ts

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -188,32 +188,67 @@ export const updateInvoiceController = async (req: ConfacRequest, res: Response)
188188
}
189189
await saveAudit(req, 'invoice', originalInvoice, invoice, ['projectMonth.consultantId']);
190190

191-
let projectMonth;
192-
if (invoice?.projectMonth?.projectMonthId) {
193-
// TODO: This should be a separate route once security is implemented
194-
// Right now it is always updating the projectMonth.verified but this only changes when the invoice.verified changes
195-
// This is now 'fixed' on the frontend.
196-
projectMonth = await req.db.collection(CollectionNames.PROJECTS_MONTH)
197-
.findOneAndUpdate({_id: new ObjectID(invoice.projectMonth.projectMonthId)}, {$set: {verified: invoice.verified}});
198-
}
199-
200191
const invoiceResponse = {_id, ...invoice};
201192
const result: Array<any> = [{
202193
type: 'invoice',
203194
model: invoiceResponse,
204195
}];
205196
emitEntityEvent(req, SocketEventTypes.EntityUpdated, CollectionNames.INVOICES, invoiceResponse._id, invoiceResponse);
206197

207-
if (projectMonth && projectMonth.ok && projectMonth.value) {
208-
const projectMonthResponse = projectMonth.value;
209-
result.push({
210-
type: 'projectMonth',
211-
model: projectMonthResponse,
212-
});
213-
emitEntityEvent(req, SocketEventTypes.EntityUpdated, CollectionNames.PROJECTS_MONTH, projectMonthResponse._id, projectMonthResponse);
198+
return res.send(result);
199+
};
200+
201+
202+
203+
export const verifyInvoiceController = async (req: ConfacRequest, res: Response) => {
204+
const {id, verified}: { id: string; verified: boolean; } = req.body;
205+
206+
const saveResult = await req.db.collection<IInvoice>(CollectionNames.INVOICES)
207+
.findOneAndUpdate({_id: new ObjectID(id)}, {$set: {verified}}, {returnOriginal: true});
208+
209+
if (!saveResult?.value) {
210+
return res.status(404).send('Invoice not found');
214211
}
215212

216-
return res.send(result);
213+
if (saveResult.value.verified === verified) {
214+
return res.status(200).send();
215+
}
216+
217+
const originalInvoice = saveResult.value;
218+
const invoice = {...originalInvoice, verified};
219+
await saveAudit(req, 'invoice', originalInvoice, invoice);
220+
221+
const result: Array<any> = [{
222+
type: 'invoice',
223+
model: invoice,
224+
}];
225+
226+
emitEntityEvent(req, SocketEventTypes.EntityUpdated, CollectionNames.INVOICES, invoice._id, invoice);
227+
228+
if (invoice?.projectMonth?.projectMonthId) {
229+
let shouldUpdateProjectMonth = true;
230+
if (verified && invoice.creditNotas?.length) {
231+
const creditNotes = await req.db.collection<IInvoice>(CollectionNames.INVOICES)
232+
.find({_id: {$in: invoice.creditNotas.map(id => new ObjectID(id))}})
233+
.toArray();
234+
235+
shouldUpdateProjectMonth = creditNotes.every(creditNote => creditNote.verified);
236+
}
237+
238+
if (shouldUpdateProjectMonth) {
239+
const projectMonth = await req.db.collection(CollectionNames.PROJECTS_MONTH)
240+
.findOneAndUpdate({_id: new ObjectID(invoice.projectMonth.projectMonthId)}, {$set: {verified}}, {returnOriginal: false});
241+
242+
emitEntityEvent(req, SocketEventTypes.EntityUpdated, CollectionNames.PROJECTS_MONTH, invoice.projectMonth.projectMonthId, projectMonth.value);
243+
244+
result.push({
245+
type: 'projectMonth',
246+
model: projectMonth.value,
247+
});
248+
}
249+
}
250+
251+
return res.status(200).send(result);
217252
};
218253

219254

backend/src/routes/invoices.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {Router} from 'express';
22
import { emailInvoiceController } from '../controllers/emailInvoices';
33
import {
44
getInvoicesController, createInvoiceController, previewPdfInvoiceController, deleteInvoiceController,
5-
updateInvoiceController, generateExcelForInvoicesController, getInvoiceXmlController,
5+
updateInvoiceController, generateExcelForInvoicesController, getInvoiceXmlController, verifyInvoiceController
66
} from '../controllers/invoices';
77

88
const invoicesRouter = Router();
@@ -15,6 +15,7 @@ invoicesRouter.post('/preview', previewPdfInvoiceController);
1515
invoicesRouter.post('/excel', generateExcelForInvoicesController);
1616

1717
invoicesRouter.put('/', updateInvoiceController as any);
18+
invoicesRouter.put('/verify', verifyInvoiceController as any);
1819

1920
invoicesRouter.delete('/', deleteInvoiceController as any);
2021
invoicesRouter.get('/xml/:id', getInvoiceXmlController);

frontend/src/actions/invoiceActions.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,33 @@ export const syncCreditNotas = (invoice: InvoiceModel, previousCreditNotas: stri
116116

117117

118118

119-
export function toggleInvoiceVerify(data: InvoiceModel) {
120-
const successMsg = data.verified ? t('invoice.isNotVerifiedConfirm') : t('invoice.isVerifiedConfirm');
121-
const newData: InvoiceModel | any = {...data, verified: !data.verified};
122-
return updateInvoiceRequest(newData, successMsg, false); // change andGoHome? also need 'navigate' from router
119+
export function toggleInvoiceVerify(data: InvoiceModel, toggleBusy = true) {
120+
return dispatch => {
121+
if (toggleBusy)
122+
dispatch(busyToggle());
123+
124+
request.put(buildUrl('/invoices/verify'))
125+
.set('Content-Type', 'application/json')
126+
.set('Authorization', authService.getBearer())
127+
.set('x-socket-id', socketService.socketId)
128+
.set('Accept', 'application/json')
129+
.send({id: data._id, verified: !data.verified})
130+
.then(res => {
131+
if (res) {
132+
dispatch({
133+
type: ACTION_TYPES.MODELS_UPDATED,
134+
payload: res.body,
135+
});
136+
}
137+
138+
success(data.verified ? t('invoice.isNotVerifiedConfirm') : t('invoice.isVerifiedConfirm'));
139+
})
140+
.catch(catchHandler)
141+
.then(() => {
142+
if (toggleBusy)
143+
dispatch(busyToggle.off())
144+
});
145+
};
123146
}
124147

125148

frontend/src/components/enhancers/EnhanceWithBusySpinner.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import {SpinnerIcon} from '../controls/Icon';
33

44

55
type EnhanceWithBusySpinnerProps = {
6+
/**
7+
* Typically busy spinner requires the busyToggle to be dispatched
8+
* For some components we don't do a global busyToggle
9+
**/
10+
withoutStoreBusy?: boolean,
611
isBusy: boolean,
712
onClick: Function,
813
model: any,
@@ -27,8 +32,8 @@ export const EnhanceWithBusySpinner = <P extends object>(ComposedComponent: Reac
2732
}
2833

2934
render() {
30-
const {isBusy, onClick, model, ...props} = this.props;
31-
if (isBusy && this.state.isBusy) {
35+
const {isBusy, onClick, model, withoutStoreBusy, ...props} = this.props;
36+
if ((isBusy || withoutStoreBusy) && this.state.isBusy) {
3237
return <SpinnerIcon style={{marginLeft: 0}} />;
3338
}
3439

frontend/src/components/invoice/invoice-list/InvoiceVerifyIconToggle.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import {EnhanceWithClaim, EnhanceWithClaimProps} from '../../enhancers/EnhanceWi
99

1010
type InvoiceVerifyIconToggleProps = EnhanceWithClaimProps & {
1111
invoice: InvoiceModel,
12-
toggleValid?: (valid: boolean) => void;
12+
toggleBusy?: boolean,
1313
}
1414

15-
export const InvoiceVerifyIconToggle = EnhanceWithClaim(({invoice, toggleValid, ...props}: InvoiceVerifyIconToggleProps) => {
15+
export const InvoiceVerifyIconToggle = EnhanceWithClaim(({invoice, toggleBusy, ...props}: InvoiceVerifyIconToggleProps) => {
1616
const dispatch = useDispatch();
1717
if (invoice.isQuotation) {
1818
return null;
@@ -22,14 +22,10 @@ export const InvoiceVerifyIconToggle = EnhanceWithClaim(({invoice, toggleValid,
2222
const title = invoice.verified ? t('invoice.unverifyActionTooltip') : t('invoice.verifyActionTooltip', {days: daysPassed});
2323
return (
2424
<BusyVerifyIcon
25+
withoutStoreBusy={!toggleBusy}
2526
model={invoice}
2627
style={{marginLeft: 8}}
27-
onClick={() => {
28-
dispatch(toggleInvoiceVerify(invoice) as any);
29-
if (toggleValid) {
30-
toggleValid(!invoice.verified);
31-
}
32-
}}
28+
onClick={() => dispatch(toggleInvoiceVerify(invoice, toggleBusy) as any)}
3329
title={title}
3430
{...props}
3531
/>

frontend/src/components/invoice/invoice-table/InvoiceListRowActions.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,14 @@ export type InvoiceListRowAction = 'comment' | 'edit' | 'validate' | 'download'
1515

1616
type InvoiceListRowActionsProps = {
1717
invoice: InvoiceModel;
18-
/** When from the ProjectMonth listing, also update the state of the form there */
19-
toggleValid?: (valid: boolean) => void;
18+
toggleBusy?: boolean;
2019
/** Hides some buttons when true */
2120
small?: boolean;
2221
buttons?: InvoiceListRowAction[]
2322
hideEdit?: boolean
2423
}
2524

26-
export const InvoiceListRowActions = ({invoice, toggleValid, small = false, buttons, hideEdit}: InvoiceListRowActionsProps) => {
25+
export const InvoiceListRowActions = ({invoice, small = false, buttons, hideEdit, toggleBusy}: InvoiceListRowActionsProps) => {
2726
const dispatch = useDispatch();
2827
const invoiceType = invoice.isQuotation ? 'quotation' : 'invoice';
2928

@@ -54,7 +53,7 @@ export const InvoiceListRowActions = ({invoice, toggleValid, small = false, butt
5453
/>
5554
)}
5655
{!hideEdit && (buttons?.includes('validate') ?? true) &&
57-
<InvoiceVerifyIconToggle claim={Claim.ValidateInvoices} invoice={invoice} toggleValid={toggleValid} />
56+
<InvoiceVerifyIconToggle claim={Claim.ValidateInvoices} invoice={invoice} toggleBusy={toggleBusy} />
5857
}
5958
{(buttons?.includes('download') ?? true) && !small &&
6059
<InvoiceDownloadIcon invoice={invoice} fileType='pdf' />

frontend/src/components/invoice/invoice-table/getInvoiceListRowClass.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@ export function getInvoiceListRowClass(invoice: InvoiceModel, invoicePayDays: nu
77
return rowTableClassName;
88
}
99

10+
export function getInvoiceDueDateStyle(invoice: InvoiceModel): React.CSSProperties {
11+
const variant = getInvoiceDueDateVariant(invoice);
12+
switch (variant) {
13+
case 'danger':
14+
return {backgroundColor: '#f8d7da'};
15+
case 'warning':
16+
return {backgroundColor: '#fff3cd'};
17+
case 'info':
18+
return {backgroundColor: '#cff4fc'};
19+
default:
20+
return {backgroundColor: 'rgba(40, 167, 69, 0.2)'};
21+
}
22+
}
23+
1024
/**
1125
* Gets the Bootstrap variant based on the Invoice due date
1226
*/

frontend/src/components/project/controls/ProjectMonthProformaStatusSelect.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import {CSSProperties} from 'react';
21
import {ButtonGroup as ReactButtonGroup} from 'react-bootstrap';
32
import {ProjectMonthProformaStatus} from '../models/ProjectMonthModel';
43
import {EnhanceWithClaim} from '../../enhancers/EnhanceWithClaim';

frontend/src/components/project/models/ProjectMonthModel.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,11 @@ export interface ProjectMonthModel {
1919
timesheet: ProjectMonthTimesheet;
2020
inbound: ProjectMonthInbound;
2121
note?: string;
22-
comments: IComment[]
22+
comments: IComment[];
2323
/** The invoice orderNr when ProjectMonthConfig.changingOrderNr */
2424
orderNr: string;
2525
audit: IAudit;
2626
verified: ProjectMonthStatus;
27-
verifiedInvoices: string[];
2827
attachments: Attachment[];
2928
}
3029

frontend/src/components/project/models/getNewProject.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ export const getNewProjectMonth = (): ProjectMonthModel => ({
4848
comments: [],
4949
orderNr: '',
5050
verified: false,
51-
verifiedInvoices: [],
5251
attachments: [],
5352
audit: {} as IAudit,
5453
});

frontend/src/components/project/models/getProjectMonthFeature.tsx

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -124,14 +124,6 @@ const projectListConfig = (config: ProjectMonthFeatureBuilderConfig): IList<Full
124124
}, {
125125
key: 'outbound',
126126
value: p => <ProjectMonthOutboundCell fullProjectMonth={p} />,
127-
className: p => {
128-
if (p.invoice) {
129-
if (p.details.verified) {
130-
return 'validated';
131-
}
132-
}
133-
return undefined;
134-
},
135127
footer: (models: FullProjectMonthModel[]) => {
136128
if (!models.length) {
137129
return null;

frontend/src/components/project/project-month-list/table/outbound/OutboundInvoice.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,15 @@ import { InvoiceNumberCell } from '../../../../invoice/invoice-table/InvoiceNumb
33
import { InvoiceListRowActions } from '../../../../invoice/invoice-table/InvoiceListRowActions';
44
import { InvoiceEmail } from './InvoiceEmail';
55
import InvoiceModel from '../../../../invoice/models/InvoiceModel';
6-
import { CSSProperties } from 'react';
76

87
interface OutboundInvoiceProps {
98
invoice: InvoiceModel;
10-
toggleValid: (valid: boolean) => void;
119
className?: string;
12-
style?: CSSProperties
10+
style?: React.CSSProperties;
1311
}
1412

1513

16-
export const OutboundInvoice = ({ invoice, toggleValid, className, style }: OutboundInvoiceProps) => {
14+
export const OutboundInvoice = ({ invoice, className, style }: OutboundInvoiceProps) => {
1715
return (
1816
<div className={`outbound-invoice-cell ${className || ''}`} style={style}>
1917
<div>
@@ -27,7 +25,7 @@ export const OutboundInvoice = ({ invoice, toggleValid, className, style }: Outb
2725
<InvoiceEmail invoice={invoice} />
2826
</div>
2927
<div className="icons-cell">
30-
<InvoiceListRowActions invoice={invoice} toggleValid={toggleValid} small buttons={['validate', 'preview']} />
28+
<InvoiceListRowActions invoice={invoice} small buttons={['validate', 'preview']} toggleBusy={false} />
3129
</div>
3230
</div>
3331
);

frontend/src/components/project/project-month-list/table/outbound/ProjectMonthOutboundCell.tsx

Lines changed: 6 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ import { OutboundInvoice } from './OutboundInvoice';
1010
import { Claim } from '../../../../users/models/UserModel';
1111
import { useSelector } from 'react-redux';
1212
import { ConfacState } from '../../../../../reducers/app-state';
13-
import InvoiceModel from '../../../../invoice/models/InvoiceModel';
14-
import { getInvoiceDueDateVariant } from '../../../../invoice/invoice-table/getInvoiceListRowClass';
13+
import { getInvoiceDueDateStyle } from '../../../../invoice/invoice-table/getInvoiceListRowClass';
1514

1615

1716
interface ProjectMonthOutboundCellProps {
@@ -30,28 +29,8 @@ export const ProjectMonthOutboundCell = ({fullProjectMonth}: ProjectMonthOutboun
3029
const [orderNr, setOrderNr/* , saveOrderNr */] = useDebouncedSave<string>(fullProjectMonth.details.orderNr || '', dispatcher);
3130

3231

33-
const toggleValid = (verified: boolean | 'forced', invoice?: InvoiceModel) => {
34-
if (!invoice) {
35-
dispatch(patchProjectsMonth({...fullProjectMonth.details, verified}) as any);
36-
}
37-
38-
if (verified === 'forced') {
39-
dispatch(patchProjectsMonth({...fullProjectMonth.details, verified}) as any);
40-
}
41-
42-
if (verified) {
43-
dispatch(patchProjectsMonth({
44-
...fullProjectMonth.details,
45-
verified: invoice!.creditNotas.every(invoiceId => fullProjectMonth.details.verifiedInvoices.includes(invoiceId)),
46-
verifiedInvoices: [...fullProjectMonth.details.verifiedInvoices, invoice!._id]
47-
}) as any);
48-
} else {
49-
dispatch(patchProjectsMonth({
50-
...fullProjectMonth.details,
51-
verified,
52-
verifiedInvoices: fullProjectMonth.details.verifiedInvoices.filter(n => n !== invoice!._id)
53-
}) as any);
54-
}
32+
const toggleValid = (verified: boolean | 'forced') => {
33+
dispatch(patchProjectsMonth({...fullProjectMonth.details, verified}) as any);
5534
};
5635

5736
const ValidityToggle = (
@@ -120,16 +99,9 @@ export const ProjectMonthOutboundCell = ({fullProjectMonth}: ProjectMonthOutboun
12099
<OutboundInvoice
121100
key={i!.number}
122101
invoice={i!}
123-
toggleValid={(valid) => toggleValid(valid, i!)}
124-
className={
125-
fullProjectMonth.details.verifiedInvoices.includes(i!._id) ?
126-
'validated' :
127-
`table-${getInvoiceDueDateVariant(i!)}`
128-
}
129-
style={{backgroundColor: !fullProjectMonth.details.verifiedInvoices.includes(i!._id) ? 'var(--bs-table-bg)' : undefined}}
102+
style={getInvoiceDueDateStyle(i!)}
130103
/>
131-
))
132-
}
104+
))}
133105
</>
134-
)
106+
);
135107
};

0 commit comments

Comments
 (0)