Skip to content
Snippets Groups Projects
Verified Commit 2abcd299 authored by Andreas Ellewsen's avatar Andreas Ellewsen
Browse files

Add guest info page

- Expanded output from guest info api endpoint
- Share guest info between both routes under /sponsor of the frontend.
- More localised text options added for use on the sponsor pages
- Details button on the sponsor overview page now points to the guest
  info page for each person.

Resolves: GREG-74
parent ba22c5a6
No related branches found
No related tags found
1 merge request!110Add guest info page
Pipeline #97066 passed
...@@ -21,6 +21,12 @@ ...@@ -21,6 +21,12 @@
"fullName": "Full name", "fullName": "Full name",
"mobilePhone": "Mobile phone" "mobilePhone": "Mobile phone"
}, },
"sponsor": {
"contactInfo": "Contact information",
"roleInfo": "Guest role- and period information",
"overviewGuest": "Guest overview"
},
"loading": "Loading...", "loading": "Loading...",
"termsHeader": "Terms", "termsHeader": "Terms",
"staging": "Staging", "staging": "Staging",
...@@ -69,7 +75,7 @@ ...@@ -69,7 +75,7 @@
"summaryPeriod": "Summary period", "summaryPeriod": "Summary period",
"contactInformation": "Contact information", "contactInformation": "Contact information",
"guestRole": "Guest role", "guestRole": "Guest role",
"guestPeriod":"Period", "guestPeriod": "Period",
"guestDepartment": "Department" "guestDepartment": "Department"
}, },
"yourGuests": "Your guests", "yourGuests": "Your guests",
......
...@@ -18,9 +18,14 @@ ...@@ -18,9 +18,14 @@
"roleEndDate": "Til", "roleEndDate": "Til",
"comment": "Kommentar", "comment": "Kommentar",
"email": "E-post", "email": "E-post",
"fullName": "Full navn", "fullName": "Fullt navn",
"mobilePhone": "Mobilnummer" "mobilePhone": "Mobilnummer"
}, },
"sponsor": {
"contactInfo": "Kontaktinformasjon",
"roleInfo": "Gjesterolle- og periodeinformasjon",
"overviewGuest": "Oversikt over gjest"
},
"loading": "Laster...", "loading": "Laster...",
"termsHeader": "Vilkår", "termsHeader": "Vilkår",
"staging": "Staging", "staging": "Staging",
......
...@@ -22,6 +22,11 @@ ...@@ -22,6 +22,11 @@
"fullName": "Fullt namn", "fullName": "Fullt namn",
"mobilePhone": "Mobilnummer" "mobilePhone": "Mobilnummer"
}, },
"sponsor": {
"contactInfo": "Kontaktinformasjon",
"roleInfo": "Gjesterolle- og periodeinformasjon",
"overviewGuest": "Oversikt over gjest"
},
"loading": "Lastar...", "loading": "Lastar...",
"termsHeader": "Vilkår", "termsHeader": "Vilkår",
"staging": "Staging", "staging": "Staging",
......
export type Guest = {
id: string
pid: string
name: string
email: string
mobile: string
fnr: string
role_nb: string
role_en: string
period: string
active: boolean
ou_nb: string
ou_en: string
}
export interface FetchedGuest {
id: string
pid: string
first: string
last: string
email: string
mobile: string
fnr: string
role_nb: string
role_en: string
period: string
ou_nb: string
ou_en: string
active: boolean
}
import { useEffect } from 'react' import { useEffect } from 'react'
import { Redirect, RouteComponentProps } from 'react-router-dom' import { Redirect, useParams } from 'react-router-dom'
type TParams = { id: string } type TParams = { id: string }
function InviteLink({ match }: RouteComponentProps<TParams>) { function InviteLink() {
// Fetch backend endpoint to preserve invite_id in backend session then redirect // Fetch backend endpoint to preserve invite_id in backend session then redirect
// to generic invite page with info about feide login or manual with passport. // to generic invite page with info about feide login or manual with passport.
const inviteId = match.params.id const { id } = useParams<TParams>()
useEffect(() => { useEffect(() => {
fetch(`/api/ui/v1/invited/${inviteId}`) fetch(`/api/ui/v1/invited/${id}`)
}, []) }, [])
return <Redirect to="/invite" /> return <Redirect to="/invite" />
} }
......
import React, { useEffect, useState } from 'react' import { useState } from 'react'
import { import {
Table, Table,
TableBody, TableBody,
...@@ -6,41 +6,24 @@ import { ...@@ -6,41 +6,24 @@ import {
TableContainer, TableContainer,
TableHead, TableHead,
TableRow, TableRow,
Paper, Accordion, AccordionSummary, AccordionDetails, Paper,
Accordion,
AccordionSummary,
AccordionDetails,
} from '@mui/material' } from '@mui/material'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import Page from 'components/page' import Page from 'components/page'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import { Guest } from 'interfaces'
import SponsorGuestButtons from '../../components/sponsorGuestButtons' import SponsorGuestButtons from '../../components/sponsorGuestButtons'
type PersonInfo = {
name: string
role_nb: string
role_en: string
period: string
active: boolean
ou_nb: string
ou_en: string
}
interface GuestProps { interface GuestProps {
persons: PersonInfo[] persons: Guest[]
} }
interface PersonLineProps { interface PersonLineProps {
person: PersonInfo person: Guest
}
interface FetchedPerson {
first: string
last: string
role_nb: string
role_en: string
period: string
ou_nb: string
ou_en: string
active: boolean
} }
const PersonLine = ({ person }: PersonLineProps) => { const PersonLine = ({ person }: PersonLineProps) => {
...@@ -51,18 +34,18 @@ const PersonLine = ({ person }: PersonLineProps) => { ...@@ -51,18 +34,18 @@ const PersonLine = ({ person }: PersonLineProps) => {
key={person.name} key={person.name}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
> >
<TableCell component='th' scope='row'> <TableCell component="th" scope="row">
{person.name} {person.name}
</TableCell> </TableCell>
<TableCell align='left'> <TableCell align="left">
{i18n.language === 'en' ? person.role_en : person.role_nb} {i18n.language === 'en' ? person.role_en : person.role_nb}
</TableCell> </TableCell>
<TableCell align='left'>{person.period}</TableCell> <TableCell align="left">{person.period}</TableCell>
<TableCell align='left'> <TableCell align="left">
{i18n.language === 'en' ? person.ou_en : person.ou_nb} {i18n.language === 'en' ? person.ou_en : person.ou_nb}
</TableCell> </TableCell>
<TableCell align='left'> <TableCell align="left">
<button type='button'>{t('common:details')}</button> <Link to={`/sponsor/guest/${person.pid}`}>{t('common:details')}</Link>
</TableCell> </TableCell>
</TableRow> </TableRow>
) )
...@@ -78,23 +61,26 @@ const ActiveGuests = ({ persons }: GuestProps) => { ...@@ -78,23 +61,26 @@ const ActiveGuests = ({ persons }: GuestProps) => {
} }
const [t] = useTranslation(['common']) const [t] = useTranslation(['common'])
return ( return (
<Accordion expanded={activeExpanded} onChange={() => { <Accordion
setActiveExpanded(!activeExpanded) expanded={activeExpanded}
}}> onChange={() => {
setActiveExpanded(!activeExpanded)
}}
>
<AccordionSummary expandIcon={<ExpandMoreIcon />}> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
<h4>{t('common:activeGuests')}</h4> <h4>{t('common:activeGuests')}</h4>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
<p>{t('common:activeGuestsDescription')}</p> <p>{t('common:activeGuestsDescription')}</p>
<TableContainer component={Paper}> <TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label='simple table'> <Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableHead sx={{ backgroundColor: 'primary.light' }}> <TableHead sx={{ backgroundColor: 'primary.light' }}>
<TableRow> <TableRow>
<TableCell>{t('common:name')}</TableCell> <TableCell>{t('common:name')}</TableCell>
<TableCell align='left'>{t('common:role')}</TableCell> <TableCell align="left">{t('common:role')}</TableCell>
<TableCell align='left'>{t('common:period')}</TableCell> <TableCell align="left">{t('common:period')}</TableCell>
<TableCell align='left'>{t('common:ou')}</TableCell> <TableCell align="left">{t('common:ou')}</TableCell>
<TableCell align='left'>{t('common:choice')}</TableCell> <TableCell align="left">{t('common:choice')}</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
...@@ -126,11 +112,13 @@ const WaitingGuests = ({ persons }: GuestProps) => { ...@@ -126,11 +112,13 @@ const WaitingGuests = ({ persons }: GuestProps) => {
const [t] = useTranslation(['common']) const [t] = useTranslation(['common'])
return ( return (
<Accordion expanded={waitingExpanded} onChange={() => { <Accordion
setWaitingExpanded(!waitingExpanded) expanded={waitingExpanded}
}} onChange={() => {
sx={{ border: 'none' }}> setWaitingExpanded(!waitingExpanded)
}}
sx={{ border: 'none' }}
>
<AccordionSummary expandIcon={<ExpandMoreIcon />}> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
<h4>{t('common:waitingGuests')}</h4> <h4>{t('common:waitingGuests')}</h4>
</AccordionSummary> </AccordionSummary>
...@@ -138,14 +126,14 @@ const WaitingGuests = ({ persons }: GuestProps) => { ...@@ -138,14 +126,14 @@ const WaitingGuests = ({ persons }: GuestProps) => {
<p>{t('common:waitingGuestsDescription')}</p> <p>{t('common:waitingGuestsDescription')}</p>
<TableContainer component={Paper}> <TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label='simple table'> <Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableHead sx={{ backgroundColor: 'primary.light' }}> <TableHead sx={{ backgroundColor: 'primary.light' }}>
<TableRow> <TableRow>
<TableCell>{t('common:name')}</TableCell> <TableCell>{t('common:name')}</TableCell>
<TableCell align='left'>{t('common:role')}</TableCell> <TableCell align="left">{t('common:role')}</TableCell>
<TableCell align='left'>{t('common:period')}</TableCell> <TableCell align="left">{t('common:period')}</TableCell>
<TableCell align='left'>{t('common:ou')}</TableCell> <TableCell align="left">{t('common:ou')}</TableCell>
<TableCell align='left'>{t('common:choice')}</TableCell> <TableCell align="left">{t('common:choice')}</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
...@@ -165,36 +153,16 @@ const WaitingGuests = ({ persons }: GuestProps) => { ...@@ -165,36 +153,16 @@ const WaitingGuests = ({ persons }: GuestProps) => {
) )
} }
function FrontPage() { interface FrontPageProps {
const [persons, setPersons] = useState<Array<PersonInfo>>([]) guests: Guest[]
}
const fetchGuestsInfo = async () => {
const response = await fetch('/api/ui/v1/guests/?format=json')
const jsonResponse = await response.json()
if (response.ok) {
const roles = await jsonResponse.roles
const guests: PersonInfo[] = roles.map((person: FetchedPerson) => ({
name: `${person.first} ${person.last}`,
role_nb: person.role_nb,
role_en: person.role_en,
period: person.period,
active: person.active,
ou_nb: person.ou_nb,
ou_en: person.ou_en,
}))
setPersons(guests)
}
}
useEffect(() => {
fetchGuestsInfo()
}, [])
function FrontPage({ guests }: FrontPageProps) {
return ( return (
<Page> <Page>
<SponsorGuestButtons yourGuestsActive /> <SponsorGuestButtons yourGuestsActive />
<WaitingGuests persons={persons} /> <WaitingGuests persons={guests} />
<ActiveGuests persons={persons} /> <ActiveGuests persons={guests} />
</Page> </Page>
) )
} }
......
import React from 'react' import React from 'react'
import { useParams } from 'react-router-dom' import { Link, useParams } from 'react-router-dom'
import Page from 'components/page' import Page from 'components/page'
import { useTranslation } from 'react-i18next'
import {
Box,
IconButton,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Theme,
Paper,
} from '@mui/material'
import PersonOutlineRoundedIcon from '@mui/icons-material/PersonOutlineRounded'
import ArrowBackIcon from '@mui/icons-material/ArrowBack'
import { Guest } from 'interfaces'
type GuestInfoParams = { type GuestInfoParams = {
id: string pid: string
} }
interface RoleLineProps {
guest: Guest
}
const RoleLine = ({ guest }: RoleLineProps) => {
const [, i18n] = useTranslation('common')
return (
<TableRow
key={guest.id}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
>
<TableCell align='left'>
{i18n.language === 'en' ? guest.role_en : guest.role_nb}
</TableCell>
<TableCell component='th' scope='row'>
{guest.period}
</TableCell>
<TableCell align='left'>
{i18n.language === 'en' ? guest.ou_en : guest.ou_nb}
</TableCell>
</TableRow>
)
}
interface GuestInfoProps {
guests: Guest[]
}
export default function GuestInfo({ guests }: GuestInfoProps) {
const { pid } = useParams<GuestInfoParams>()
const [t] = useTranslation(['common'])
export default function GuestInfo() { const roles = guests.filter((guest) => guest.pid.toString() === pid)
const { id } = useParams<GuestInfoParams>() const guestInfo = roles[0]
return ( return (
<Page header="Sponsor info page"> <Page>
<p>Display info of guest with id: {id}</p> <Box
sx={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
marginBottom: '2rem',
}}
>
<Box>
<IconButton component={Link} to='/sponsor'>
<ArrowBackIcon />
</IconButton>
</Box>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<PersonOutlineRoundedIcon
fontSize='large'
sx={{
borderRadius: '2rem',
borderStyle: 'solid',
borderColor: (theme: Theme) => theme.palette.primary.main,
fill: 'white',
backgroundColor: (theme: Theme) => theme.palette.primary.main,
}}
/>
<Box
sx={{
typography: 'caption',
}}
>
{t('sponsor.overviewGuest')}
</Box>
</Box>
</Box>
<h4>{t('sponsor.contactInfo')}</h4>
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label='simple table'>
<TableHead sx={{ backgroundColor: 'primary.light' }}>
<TableRow>
<TableCell align='left'>{t('sponsor.contactInfo')}</TableCell>
<TableCell />
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell align='left'>{t('input.fullName')}</TableCell>
<TableCell align='left'>{guestInfo.name}</TableCell>
</TableRow>
<TableRow>
<TableCell align='left'>{t('input.email')}</TableCell>
<TableCell align='left'>{guestInfo.email}</TableCell>
</TableRow>
<TableRow>
<TableCell align='left'>{t('input.nationalIdNumber')}</TableCell>
<TableCell align='left'>{guestInfo.fnr}</TableCell>
</TableRow>
<TableRow>
<TableCell align='left'>{t('input.mobilePhone')}</TableCell>
<TableCell align='left'>{guestInfo.mobile}</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
<h4>{t('sponsor.roleInfo')}</h4>
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label='simple table'>
<TableHead sx={{ backgroundColor: 'primary.light' }}>
<TableRow>
<TableCell align='left'>{t('common:role')}</TableCell>
<TableCell align='left'>{t('common:period')}</TableCell>
<TableCell align='left'>{t('common:ou')}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{roles.map((guest) => (
<RoleLine guest={guest} />
))}
</TableBody>
</Table>
</TableContainer>
</Page> </Page>
) )
} }
import React from 'react' import React, { useEffect, useState } from 'react'
import { Route } from 'react-router-dom' import { Route } from 'react-router-dom'
import FrontPage from 'routes/sponsor/frontpage' import FrontPage from 'routes/sponsor/frontpage'
import GuestInfo from 'routes/sponsor/guestInfo' import GuestInfo from 'routes/sponsor/guestInfo'
import { FetchedGuest, Guest } from 'interfaces'
function Sponsor() { function Sponsor() {
const [guests, setGuests] = useState<Guest[]>([])
const getGuestsInfo = async () => {
try {
const response = await fetch('/api/ui/v1/guests/?format=json')
const jsonResponse = await response.json()
if (response.ok) {
const roles = await jsonResponse.roles
setGuests(
roles.map((person: FetchedGuest) => ({
id: person.id,
pid: person.pid,
name: `${person.first} ${person.last}`,
email: person.email,
mobile: person.mobile,
fnr: person.fnr,
role_nb: person.role_nb,
role_en: person.role_en,
period: person.period,
active: person.active,
ou_nb: person.ou_nb,
ou_en: person.ou_en,
}))
)
} else {
setGuests([])
}
} catch (error) {
setGuests([])
}
}
useEffect(() => {
getGuestsInfo()
}, [])
return ( return (
<> <>
<Route path="/sponsor/guest/:id"> <Route path="/sponsor/guest/:pid">
<GuestInfo /> <GuestInfo guests={guests} />
</Route> </Route>
<Route exact path="/sponsor"> <Route exact path="/sponsor">
<FrontPage /> <FrontPage guests={guests} />
</Route> </Route>
</> </>
) )
......
...@@ -76,6 +76,13 @@ class Person(BaseModel): ...@@ -76,6 +76,13 @@ class Person(BaseModel):
type=Identity.IdentityType.PRIVATE_MOBILE_NUMBER type=Identity.IdentityType.PRIVATE_MOBILE_NUMBER
).first() ).first()
@property
def fnr(self) -> Optional["Identity"]:
"""The person's fnr if they have one registered."""
return self.identities.filter(
type=Identity.IdentityType.NORWEGIAN_NATIONAL_ID_NUMBER
).first()
@property @property
def is_registered(self) -> bool: def is_registered(self) -> bool:
""" """
......
...@@ -41,6 +41,8 @@ SESSION_COOKIE_SAMESITE = "Lax" ...@@ -41,6 +41,8 @@ SESSION_COOKIE_SAMESITE = "Lax"
# CSRF_COOKIE_HTTPONLY = True # CSRF_COOKIE_HTTPONLY = True
# SESSION_COOKIE_HTTPONLY = True # SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_AGE = 1209600 # two weeks for easy development
try: try:
from .local import * from .local import *
except ImportError: except ImportError:
......
...@@ -57,8 +57,9 @@ class UserInfoView(APIView): ...@@ -57,8 +57,9 @@ class UserInfoView(APIView):
{ {
"first_name": person.first_name, "first_name": person.first_name,
"last_name": person.last_name, "last_name": person.last_name,
"email": person.email, "email": person.private_email and person.private_email.value,
"mobile_phone": person.mobile_phone, "mobile_phone": person.private_mobile
and person.private_mobile.value,
} }
) )
roles = person.roles roles = person.roles
...@@ -90,24 +91,18 @@ class UserInfoView(APIView): ...@@ -90,24 +91,18 @@ class UserInfoView(APIView):
link = InvitationLink.objects.get(uuid=invite_id) link = InvitationLink.objects.get(uuid=invite_id)
invitation = link.invitation invitation = link.invitation
person = invitation.role.person person = invitation.role.person
roles = person.roles passports = person.identities.filter(
try: type=Identity.IdentityType.PASSPORT_NUMBER
fnr = person.identities.get(type="norwegian_national_id_number").value ).first()
except Identity.DoesNotExist:
fnr = None
try:
passport = person.identities.get(type="passport_number").value
except Identity.DoesNotExist:
passport = None
content = { content = {
"feide_id": None, "feide_id": None,
"first_name": person.first_name, "first_name": person.first_name,
"last_name": person.last_name, "last_name": person.last_name,
"email": person.email, "email": person.private_email and person.private_email.value,
"mobile_phone": person.mobile_phone, "mobile_phone": person.private_mobile and person.private_mobile.value,
"fnr": fnr, "fnr": person.fnr and person.fnr.value,
"passport": passport, "passport": passports and passports.value,
"roles": [ "roles": [
{ {
"ou_name_nb": role.orgunit_id.name_nb, "ou_name_nb": role.orgunit_id.name_nb,
...@@ -122,7 +117,7 @@ class UserInfoView(APIView): ...@@ -122,7 +117,7 @@ class UserInfoView(APIView):
"last_name": role.sponsor_id.last_name, "last_name": role.sponsor_id.last_name,
}, },
} }
for role in roles.all() for role in person.roles.all()
], ],
} }
......
...@@ -96,8 +96,16 @@ class GuestInfoView(APIView): ...@@ -96,8 +96,16 @@ class GuestInfoView(APIView):
{ {
"roles": [ "roles": [
{ {
"id": i.id,
"pid": i.person.id,
"first": i.person.first_name, "first": i.person.first_name,
"last": i.person.last_name, "last": i.person.last_name,
"email": i.person.private_email
and i.person.private_email.value,
"mobile": i.person.private_mobile
and i.person.private_mobile.value,
"fnr": i.person.fnr
and "".join((i.person.fnr.value[:-5], "*****")),
"role_nb": i.type.name_nb, "role_nb": i.type.name_nb,
"role_en": i.type.name_en, "role_en": i.type.name_en,
"period": f"{i.start_date} - {i.end_date}", "period": f"{i.start_date} - {i.end_date}",
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment