Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • andretol/greg
1 result
Show changes
Commits on Source (22)
Showing
with 429 additions and 89 deletions
......@@ -6,6 +6,7 @@ REACT_APP_GUEST_CONSENT_STEP_ENABLED=true
REACT_APP_SUPPORT_MAIL=test@example.org
REACT_APP_INST=uio
REACT_APP_SUPPORT_URL=https://example.org
REACT_APP_SUPPORT_URL_EN=https://example.org/en
REACT_APP_RESPONSIBLE_ORGANIZATION='Seksjon for integrasjon og elektroniske identiteter (INT)'
REACT_APP_RESPONSIBLE_ORGANIZATION_LINK='https://www.usit.uio.no/om/organisasjon/bnt/usitint/'
\ No newline at end of file
REACT_APP_RESPONSIBLE_ORGANIZATION_LINK='https://www.usit.uio.no/om/organisasjon/bnt/usitint/'
......@@ -91,6 +91,8 @@
"statusText": {
"active": "Active",
"expired": "Expired",
"expiring_other": "Expiring in {{count}} days",
"expiring_one": "Expiring in {{count}} day",
"waitingForGuest": "Waiting for guest",
"waitingForSponsor": "Needs confirmation"
},
......@@ -162,7 +164,12 @@
"sponsorSubmitSuccessDescription": "Your registration has been completed. You will receive an e-mail when the guest has filled in the missing information, so that the guest account can be approved.",
"guestSubmitSuccessDescription": "Your registration is now completed. You will receive an e-mail or SMS when your account has been created.",
"confirmationDialog": {
"confirmIdentityText": "Are you sure you want to verify this identity?",
"text": {
"uio": "Before verifying this identity, please make sure that you have seen ID-papers with a picture matching the supplied value. Approved papers are passport, Norwegian driver's license, and Norwegian national ID card.",
"uib": "Are you sure you want to verify this identity?",
"default": "Are you sure you want to verify this identity?"
},
"confirmIdentityTitle": "Confirm?",
"cancelInvitation": "Cancel invitation?",
"cancelInvitationDescription": "Do you want to cancel the invitation?"
......
......@@ -91,6 +91,8 @@
"statusText": {
"active": "Aktiv",
"expired": "Utgått",
"expiring_other": "Utløper om {{count}} dager",
"expiring_one": "Utløper om {{count}} dag",
"waitingForGuest": "Venter på gjest",
"waitingForSponsor": "Trenger godkjenning"
},
......@@ -162,7 +164,11 @@
"sponsorSubmitSuccessDescription": "Din registrering er nå fullført. Du vil få en e-post når gjesten har fylt inn informasjonen som mangler, slik at gjestekontoen kan godkjennes.",
"guestSubmitSuccessDescription": "Din registrering er nå fullført. Du vil få en e-post eller SMS når kontoen er opprettet.",
"confirmationDialog": {
"confirmIdentityText": "Er du sikker på at du vil bekrefte denne identiteten?",
"text": {
"uio": "Vennligst sammenlign verdien du godkjenner mot ID-papirer med bilde, før du godkjenner. Godkjente papirer er pass, norsk førerkort og norsk nasjonalt ID-kort.",
"uib": "Er du sikker på at du vil bekrefte denne identiteten?",
"default": "Er du sikker på at du vil bekrefte denne identiteten?"
},
"confirmIdentityTitle": "Bekrefte?",
"cancelInvitation": "Kanseller invitasjon?",
"cancelInvitationDescription": "Vil du kansellere invitasjonen?"
......
......@@ -91,6 +91,8 @@
"statusText": {
"active": "Aktiv",
"expired": "Utgått",
"expiring_other": "Utløper om {{count}} dagar",
"expiring_one": "Utløper om {{count}} dag",
"waitingForGuest": "Venter på gjest",
"waitingForSponsor": "Trenger godkjenning"
},
......@@ -162,7 +164,12 @@
"sponsorSubmitSuccessDescription": "Di registrering er no fullført. Du vil få ein e-post når gjesten har fylt inn informasjonen som manglar, slik at gjestekontoen kan godkjennast.",
"guestSubmitSuccessDescription": "Di registrering er no fullført. Du vil få ein e-post eller SMS når kontoen er oppretta.",
"confirmationDialog": {
"confirmIdentityText": "Er du sikker på at du vil bekrefte denne identiteten?",
"text": {
"uio": "Vennligst sammenlign verdien du godkjenner mot ID-papirer med bilde, før du godkjenner. Godkjente papirer er pass, norsk førerkort og norsk nasjonalt ID-kort.",
"uib": "Er du sikker på at du vil bekrefte denne identiteten?",
"default": "Er du sikker på at du vil bekrefte denne identiteten?"
},
"confirmIdentityTitle": "Bekrefte?",
"cancelInvitation": "Kanseller invitasjon?",
"cancelInvitationDescription": "Vil du kansellere invitasjonen?"
......
......@@ -28,13 +28,26 @@ export const guestConsentStepEnabled: boolean =
env.REACT_APP_GUEST_CONSENT_STEP_ENABLED === 'true'
/* Footer content */
export const responsibleOrganization: string =
env.REACT_APP_RESPONSIBLE_ORGANIZATION as string
/* no links */
export const itRulesLink: string = env.REACT_APP_IT_RULES_LINK as string
export const privacyPolicyLink: string =
env.REACT_APP_PRIVACY_POLICY_LINK as string
export const technicalSupportLink: string = env.REACT_APP_SUPPORT_URL as string
export const documentationLink: string =
env.REACT_APP_DOCUMENTATION_LINK as string
export const responsibleOrganization: string =
env.REACT_APP_RESPONSIBLE_ORGANIZATION as string
export const responsibleOrganizationLink: string =
env.REACT_APP_RESPONSIBLE_ORGANIZATION_LINK as string
/* en links */
export const itRulesLinkEn: string = env.REACT_APP_IT_RULES_LINK_EN as string
export const privacyPolicyLinkEn: string =
env.REACT_APP_PRIVACY_POLICY_LINK_EN as string
export const technicalSupportLinkEn: string =
env.REACT_APP_SUPPORT_URL_EN as string
export const documentationLinkEn: string =
env.REACT_APP_DOCUMENTATION_LINK_EN as string
export const responsibleOrganizationLinkEn: string =
env.REACT_APP_RESPONSIBLE_ORGANIZATION_LINK_EN as string
......@@ -6,6 +6,7 @@ import { submitJsonOpts } from 'utils'
import CheckIcon from '@mui/icons-material/Check'
import { useTranslation } from 'react-i18next'
import { TableCell } from 'components/table'
import { appInst } from 'appConfig'
interface IdentityLineProps {
text: string
......@@ -32,6 +33,16 @@ const IdentityLine = ({
if (identity == null) {
return <></>
}
const getDialogText = () => {
switch (appInst) {
case 'uio':
return t('confirmationDialog.text.uio')
case 'uib':
return t('confirmationDialog.text.uib')
default:
return t('confirmationDialog.text.default')
}
}
return (
<TableRow>
<TableCell align="left">{text}</TableCell>
......@@ -52,7 +63,7 @@ const IdentityLine = ({
setOpen={setConfirmOpen}
onConfirm={verifyIdentity(identity.id)}
>
{t('confirmationDialog.confirmIdentityText')}
{getDialogText()}
</ConfirmDialog>
</Typography>
) : (
......
......@@ -39,6 +39,7 @@ function LanguageSelector({ noPadding }: { noPadding: boolean }) {
labelId="language-select"
label={t('header.selectLanguage')}
defaultValue={selectedLanguage}
renderValue={() => (i18n.language !== 'en' ? 'Language' : 'Språk')}
sx={sx}
variant="standard"
value={selectedLanguage}
......
......@@ -52,6 +52,7 @@ export type Role = {
end_date: Date
max_days: number
contact_person_unit: string | null
comments: string | null
}
export type FetchedRole = {
......@@ -64,6 +65,7 @@ export type FetchedRole = {
end_date: string
max_days: number
contact_person_unit: string | null
comments: string | null
}
export type ConsentType = {
......
......@@ -5,10 +5,15 @@ import { Link } from '@mui/material'
import {
responsibleOrganization,
responsibleOrganizationLink,
responsibleOrganizationLinkEn,
itRulesLink,
itRulesLinkEn,
privacyPolicyLink,
privacyPolicyLinkEn,
technicalSupportLink,
technicalSupportLinkEn,
documentationLink,
documentationLinkEn,
} from 'appConfig'
import { getFooterLogo } from './logos'
......@@ -70,7 +75,26 @@ StyledLink.defaultProps = {
const LogoContainer = styled('div')({})
function Footer() {
const { t } = useTranslation(['common', 'footer'])
const { t, i18n } = useTranslation(['common', 'footer'])
const getLink = (enLink: string | null, noLink: string | null) => {
let link
if (i18n.language === 'en') {
link = enLink || noLink
} else {
link = noLink || enLink
}
return link || ''
}
const getItRulesLink = () => getLink(itRulesLinkEn, itRulesLink)
const getTechnicalSupportLink = () =>
getLink(technicalSupportLinkEn, technicalSupportLink)
const getPrivacyPolicyLink = () =>
getLink(privacyPolicyLinkEn, privacyPolicyLink)
const getDocumentationLink = () =>
getLink(documentationLinkEn, documentationLink)
const getResponsibleOrganizationLink = () =>
getLink(responsibleOrganizationLinkEn, responsibleOrganizationLink)
return (
<FooterWrapper>
<InfoSection>
......@@ -79,20 +103,20 @@ function Footer() {
<LinkContainer>
<LinkSection>
<LinkHeader>{t('footer:link.terms.header')}</LinkHeader>
<StyledLink href={itRulesLink}>
<StyledLink href={getItRulesLink()}>
{t('footer:link.terms.ITRules')}
</StyledLink>
<StyledLink href={privacyPolicyLink}>
<StyledLink href={getPrivacyPolicyLink()}>
{t('footer:link.terms.privacy')}
</StyledLink>
</LinkSection>
<LinkSection>
<LinkHeader>{t('footer:link.help.header')}</LinkHeader>
<StyledLink href={technicalSupportLink}>
<StyledLink href={getTechnicalSupportLink()}>
{t('footer:link.help.contact')}
</StyledLink>
<StyledLink href={documentationLink}>
<StyledLink href={getDocumentationLink()}>
{t('footer:link.help.doc')}
</StyledLink>
</LinkSection>
......@@ -101,7 +125,7 @@ function Footer() {
<LinkHeader>
{t('footer:link.responsibleOrganization.header')}
</LinkHeader>
<StyledLink href={responsibleOrganizationLink}>
<StyledLink href={getResponsibleOrganizationLink()}>
{responsibleOrganization}
</StyledLink>
</LinkSection>
......
......@@ -3,6 +3,7 @@ import fetchMock, { enableFetchMocks } from 'jest-fetch-mock'
import { render, screen } from 'test-utils'
import AdapterDateFns from '@mui/lab/AdapterDateFns'
import { LocalizationProvider } from '@mui/lab'
import { FeatureContext } from 'contexts'
import GuestRegister from './index'
enableFetchMocks()
......@@ -13,9 +14,8 @@ const testData = {
last_name: 'Tester',
private_mobile: '+4797543910',
private_email: 'test@example.org',
fnr: '04062141242',
fnr: '08015214555',
passport: 'DK-123456',
date_of_birth: '1995-02-25',
},
role: {
ou_name_en: 'English organizational unit name',
......@@ -31,17 +31,24 @@ const testData = {
},
}
beforeEach(() => {
const allFeaturesOn = {
displayContactAtUnit: true,
displayComment: true,
displayContactAtUnitGuestInput: true,
showGenderFieldForGuest: true,
}
test('Field showing values correctly', async () => {
fetchMock.mockIf('/api/ui/v1/invited/', () =>
Promise.resolve<any>(JSON.stringify(testData))
)
})
test('Field showing values correctly', async () => {
render(
<LocalizationProvider dateAdapter={AdapterDateFns}>
<GuestRegister />
</LocalizationProvider>
<FeatureContext.Provider value={allFeaturesOn}>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<GuestRegister />
</LocalizationProvider>
</FeatureContext.Provider>
)
await screen.findByDisplayValue(testData.person.first_name)
......@@ -49,10 +56,13 @@ test('Field showing values correctly', async () => {
await screen.findByDisplayValue(testData.person.private_email)
await screen.findByDisplayValue(testData.person.fnr)
// Check that suggestions for date of birth and gender are showing
await screen.findByDisplayValue('1952-01-08')
await screen.findByDisplayValue('male')
// Passport nationality. The i18n-mock sets up en as the i18n.language property, so look for the English name
await screen.findByText('DK')
await screen.findByDisplayValue('123456')
await screen.findByDisplayValue(testData.person.date_of_birth)
// Mobile phone country code
await screen.findByDisplayValue('NO')
......@@ -69,3 +79,59 @@ test('Field showing values correctly', async () => {
// For the default setup the contact person at unit field should be showing
await screen.findByDisplayValue(testData.role.contact_person_unit)
})
test('Gender and birth date suggestions not showing if no national ID given', async () => {
// Clear the fnr
// @ts-ignore
testData.person.fnr = null
fetchMock.mockIf('/api/ui/v1/invited/', () =>
Promise.resolve<any>(JSON.stringify(testData))
)
render(
<FeatureContext.Provider value={allFeaturesOn}>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<GuestRegister />
</LocalizationProvider>
</FeatureContext.Provider>
)
// Wait a bit so that all the values are showing
await screen.findByDisplayValue(testData.person.first_name)
await screen.findByDisplayValue(testData.person.last_name)
// No national is given in the input data so there should be no
// suggestion for the birthdate or gender
const dateOfBirth = screen.queryByDisplayValue('1952-01-08')
expect(dateOfBirth).toBeNull()
const gender = screen.queryByDisplayValue('male')
expect(gender).toBeNull()
})
test('Gender and birth date suggestions not overriding existing values', async () => {
// Make the date of birth and national ID not match
testData.person.fnr = '08015214555'
// @ts-ignore
testData.person.date_of_birth = '1960-01-08'
// Also set the gender to female to check that it is not overridden by a suggestion
// @ts-ignore
testData.person.gender = 'female'
fetchMock.mockIf('/api/ui/v1/invited/', () =>
Promise.resolve<any>(JSON.stringify(testData))
)
render(
<FeatureContext.Provider value={allFeaturesOn}>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<GuestRegister />
</LocalizationProvider>
</FeatureContext.Provider>
)
// In this a date of birth was already set, and it should not have been overridden by a suggestion
// @ts-ignore
await screen.findByDisplayValue(testData.person.date_of_birth)
// Check that the gender has not been overridden
await screen.findByDisplayValue('female')
})
......@@ -38,9 +38,12 @@ const GuestConsentStep = forwardRef(
useFieldArray({ control, name: 'consents' })
const submit: SubmitHandler<GuestConsentData> = (data) => {
console.log('consent submit', data)
nextHandler(data)
// Exclude consents the guest did not accept an answer for
const submitdata: GuestConsentData = {
consents: data.consents?.filter((cons) => cons.choice !== null),
}
console.log('consent submit', submitdata)
nextHandler(submitdata)
}
const onSubmit = handleSubmit<GuestConsentData>(submit)
......
......@@ -17,7 +17,6 @@ function getEmptyGuestData(): GuestInviteInformation {
role_name_nb: '',
role_start: '',
role_end: '',
comment: '',
email: '',
mobile_phone: '',
date_of_birth: null,
......@@ -25,6 +24,7 @@ function getEmptyGuestData(): GuestInviteInformation {
passport: '',
countryForCallingCode: '',
authentication_method: AuthenticationMethod.Invite,
gender: '',
}
}
......
......@@ -27,7 +27,12 @@ import {
import { getAlpha2Codes, getName } from 'i18n-iso-countries'
import { DatePicker } from '@mui/lab'
import { subYears } from 'date-fns/fp'
import { isValidFnr, isValidMobilePhoneNumber } from 'utils'
import {
isValidFnr,
isValidMobilePhoneNumber,
extractGenderOrBlank,
extractBirthdateFromNationalId,
} from 'utils'
import { GuestInviteInformation } from '../guestDataForm'
import { GuestRegisterData } from '../enteredGuestData'
import { GuestRegisterCallableMethods } from '../guestRegisterCallableMethods'
......@@ -62,7 +67,14 @@ const GuestRegisterStep = forwardRef(
const [passportNationality, setPassportNationality] = useState<
string | undefined
>(undefined)
const [gender, setGender] = useState<string>('')
// Set suggestion for gender field is a gender is not already given in the input
const [gender, setGender] = useState<string>(
!initialGuestData.gender || !initialGuestData.gender.trim()
? extractGenderOrBlank(initialGuestData.fnr)
: initialGuestData.gender
)
const [idErrorState, setIdErrorState] = useState<string>('')
const [phoneErrorState, setPhoneErrorState] = useState<string>('')
const { displayContactAtUnitGuestInput } = useContext(FeatureContext)
......@@ -82,6 +94,19 @@ const GuestRegisterStep = forwardRef(
defaultValues: registerData ?? {},
})
// If there is no already a date of birth set, add a suggestion for
// this value based on the national ID, if it is set
if (
(!registerData || !registerData.dateOfBirth) &&
!initialGuestData.date_of_birth &&
initialGuestData.fnr
) {
const dateOfBirth = extractBirthdateFromNationalId(initialGuestData.fnr)
if (dateOfBirth) {
setValue('dateOfBirth', dateOfBirth)
}
}
const submit: SubmitHandler<GuestRegisterData> = async (data) => {
console.log('submit data is', data)
const result = await trigger()
......
......@@ -21,8 +21,8 @@ import Page from 'components/page'
import { useTranslation, Trans } from 'react-i18next'
import { Link } from 'react-router-dom'
import { Guest, Role } from 'interfaces'
import { isBefore } from 'date-fns'
import { differenceInDays, isBefore } from 'date-fns'
import { getRoleName, getRoleOuName } from 'utils'
import SponsorGuestButtons from '../../components/sponsorGuestButtons'
interface GuestProps {
......@@ -35,7 +35,8 @@ interface PersonLineProps {
}
interface StatusProps {
status: string
person: Guest
role: Role
}
interface GuestTableProps {
......@@ -81,8 +82,27 @@ const StyledTableHead = styled(TableHead)(({ theme }) => ({
borderRadius: '0',
}))
const Status = ({ status }: StatusProps) => {
const Status = ({ person, role }: StatusProps) => {
const today = new Date()
today.setHours(0, 0, 0, 0)
const { t } = useTranslation('common')
let status = ''
const days = differenceInDays(role.end_date, today)
if (!person.registered) {
status = 'waitingForGuest'
} else if (person.registered && !person.verified) {
status = 'waitingForSponsor'
} else if (person.registered && person.verified) {
if (isBefore(role.end_date, today)) {
status = 'expired'
} else if (days <= 7) {
status = 'expiring'
} else {
status = 'active'
}
}
switch (status) {
case 'active':
return (
......@@ -108,6 +128,12 @@ const Status = ({ status }: StatusProps) => {
{t('statusText.waitingForSponsor')}
</TableCell>
)
case 'expiring':
return (
<TableCell sx={{ color: 'blue' }} align="left">
<Trans t={t} i18nKey="statusText.expiring" count={days} />
</TableCell>
)
default:
return (
<TableCell sx={{ color: 'error.main' }} align="left">
......@@ -118,35 +144,16 @@ const Status = ({ status }: StatusProps) => {
}
const PersonLine = ({ person, role }: PersonLineProps) => {
const [t, i18n] = useTranslation(['common'])
const today = new Date()
today.setHours(0, 0, 0, 0)
let status = ''
if (!person.registered) {
status = 'waitingForGuest'
} else if (person.registered && !person.verified) {
status = 'waitingForSponsor'
} else if (person.registered && person.verified) {
if (!isBefore(role.end_date, today)) {
status = 'active'
} else {
status = 'expired'
}
}
const [t] = useTranslation(['common'])
return (
<StyledTableRow key={`${person.first} ${person.last}`}>
<TableCell component="th" scope="row">
{`${person.first} ${person.last}`}
</TableCell>
<TableCell align="left">
{i18n.language === 'en' ? role.name_en : role.name_nb}
</TableCell>
<Status status={status} />
<TableCell align="left">
{i18n.language === 'en' ? role.ou_en : role.ou_nb}
</TableCell>
<TableCell align="left">{getRoleName(role)}</TableCell>
<Status person={person} role={role} />
<TableCell align="left">{getRoleOuName(role)}</TableCell>
<TableCell align="left">
<Button
variant="contained"
......@@ -217,7 +224,9 @@ const InvitedGuests = ({ persons }: GuestProps) => {
<StyledAccordionSummary
expandIcon={<ArrowUpwardIcon sx={{ fill: 'black' }} />}
>
<Typography variant="h2">{t('common:sentInvitations')}</Typography>
<Typography variant="h2">
{`${t('common:sentInvitations')} (${guests.length})`}
</Typography>
</StyledAccordionSummary>
<AccordionDetails>
<Typography variant="body1">
......@@ -248,7 +257,9 @@ const ActiveGuests = ({ persons }: GuestProps) => {
<StyledAccordionSummary
expandIcon={<ArrowUpwardIcon sx={{ fill: 'black' }} />}
>
<Typography variant="h2">{t('common:activeGuests')}</Typography>
<Typography variant="h2">
{`${t('common:activeGuests')} (${guests.length})`}
</Typography>
</StyledAccordionSummary>
<AccordionDetails>
<Typography variant="body1">
......@@ -281,7 +292,7 @@ const WaitingGuests = ({ persons }: GuestProps) => {
expandIcon={<ArrowUpwardIcon sx={{ fill: 'black' }} />}
>
<Typography variant="h2">
{t('common:waitingGuests')}{' '}
{`${t('common:waitingGuests')} (${guests.length})`}{' '}
{guests.length > 0 && (
<NotificationsActiveIcon
sx={{ color: 'error.main', fontSize: '1.625rem' }}
......
......@@ -16,6 +16,7 @@ import SponsorInfoButtons from 'routes/components/sponsorInfoButtons'
import { DatePicker } from '@mui/lab'
import { Controller, SubmitHandler, useForm } from 'react-hook-form'
import { getRoleName, getRoleOuName, submitJsonOpts } from 'utils'
import { useFeatureContext } from 'contexts/featureContext'
interface GuestRoleInfoProps {
guest: Guest
......@@ -95,6 +96,7 @@ const TableContainer = styled(TableContainerMui)({
export default function GuestRoleInfo({ guest }: GuestRoleInfoProps) {
const { pid, id } = useParams<GuestRoleInfoParams>()
const [t] = useTranslation('common')
const { displayContactAtUnit, displayComment } = useFeatureContext()
const [role, setRole] = useState<Role>({
id: '',
name_nb: '',
......@@ -105,14 +107,8 @@ export default function GuestRoleInfo({ guest }: GuestRoleInfoProps) {
end_date: new Date(),
max_days: 365,
contact_person_unit: null,
comments: null,
})
// Find the role info relevant for this page
const getRoleInfo = () => {
const roleInfo = guest.roles.filter((ro) => ro.id.toString() === id)[0]
if (roleInfo) {
setRole(roleInfo)
}
}
// Prepare min and max date values
const today = new Date()
const todayPlusMaxDays = addDays(role.max_days)(today)
......@@ -151,10 +147,21 @@ export default function GuestRoleInfo({ guest }: GuestRoleInfoProps) {
const {
control,
handleSubmit,
setValue,
formState: { isDirty, isValid },
} = useForm({ mode: 'onChange' })
} = useForm<RoleFormData>({ mode: 'onChange' })
const onSubmit = handleSubmit(submit)
// Find the role info relevant for this page
const getRoleInfo = () => {
const roleInfo = guest.roles.filter((ro) => ro.id.toString() === id)[0]
if (roleInfo) {
setRole(roleInfo)
// Set values of date fields to current dates if role exists
setValue('end_date', roleInfo.end_date)
setValue('start_date', roleInfo.start_date)
}
}
useEffect(() => {
getRoleInfo()
}, [guest])
......@@ -232,13 +239,24 @@ export default function GuestRoleInfo({ guest }: GuestRoleInfoProps) {
<TableCell align="left">{getRoleOuName(role)}</TableCell>
<TableCell align="left" />
</TableRow>
<TableRow>
<TableCell align="left" sx={{ fontWeight: 'bold' }}>
{t('sponsor.contactPerson')}
</TableCell>
<TableCell align="left">{role.contact_person_unit}</TableCell>
<TableCell align="left" />
</TableRow>
{displayContactAtUnit && (
<TableRow>
<TableCell align="left" sx={{ fontWeight: 'bold' }}>
{t('sponsor.contactPerson')}
</TableCell>
<TableCell align="left">{role.contact_person_unit}</TableCell>
<TableCell align="left" />
</TableRow>
)}
{displayComment && (
<TableRow>
<TableCell align="left" sx={{ fontWeight: 'bold' }}>
{t('input.comment')}
</TableCell>
<TableCell align="left">{role.comments}</TableCell>
<TableCell align="left" />
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
......
......@@ -25,6 +25,7 @@ import { useTranslation } from 'react-i18next'
import { Link, useHistory, useParams } from 'react-router-dom'
import SponsorInfoButtons from 'routes/components/sponsorInfoButtons'
import { submitJsonOpts } from 'utils'
import { useFeatureContext } from 'contexts/featureContext'
type AddRoleFormData = {
orgunit: number
......@@ -104,6 +105,7 @@ function NewGuestRole({ guest, reloadGuestInfo }: NewGuestRoleProps) {
setValue,
getValues,
} = useForm<AddRoleFormData>()
const { displayContactAtUnit, displayComment } = useFeatureContext()
const { pid } = useParams<GuestInfoParams>()
const history = useHistory()
......@@ -279,20 +281,25 @@ function NewGuestRole({ guest, reloadGuestInfo }: NewGuestRoleProps) {
/>
)}
/>
<TextField
id="contact"
label={t('input.contactPersonUnit')}
multiline
rows={5}
{...register('contact_person_unit')}
/>
<TextField
id="comments"
label={t('input.comment')}
multiline
rows={5}
{...register('comments')}
/>
{displayContactAtUnit && (
<TextField
id="contact"
label={t('input.contactPersonUnit')}
multiline
rows={5}
{...register('contact_person_unit')}
/>
)}
{displayComment && (
<TextField
id="comments"
label={t('input.comment')}
multiline
rows={5}
{...register('comments')}
/>
)}
<FormControlLabel
control={
<Checkbox
......
import parse from 'date-fns/parse'
import {
getCookie,
deleteCookie,
......@@ -7,6 +8,8 @@ import {
isValidMobilePhoneNumber,
maybeCsrfToken,
submitJsonOpts,
isFemaleBasedOnNationalId,
extractBirthdateFromNationalId,
} from './index'
// Mock i18next module to return a translation that just returns the key
......@@ -117,3 +120,31 @@ test('Null fnr', async () => {
test('Invalid fnr', async () => {
expect(isValidFnr('')).toEqual('common:validation.invalidIdNumber')
})
test('Female extracted from fnr', async () => {
expect(isFemaleBasedOnNationalId('12103626631')).toEqual(true)
expect(isFemaleBasedOnNationalId('08015214474')).toEqual(true)
expect(isFemaleBasedOnNationalId('26052088029')).toEqual(true)
expect(isFemaleBasedOnNationalId('11082335449')).toEqual(true)
expect(isFemaleBasedOnNationalId('11081670619')).toEqual(true)
})
test('Male extracted from fnr', async () => {
expect(isFemaleBasedOnNationalId('12103626712')).toEqual(false)
expect(isFemaleBasedOnNationalId('08015214555')).toEqual(false)
expect(isFemaleBasedOnNationalId('01088538788')).toEqual(false)
expect(isFemaleBasedOnNationalId('15101739551')).toEqual(false)
expect(isFemaleBasedOnNationalId('05127648192')).toEqual(false)
})
test('Date of birth extract from D-number', async () => {
expect(extractBirthdateFromNationalId('53097248016')).toEqual(
parse('1972-09-13', 'yyyy-MM-dd', new Date())
)
})
test('Date of birth extracted from fødselsnummer', async () => {
expect(extractBirthdateFromNationalId('04062141242')).toEqual(
parse('1921-06-04', 'yyyy-MM-dd', new Date())
)
})
import validator from '@navikt/fnrvalidator'
import { parseISO } from 'date-fns'
import { getYear, parseISO } from 'date-fns'
import { OuData } from 'hooks/useOus'
import i18n from 'i18next'
import {
......@@ -11,6 +11,8 @@ import {
FetchedConsent,
} from 'interfaces'
import { isValidPhoneNumber, parsePhoneNumber } from 'libphonenumber-js'
import parse from 'date-fns/parse'
import { parseInt } from 'lodash'
const validEmailRegex =
/^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/
......@@ -147,6 +149,7 @@ export function parseRole(role: FetchedRole): Role {
end_date: parseISO(role.end_date),
max_days: role.max_days,
contact_person_unit: role.contact_person_unit,
comments: role.comments,
}
}
......@@ -220,3 +223,106 @@ export function getOuName(ou: OuData) {
}
return ou.nb ? ou.nb : ou.en
}
/**
* Note the the input is assumed to be either a D-number or a "fødselsnummer". Other types such as H-numbers are not supported.
*
* @param nationalId D-number or "fødselsnummer"
*/
function isDnr(nationalId: string): boolean {
return parseInt(nationalId.substring(0, 1), 10) >= 4
}
export function isFemaleBasedOnNationalId(nationalId: string): boolean {
if (isDnr(nationalId)) {
return parseInt(nationalId.charAt(10), 10) % 2 === 0
}
return parseInt(nationalId.charAt(8), 10) % 2 === 0
}
/**
* The ID-number format is scheduled to change in the future:
* https://www.skatteetaten.no/deling/opplysninger/folkeregisteropplysninger/pid/
*
* Do not attempt a suggestion if we are at the time when the format will change.
*/
function shouldAttemptSuggestion(): boolean {
const currentYear = getYear(new Date())
// Keeping it simple, if this code is still running and should be revised.
// The new numbers are scheduled to appear in 2032
return currentYear <= 2031
}
export function extractGenderOrBlank(nationalId?: string): string {
if (
nationalId == null ||
nationalId === '' ||
isValidFnr(nationalId) !== true ||
!shouldAttemptSuggestion()
) {
return ''
}
if (isFemaleBasedOnNationalId(nationalId)) {
return 'female'
}
return 'male'
}
/**
* Gives a guess of the birthdate with century included.
*
* @param dateOfBirth a date on the form ddMMyy
*/
function suggestBirthDate(dateOfBirth: string): Date {
const currentYear = getYear(new Date())
const year = dateOfBirth.substring(4, 6)
const yearAsInt = parseInt(year)
let century = '20'
// Check that the year the person is born is not a year in the future,
// given he is born in the 21th century. Also assuming he is born in
// the 21th century, check that he is older than 15 years, if not
// then assume he is born in the 20th century
if (
yearAsInt + 2000 > currentYear ||
!(currentYear - 2000 - yearAsInt > 15)
) {
century = '19'
}
return parse(
dateOfBirth.substring(0, 4) + century + dateOfBirth.substring(4, 6),
'ddMMyyyy',
new Date()
)
}
function extractBirthdateFromFnr(nationalId: string): Date {
return suggestBirthDate(nationalId.substring(0, 6))
}
function extractBirthdateFromDnumber(nationalId: string): Date {
return suggestBirthDate(
(parseInt(nationalId.charAt(0), 10) - 4).toString(10) +
nationalId.substring(1, 6)
)
}
export function extractBirthdateFromNationalId(
nationalId?: string
): Date | null {
if (
nationalId == null ||
nationalId === '' ||
isValidFnr(nationalId) !== true ||
!shouldAttemptSuggestion()
) {
return null
}
if (isDnr(nationalId)) {
return extractBirthdateFromDnumber(nationalId)
}
return extractBirthdateFromFnr(nationalId)
}
......@@ -78,6 +78,7 @@ class SpecialRoleSerializer(serializers.ModelSerializer):
"end_date",
"max_days",
"contact_person_unit",
"comments",
]
read_only_fields = [
"contact_person_unit",
......
......@@ -9,4 +9,4 @@ class SponsorSerializer(serializers.ModelSerializer):
class Meta:
model = Sponsor
fields = ["id", "feide_id", "first_name", "last_name", "orgunits"]
fields = ["id", "feide_id", "first_name", "last_name", "work_email", "orgunits"]