Skip to content

Commit 0a47310

Browse files
committed
EditClient / ClientModal / NewClientForm: consolidate & strengthen
1 parent 39e94b9 commit 0a47310

File tree

15 files changed

+257
-257
lines changed

15 files changed

+257
-257
lines changed

frontend/src/components/client/EditClient.tsx

Lines changed: 28 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,34 @@
1-
import {useState} from 'react';
2-
import {useDispatch, useSelector} from 'react-redux';
3-
import {Container, Row, Form, Alert} from 'react-bootstrap';
4-
import {t} from '../utils';
5-
import {saveClient} from '../../actions/index';
6-
import {defaultClientProperties} from './models/ClientConfig';
7-
import {getNewClient} from './models/getNewClient';
8-
import {ClientModel} from './models/ClientModels';
9-
import {ConfacState} from '../../reducers/app-state';
10-
import {ConfigModel} from '../config/models/ConfigModel';
11-
import {StickyFooter} from '../controls/other/StickyFooter';
12-
import {NewClient} from './NewClient';
13-
import {ArrayInput} from '../controls/form-controls/inputs/ArrayInput';
14-
import {BusyButton} from '../controls/form-controls/BusyButton';
15-
import {useDocumentTitle} from '../hooks/useDocumentTitle';
16-
import {ClientAttachmentsForm} from './controls/ClientAttachmentsForm';
17-
import {Audit} from '../admin/audit/Audit';
18-
import {Claim} from '../users/models/UserModel';
19-
import {useParams} from 'react-router-dom';
20-
import {InvoiceLine} from '../invoice/models/InvoiceLineModels';
1+
import { useDispatch } from 'react-redux';
2+
import { useParams } from 'react-router-dom';
3+
import { Container, Row, Form, Alert } from 'react-bootstrap';
4+
import { t } from '../utils';
5+
import { saveClient } from '../../actions/index';
6+
import { defaultClientProperties } from './models/ClientConfig';
7+
import { ClientModel } from './models/ClientModels';
8+
import { StickyFooter } from '../controls/other/StickyFooter';
9+
import { NewClient } from './NewClient';
10+
import { ArrayInput } from '../controls/form-controls/inputs/ArrayInput';
11+
import { BusyButton } from '../controls/form-controls/BusyButton';
12+
import { useDocumentTitle } from '../hooks/useDocumentTitle';
13+
import { ClientAttachmentsForm } from './controls/ClientAttachmentsForm';
14+
import { Audit } from '../admin/audit/Audit';
15+
import { Claim } from '../users/models/UserModel';
2116
import useEntityChangedToast from '../hooks/useEntityChangedToast';
2217
import { NotesWithCommentsModalButton } from '../controls/form-controls/button/NotesWithCommentsModalButton';
23-
24-
25-
/** Different spellings of "Belgium": TODO: this should just be 'BE' now */
26-
export const belgiums = ['België', 'Belgium', 'Belgie', 'BE'];
27-
28-
29-
function getClient(client: ClientModel | undefined, config: ConfigModel) {
30-
if (client) {
31-
return JSON.parse(JSON.stringify(client));
32-
}
33-
return getNewClient(config);
34-
}
18+
import { useClientState } from './client-helpers';
3519

3620

3721
const EditClient = () => {
3822
const params = useParams();
39-
const config = useSelector((state: ConfacState) => state.config);
40-
const storeClient = useSelector((state: ConfacState) => state.clients.find(x => x.slug === params.id));
41-
const initClient = getClient(storeClient, config);
42-
const [client, setClient] = useState<ClientModel>(initClient);
43-
const clientWithSameKbo = useSelector((state: ConfacState) => state.clients.filter(x => x.btw === client.btw).find(x => x.slug !== params.id));
44-
45-
useEntityChangedToast(client._id);
46-
4723
const dispatch = useDispatch();
48-
// useEffect(() => window.scrollTo(0, 0)); // TODO: each keystroke made it scroll to top :(
49-
useDocumentTitle('clientEdit', {name: client.name});
50-
51-
52-
if (storeClient && !client._id) {
53-
setClient(storeClient);
54-
}
55-
56-
const clientAlreadyExists = !!clientWithSameKbo && client.btw && client.btw !== t('taxRequest');
57-
const isClientDisabled = (client: ClientModel): boolean => {
58-
if (clientAlreadyExists || client.name.length === 0) {
59-
return true;
60-
}
61-
if (client.slug && client.slug.length === 0) {
62-
// slug can only be filled in for an existing invoice
63-
// (it's set on the backend create)
64-
return true;
65-
}
66-
if (!client.btw)
67-
return true;
68-
69-
return false;
70-
}
24+
const {client, setClient, clientAlreadyExists, canSaveClient} = useClientState(params.id);
7125

26+
useEntityChangedToast(client?._id);
27+
useDocumentTitle('clientEdit', {name: client?.name || ''});
7228

7329
if (!client) {
74-
return null;
75-
}
76-
77-
const setClientIntercept = (newClient: ClientModel): void => {
78-
if (newClient.country && client.country !== newClient.country && !client.defaultInvoiceLines.length && !belgiums.includes(newClient.country)) {
79-
let btwRemark: string;
80-
switch (newClient.country) {
81-
case 'UK':
82-
btwRemark = 'Belgian VAT not applicable - Article 44 EU Directive 2006/112/EC';
83-
break;
84-
default:
85-
btwRemark = 'Vrijgesteld van BTW overeenkomstig art. 39bis W. BTW';
86-
}
87-
88-
const invoiceLines = config.defaultInvoiceLines.map(x => ({...x, tax: 0}));
89-
invoiceLines.push({type: 'section', desc: btwRemark, sort: invoiceLines.length} as InvoiceLine);
90-
newClient.defaultInvoiceLines = invoiceLines;
91-
}
92-
setClient(newClient);
93-
};
94-
95-
if (!client.btw && !initClient._id) {
9630
return (
97-
<NewClient
98-
client={client}
99-
onChange={(value: ClientModel) => setClientIntercept({...client, ...value})}
100-
/>
31+
<NewClient onChange={(value: ClientModel) => setClient(value)} />
10132
);
10233
}
10334

@@ -106,7 +37,7 @@ const EditClient = () => {
10637
<Form>
10738
<Row>
10839
<h1 style={{marginBottom: 10}}>
109-
{client.name || (initClient._id ? '' : t('client.createNew'))}
40+
{client.name || (client._id ? '' : t('client.createNew'))}
11041
<NotesWithCommentsModalButton
11142
claim={Claim.ManageClients}
11243
value={{comments: client.comments || []}}
@@ -115,27 +46,29 @@ const EditClient = () => {
11546
style={{marginLeft: 6, marginBottom: 6}}
11647
showNote={false}
11748
/>
118-
<Audit model={storeClient} modelType="client" />
49+
<Audit model={client} modelType="client" />
11950
</h1>
120-
{clientAlreadyExists && <Alert variant="danger">{t('client.alreadyExists', {btw: client.btw})}</Alert>}
51+
{clientAlreadyExists && (
52+
<Alert variant="danger">{t('client.alreadyExists', {btw: client.btw})}</Alert>
53+
)}
12154
</Row>
12255
<Row>
12356
<ArrayInput
12457
config={defaultClientProperties}
12558
model={client}
126-
onChange={value => setClientIntercept({...client, ...value})}
59+
onChange={value => setClient({...client, ...value})}
12760
tPrefix="client."
12861
/>
12962
</Row>
13063

131-
<ClientAttachmentsForm model={initClient} />
64+
<ClientAttachmentsForm model={client} />
13265

13366
</Form>
13467
<StickyFooter claim={Claim.ManageClients}>
13568
<BusyButton
13669
onClick={() => dispatch(saveClient(client) as any)}
13770
className="tst-save-client"
138-
disabled={isClientDisabled(client)}
71+
disabled={!canSaveClient}
13972
>
14073
{t('save')}
14174
</BusyButton>
Lines changed: 67 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,92 @@
1-
import {useState} from 'react';
2-
import {Container, Row, Form, Col} from 'react-bootstrap';
3-
import {ClientModel} from './models/ClientModels';
4-
import {t} from '../utils';
5-
import {BtwInput, BtwResponse, formatBtw} from '../controls/form-controls/inputs/BtwInput';
6-
import {useDocumentTitle} from '../hooks/useDocumentTitle';
1+
import { useState } from 'react';
2+
import { Container, Row, Form } from 'react-bootstrap';
3+
import { ClientModel, ClientType } from './models/ClientModels';
4+
import { t } from '../utils';
5+
import { BtwInput, BtwResponse } from '../controls/form-controls/inputs/BtwInput';
6+
import { useDocumentTitle } from '../hooks/useDocumentTitle';
77
import { getNewClient } from './models/getNewClient';
88
import { useSelector } from 'react-redux';
99
import { ConfacState } from '../../reducers/app-state';
10-
import { ConfigModel } from '../config/models/ConfigModel';
10+
import { countries } from '../controls/other/CountrySelect';
11+
import { btwResponseToModel, useClientAlreadyExists } from './client-helpers';
12+
import { Alert } from 'react-bootstrap';
13+
1114

1215
type NewClientProps = {
13-
client: ClientModel,
1416
onChange: (client: ClientModel) => void,
1517
}
1618

17-
/** Returns a partial ClientModel with the data from the BTW lookup */
18-
export function btwResponseToModel(config: ConfigModel, btw: BtwResponse): ClientModel {
19-
return {
20-
...getNewClient(config),
21-
name: btw.name,
22-
types: ['client', 'endCustomer'],
23-
btw: formatBtw(`${btw.countryCode}${btw.vatNumber}`),
24-
address: `${btw.address.street} ${btw.address.number}`,
25-
postalCode: btw.address.zip_code,
26-
city: btw.address.city,
27-
country: 'BE', // btw.address.country
28-
};
29-
}
30-
31-
3219
/**
3320
* Enter a btw nr with lookup company details with external API
3421
* and display those details.
3522
* Allow creation with "btw in aanvraag"
3623
**/
37-
export const NewClient = (props: NewClientProps) => {
24+
export const NewClient = ({onChange}: NewClientProps) => {
3825
useDocumentTitle('clientNew');
39-
const config = useSelector((state: ConfacState) => state.config);
40-
const [client, setClient] = useState<ClientModel | null>(null);
41-
const [btw, setBtw] = useState<string>(props.client.btw);
42-
43-
const onBtwChange = (res: BtwResponse) => {
44-
setClient(btwResponseToModel(config, res));
45-
};
4626

4727
return (
4828
<Container className="edit-container">
4929
<Form>
5030
<Row>
5131
<h1>{t('client.createNew')}</h1>
5232
</Row>
53-
<Row>
54-
<Col lg={8} md={10} sm={12}>
55-
<BtwInput
56-
value={btw}
57-
onChange={(val: string) => setBtw(val)}
58-
onBtwChange={onBtwChange}
59-
onFinalize={(val: string) => {
60-
return (client ? props.onChange(client) : props.onChange({btw: val || ' '} as ClientModel))
61-
}}
62-
/>
63-
</Col>
64-
</Row>
65-
{client && (
66-
<Row style={{marginTop: 25}}>
67-
<Col md={6} sm={12}>
68-
<h3>{client.name}</h3>
69-
<div>{client.address}</div>
70-
<div>{client.postalCode} {client.city}</div>
71-
<div>{client.country}</div>
72-
</Col>
73-
</Row>
74-
)}
33+
<NewClientForm
34+
onFinalize={onChange}
35+
fullWidth={false}
36+
/>
7537
</Form>
7638
</Container>
7739
);
7840
};
41+
42+
43+
44+
type NewClientFormProps = {
45+
onFinalize: (client: ClientModel) => void;
46+
/** Full width on a modal but on a full screen make the btw input smaller */
47+
fullWidth: boolean;
48+
newClientTypes?: ClientType[];
49+
}
50+
51+
52+
export const NewClientForm = ({onFinalize, fullWidth, newClientTypes}: NewClientFormProps) => {
53+
const config = useSelector((state: ConfacState) => state.config);
54+
const [btw, setBtw] = useState<string>('');
55+
const [btwResponse, setBtwResponse] = useState<ClientModel | null>(null);
56+
57+
const clientAlreadyExists = useClientAlreadyExists(btwResponse);
58+
59+
return (
60+
<>
61+
<Row>
62+
<div className={fullWidth ? '' : 'col-lg-8 col-md-10 col-sm-12'}>
63+
<BtwInput
64+
value={btw}
65+
onChange={(val: string) => setBtw(val)}
66+
onFinalize={(btw: string, btwResp?: BtwResponse) => {
67+
if (btwResp && btwResp.valid) {
68+
onFinalize(btwResponseToModel(config, btwResp, newClientTypes));
69+
} else {
70+
onFinalize({...getNewClient(config), btw });
71+
}
72+
}}
73+
onBtwChange={btw => setBtwResponse(btwResponseToModel(config, btw, newClientTypes))}
74+
/>
75+
</div>
76+
</Row>
77+
{btwResponse && (
78+
<Row>
79+
<div className={fullWidth ? '' : 'col-lg-8 col-md-10 col-sm-12'}>
80+
{clientAlreadyExists && (
81+
<Alert variant="danger">{t('client.alreadyExists', {btw: btwResponse.btw})}</Alert>
82+
)}
83+
<h3>{btwResponse.name}</h3>
84+
<div>{btwResponse.address}</div>
85+
<div>{btwResponse.postalCode} {btwResponse.city}</div>
86+
<div>{countries.find(x => x.value === btwResponse.country)?.label || btwResponse.country}</div>
87+
</div>
88+
</Row>
89+
)}
90+
</>
91+
);
92+
};

0 commit comments

Comments
 (0)