Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/Organisation Lookup #2756

Merged
merged 28 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
47b4f00
add organisation lookup component
HauklandJ Nov 26, 2024
1f2d5ac
render organisation lookup component, add text resoruce
HauklandJ Nov 26, 2024
c2bf4e1
add ajv-errors and validation
HauklandJ Nov 26, 2024
be266c7
fix validation algorithm, add test
HauklandJ Nov 27, 2024
8f9a23e
add decription text resources
HauklandJ Nov 27, 2024
3f43109
additional validation tests
HauklandJ Nov 27, 2024
3751dab
fix css issue when error is present
HauklandJ Nov 27, 2024
f624994
update model for serialization from organisation lookup
HauklandJ Nov 27, 2024
20f9d54
update text resources to be exhaustive
HauklandJ Nov 27, 2024
7c62f75
add displaydata test for organisation lookup
HauklandJ Nov 27, 2024
380b1a7
show organisation name on successful lookup
HauklandJ Nov 28, 2024
d19fa6b
make persisting org name opt-in
HauklandJ Nov 28, 2024
db23078
add summary for organisation lookup
HauklandJ Nov 28, 2024
f24a604
add cypress tests
HauklandJ Nov 29, 2024
9c89e91
Merge branch 'main' into feature/organisation-lookup
HauklandJ Nov 29, 2024
6fa9dad
fix casing
HauklandJ Nov 29, 2024
b027b82
queryOptions
HauklandJ Nov 29, 2024
fe7fd9b
midas (camillas) touch
HauklandJ Nov 29, 2024
352e4be
cleanup fault handling
HauklandJ Dec 3, 2024
a75ad0f
update url
HauklandJ Dec 3, 2024
88ff419
make http response explicitly typed
HauklandJ Dec 3, 2024
8c22af5
Merge branch 'main' into feature/organisation-lookup
HauklandJ Dec 4, 2024
eb29969
add feildset
HauklandJ Dec 4, 2024
b5f600c
update and add to tests
HauklandJ Dec 4, 2024
825aafc
update summary to include title
HauklandJ Dec 4, 2024
47ce82b
use underscore to denote no usage of properties
HauklandJ Dec 4, 2024
08324ee
simplify http response serializing
HauklandJ Dec 4, 2024
5751967
Merge branch 'main' into feature/organisation-lookup
HauklandJ Dec 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "Display value of Organisation Lookup component",
"expression": [
"displayValue",
"organisationlookup"
],
"context": {
"component": "organisationlookup",
"currentLayout": "Page"
},
"expects": "043871668",
"layouts": {
"Page": {
"$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json",
"data": {
"layout": [
{
"id": "organisationlookup",
"type": "OrganisationLookup",
"dataModelBindings": {
"organisation_lookup_orgnr": "OrganisationLookup.OrgNr"
}
}
]
}
}
},
"dataModel": {
"OrganisationLookup": {
"OrgNr": "043871668"
}
}
}
9 changes: 9 additions & 0 deletions src/language/texts/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,15 @@ export function en() {
vat: 'VAT',
},
},
organisation_lookup: {
orgnr_label: 'Organisation number',
org_name: 'Organisation name',
from_registry_description: 'From the CCR',
validation_error_not_found: 'Organisation number not found in the registry',
validation_invalid_response_from_server: 'Invalid response from the server',
unknown_error: 'An unknown error occurred. Please try again later',
validation_error_orgnr: 'The organisation number is invalid',
},
person_lookup: {
ssn_label: 'National ID number/D-number',
surname_label: 'Surname',
Expand Down
9 changes: 9 additions & 0 deletions src/language/texts/nb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,15 @@ export function nb(): FixedLanguageList {
vat: 'MVA',
},
},
organisation_lookup: {
orgnr_label: 'Organisasjonsnummer',
org_name: 'Organisasjonsnavn',
from_registry_description: 'Fra enhetsregisteret',
validation_error_not_found: 'Organisasjonsnummeret ble ikke funnet i enhetsregisteret',
validation_invalid_response_from_server: 'Ugyldig respons fra server',
unknown_error: 'Ukjent feil. Vennligst prøv igjen senere',
validation_error_orgnr: 'Organisasjonsnummeret er ugyldig',
},
person_lookup: {
ssn_label: 'Fødselsnummer',
surname_label: 'Etternavn',
Expand Down
9 changes: 9 additions & 0 deletions src/language/texts/nn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,15 @@ export function nn(): FixedLanguageList {
vat: 'MVA',
},
},
organisation_lookup: {
orgnr_label: 'Organisasjonsnummer',
org_name: 'Organisasjonsnamn',
from_registry_description: 'Frå enhetsregisteret',
validation_error_not_found: 'Organisasjonsnummeret er ikkje funne i registeret',
validation_invalid_response_from_server: 'Ugyldig respons frå server',
unknown_error: 'Ukjent feil. Ver venleg og prøv igjen seinare',
validation_error_orgnr: 'Organisasjonsnummeret er ugyldig',
},
person_lookup: {
ssn_label: 'Fødselsnummer',
surname_label: 'Etternamn',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/* breakpoints-xs */
@media only screen and (min-width: 0) {
.componentWrapper {
grid-template-areas:
'orgnrLabel'
'orgnr'
'orgname'
'submit'
'apiError';
gap: var(--fds-spacing-2);
}
}

/* breakpoints-md */
@media only screen and (min-width: 840px) {
.componentWrapper {
grid-template-columns: repeat(2, minmax(250px, 250px)) auto;
grid-template-areas:
'orgnrLabel .'
'orgnr submit'
'orgname .'
'apiError .';
align-items: end;
column-gap: var(--fds-spacing-2);
}
}

.componentWrapper {
display: grid;

.orgnrLabel {
grid-area: orgnrLabel;
align-self: start;
}

.orgnr {
grid-area: orgnr;
align-self: start;
}

.orgname {
grid-area: orgname;
align-self: start;
}

.submit {
grid-area: submit;
align-self: start;
white-space: nowrap;
}
}
210 changes: 210 additions & 0 deletions src/layout/OrganisationLookup/OrganisationLookupComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import React, { useState } from 'react';

import { ErrorMessage } from '@digdir/designsystemet-react';
import { queryOptions, useQuery } from '@tanstack/react-query';

import type { PropsFromGenericComponent } from '..';

import { Button } from 'src/app-components/Button/Button';
import { NumericInput } from 'src/app-components/Input/NumericInput';
import { Fieldset } from 'src/app-components/Label/Fieldset';
import { Label } from 'src/app-components/Label/Label';
import { Description } from 'src/components/form/Description';
import { RequiredIndicator } from 'src/components/form/RequiredIndicator';
import { getDescriptionId } from 'src/components/label/Label';
import { useDataModelBindings } from 'src/features/formData/useDataModelBindings';
import { Lang } from 'src/features/language/Lang';
import { useLanguage } from 'src/features/language/useLanguage';
import { ComponentValidations } from 'src/features/validation/ComponentValidations';
import { useBindingValidationsForNode } from 'src/features/validation/selectors/bindingValidationsForNode';
import { hasValidationErrors } from 'src/features/validation/utils';
import { ComponentStructureWrapper } from 'src/layout/ComponentStructureWrapper';
import classes from 'src/layout/OrganisationLookup/OrganisationLookupComponent.module.css';
import { validateOrganisationLookupResponse, validateOrgnr } from 'src/layout/OrganisationLookup/validation';
import { useLabel } from 'src/utils/layout/useLabel';
import { useNodeItem } from 'src/utils/layout/useNodeItem';
import { httpGet } from 'src/utils/network/networking';
import { appPath } from 'src/utils/urls/appUrlHelper';

const orgLookupQueries = {
lookup: (orgNr: string) =>
queryOptions({
queryKey: [{ scope: 'organisationLookup', orgNr }],
queryFn: () => fetchOrg(orgNr),
enabled: false,
gcTime: 0,
}),
};

export type Organisation = {
orgNr: string;
name: string;
};
export type OrganisationLookupResponse =
| { success: false; organisationDetails: null }
| { success: true; organisationDetails: Organisation };

async function fetchOrg(orgNr: string): Promise<{ org: Organisation; error: null } | { org: null; error: string }> {
if (!orgNr) {
throw new Error('orgNr is required');
}
const url = `${appPath}/api/v1/lookup/organisation/${orgNr}`;

try {
const response = await httpGet(url);

if (!validateOrganisationLookupResponse(response)) {
return { org: null, error: 'organisation_lookup.validation_invalid_response_from_server' };
}

if (!response.success || !response.organisationDetails) {
HauklandJ marked this conversation as resolved.
Show resolved Hide resolved
return { org: null, error: 'organisation_lookup.validation_error_not_found' };
}

return { org: response.organisationDetails, error: null };
} catch {
return { org: null, error: 'organisation_lookup.unknown_error' };
}
}

export function OrganisationLookupComponent({
node,
overrideDisplay,
}: PropsFromGenericComponent<'OrganisationLookup'>) {
const { id, dataModelBindings, required } = useNodeItem(node);
const { labelText, getHelpTextComponent, getDescriptionComponent } = useLabel({ node, overrideDisplay });
const [orgName, setOrgName] = useState('');
const [tempOrgNr, setTempOrgNr] = useState('');
const [orgNrErrors, setOrgNrErrors] = useState<string[]>();

const {
formData: { organisation_lookup_orgnr },
setValue,
} = useDataModelBindings(dataModelBindings);

const bindingValidations = useBindingValidationsForNode(node);
const { langAsString } = useLanguage();

const { data, refetch: performLookup, isFetching } = useQuery(orgLookupQueries.lookup(tempOrgNr));

function handleValidateOrgnr(orgNr: string) {
if (!validateOrgnr({ orgNr })) {
const errors = validateOrgnr.errors
?.filter((error) => error.instancePath === '/orgNr')
.map((error) => error.message)
.filter((it) => it != null);

setOrgNrErrors(errors);
return false;
}
setOrgNrErrors(undefined);
return true;
}

async function handleSubmit() {
const isValid = handleValidateOrgnr(tempOrgNr);

if (!isValid) {
return;
}

const { data } = await performLookup();
if (data?.org) {
setOrgName(data.org.name);
setValue('organisation_lookup_orgnr', data.org.orgNr);
dataModelBindings.organisation_lookup_name && setValue('organisation_lookup_name', data.org.name);
}
}

function handleClear() {
setValue('organisation_lookup_orgnr', '');
dataModelBindings.organisation_lookup_name && setValue('organisation_lookup_name', '');
setOrgName('');
setTempOrgNr('');
}

const hasSuccessfullyFetched = !!organisation_lookup_orgnr;

return (
<Fieldset
legend={labelText}
legendSize='lg'
description={getDescriptionComponent()}
help={getHelpTextComponent()}
size='sm'
>
<ComponentStructureWrapper node={node}>
<div className={classes.componentWrapper}>
<div className={classes.orgnrLabel}>
<Label
htmlFor={`${id}_orgnr`}
label={langAsString('organisation_lookup.orgnr_label')}
required={required}
requiredIndicator={<RequiredIndicator required={required} />}
description={
hasSuccessfullyFetched ? (
<Description
description={langAsString('organisation_lookup.from_registry_description')}
componentId={`${id}_orgnr`}
/>
) : undefined
}
/>
</div>
<NumericInput
id={`${id}_orgnr`}
aria-describedby={hasSuccessfullyFetched ? getDescriptionId(`${id}_orgnr`) : undefined}
value={hasSuccessfullyFetched ? organisation_lookup_orgnr : tempOrgNr}
className={classes.orgnr}
required={required}
readOnly={hasSuccessfullyFetched || isFetching}
error={
(orgNrErrors?.length && <Lang id={orgNrErrors.join(' ')} />) ||
(hasValidationErrors(bindingValidations?.organisation_lookup_orgnr) && (
<ComponentValidations validations={bindingValidations?.organisation_lookup_orgnr} />
))
}
onValueChange={(e) => {
setTempOrgNr(e.value);
}}
onBlur={(e) => handleValidateOrgnr(e.target.value)}
allowLeadingZeros
/>
<div className={classes.submit}>
{!hasSuccessfullyFetched ? (
<Button
onClick={handleSubmit}
variant='secondary'
isLoading={isFetching}
>
Hent opplysninger
</Button>
) : (
<Button
variant='secondary'
color='danger'
onClick={handleClear}
>
Fjern
</Button>
)}
</div>
{data?.error && (
<ErrorMessage
size='sm'
style={{ gridArea: 'apiError' }}
>
<Lang id={data.error} />
</ErrorMessage>
)}
<div
className={classes.orgname}
aria-label={langAsString('organisation_lookup.org_name')}
>
{hasSuccessfullyFetched && <span>{orgName}</span>}
</div>
</div>
</ComponentStructureWrapper>
</Fieldset>
);
}
29 changes: 29 additions & 0 deletions src/layout/OrganisationLookup/OrganisationLookupSummary.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* breakpoints-xs */
@media only screen and (min-width: 0) {
.organisationLookupSummary {
display: flex;
flex-direction: column;
width: 100%;
}
}

/* breakpoints-md */
@media only screen and (min-width: 840px) {
.organisationLookupSummary {
flex-direction: row;
}
}

.organisationSummaryWrapper {
display: flex;
flex-direction: column;
gap: var(--fds-spacing-6);
width: 100%;
}

.organisationLookupSummaryNr {
flex-grow: 5;
}
.organisationLookupSummaryName {
flex-grow: 4;
}
Loading
Loading