From 2abcd299fcce4dfba13ab767138193782fd0e830 Mon Sep 17 00:00:00 2001 From: Andreas Ellewsen <ae@uio.no> Date: Thu, 14 Oct 2021 13:14:09 +0200 Subject: [PATCH] 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 --- frontend/public/locales/en/common.json | 8 +- frontend/public/locales/nb/common.json | 7 +- frontend/public/locales/nn/common.json | 5 + frontend/src/interfaces/index.ts | 30 ++++ frontend/src/routes/invitelink/index.tsx | 8 +- .../src/routes/sponsor/frontpage/index.tsx | 120 ++++++--------- .../src/routes/sponsor/guestInfo/index.tsx | 143 +++++++++++++++++- frontend/src/routes/sponsor/index.tsx | 45 +++++- greg/models.py | 7 + gregsite/settings/dev.py | 2 + gregui/api/views/userinfo.py | 27 ++-- gregui/views.py | 8 + 12 files changed, 302 insertions(+), 108 deletions(-) create mode 100644 frontend/src/interfaces/index.ts diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json index ef1df5b5..b90a20a2 100644 --- a/frontend/public/locales/en/common.json +++ b/frontend/public/locales/en/common.json @@ -21,6 +21,12 @@ "fullName": "Full name", "mobilePhone": "Mobile phone" }, + "sponsor": { + "contactInfo": "Contact information", + "roleInfo": "Guest role- and period information", + "overviewGuest": "Guest overview" + }, + "loading": "Loading...", "termsHeader": "Terms", "staging": "Staging", @@ -69,7 +75,7 @@ "summaryPeriod": "Summary period", "contactInformation": "Contact information", "guestRole": "Guest role", - "guestPeriod":"Period", + "guestPeriod": "Period", "guestDepartment": "Department" }, "yourGuests": "Your guests", diff --git a/frontend/public/locales/nb/common.json b/frontend/public/locales/nb/common.json index 76c1fc23..a03c35a9 100644 --- a/frontend/public/locales/nb/common.json +++ b/frontend/public/locales/nb/common.json @@ -18,9 +18,14 @@ "roleEndDate": "Til", "comment": "Kommentar", "email": "E-post", - "fullName": "Full navn", + "fullName": "Fullt navn", "mobilePhone": "Mobilnummer" }, + "sponsor": { + "contactInfo": "Kontaktinformasjon", + "roleInfo": "Gjesterolle- og periodeinformasjon", + "overviewGuest": "Oversikt over gjest" + }, "loading": "Laster...", "termsHeader": "Vilkår", "staging": "Staging", diff --git a/frontend/public/locales/nn/common.json b/frontend/public/locales/nn/common.json index 13c58859..8ec2986b 100644 --- a/frontend/public/locales/nn/common.json +++ b/frontend/public/locales/nn/common.json @@ -22,6 +22,11 @@ "fullName": "Fullt namn", "mobilePhone": "Mobilnummer" }, + "sponsor": { + "contactInfo": "Kontaktinformasjon", + "roleInfo": "Gjesterolle- og periodeinformasjon", + "overviewGuest": "Oversikt over gjest" + }, "loading": "Lastar...", "termsHeader": "Vilkår", "staging": "Staging", diff --git a/frontend/src/interfaces/index.ts b/frontend/src/interfaces/index.ts new file mode 100644 index 00000000..5eed9cc3 --- /dev/null +++ b/frontend/src/interfaces/index.ts @@ -0,0 +1,30 @@ +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 +} diff --git a/frontend/src/routes/invitelink/index.tsx b/frontend/src/routes/invitelink/index.tsx index 2a34ff22..32dc16d0 100644 --- a/frontend/src/routes/invitelink/index.tsx +++ b/frontend/src/routes/invitelink/index.tsx @@ -1,16 +1,16 @@ import { useEffect } from 'react' -import { Redirect, RouteComponentProps } from 'react-router-dom' +import { Redirect, useParams } from 'react-router-dom' type TParams = { id: string } -function InviteLink({ match }: RouteComponentProps<TParams>) { +function InviteLink() { // 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. - const inviteId = match.params.id + const { id } = useParams<TParams>() useEffect(() => { - fetch(`/api/ui/v1/invited/${inviteId}`) + fetch(`/api/ui/v1/invited/${id}`) }, []) return <Redirect to="/invite" /> } diff --git a/frontend/src/routes/sponsor/frontpage/index.tsx b/frontend/src/routes/sponsor/frontpage/index.tsx index f7e6aa7f..5a9c0ef2 100644 --- a/frontend/src/routes/sponsor/frontpage/index.tsx +++ b/frontend/src/routes/sponsor/frontpage/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react' +import { useState } from 'react' import { Table, TableBody, @@ -6,41 +6,24 @@ import { TableContainer, TableHead, TableRow, - Paper, Accordion, AccordionSummary, AccordionDetails, + Paper, + Accordion, + AccordionSummary, + AccordionDetails, } from '@mui/material' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import Page from 'components/page' import { useTranslation } from 'react-i18next' +import { Link } from 'react-router-dom' +import { Guest } from 'interfaces' 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 { - persons: PersonInfo[] + persons: Guest[] } - interface PersonLineProps { - person: PersonInfo -} - -interface FetchedPerson { - first: string - last: string - role_nb: string - role_en: string - period: string - ou_nb: string - ou_en: string - active: boolean + person: Guest } const PersonLine = ({ person }: PersonLineProps) => { @@ -51,18 +34,18 @@ const PersonLine = ({ person }: PersonLineProps) => { key={person.name} sx={{ '&:last-child td, &:last-child th': { border: 0 } }} > - <TableCell component='th' scope='row'> + <TableCell component="th" scope="row"> {person.name} </TableCell> - <TableCell align='left'> + <TableCell align="left"> {i18n.language === 'en' ? person.role_en : person.role_nb} </TableCell> - <TableCell align='left'>{person.period}</TableCell> - <TableCell align='left'> + <TableCell align="left">{person.period}</TableCell> + <TableCell align="left"> {i18n.language === 'en' ? person.ou_en : person.ou_nb} </TableCell> - <TableCell align='left'> - <button type='button'>{t('common:details')}</button> + <TableCell align="left"> + <Link to={`/sponsor/guest/${person.pid}`}>{t('common:details')}</Link> </TableCell> </TableRow> ) @@ -78,23 +61,26 @@ const ActiveGuests = ({ persons }: GuestProps) => { } const [t] = useTranslation(['common']) return ( - <Accordion expanded={activeExpanded} onChange={() => { - setActiveExpanded(!activeExpanded) - }}> + <Accordion + expanded={activeExpanded} + onChange={() => { + setActiveExpanded(!activeExpanded) + }} + > <AccordionSummary expandIcon={<ExpandMoreIcon />}> <h4>{t('common:activeGuests')}</h4> </AccordionSummary> <AccordionDetails> <p>{t('common:activeGuestsDescription')}</p> <TableContainer component={Paper}> - <Table sx={{ minWidth: 650 }} aria-label='simple table'> + <Table sx={{ minWidth: 650 }} aria-label="simple table"> <TableHead sx={{ backgroundColor: 'primary.light' }}> <TableRow> <TableCell>{t('common:name')}</TableCell> - <TableCell align='left'>{t('common:role')}</TableCell> - <TableCell align='left'>{t('common:period')}</TableCell> - <TableCell align='left'>{t('common:ou')}</TableCell> - <TableCell align='left'>{t('common:choice')}</TableCell> + <TableCell align="left">{t('common:role')}</TableCell> + <TableCell align="left">{t('common:period')}</TableCell> + <TableCell align="left">{t('common:ou')}</TableCell> + <TableCell align="left">{t('common:choice')}</TableCell> </TableRow> </TableHead> <TableBody> @@ -126,11 +112,13 @@ const WaitingGuests = ({ persons }: GuestProps) => { const [t] = useTranslation(['common']) return ( - <Accordion expanded={waitingExpanded} onChange={() => { - setWaitingExpanded(!waitingExpanded) - }} - sx={{ border: 'none' }}> - + <Accordion + expanded={waitingExpanded} + onChange={() => { + setWaitingExpanded(!waitingExpanded) + }} + sx={{ border: 'none' }} + > <AccordionSummary expandIcon={<ExpandMoreIcon />}> <h4>{t('common:waitingGuests')}</h4> </AccordionSummary> @@ -138,14 +126,14 @@ const WaitingGuests = ({ persons }: GuestProps) => { <p>{t('common:waitingGuestsDescription')}</p> <TableContainer component={Paper}> - <Table sx={{ minWidth: 650 }} aria-label='simple table'> + <Table sx={{ minWidth: 650 }} aria-label="simple table"> <TableHead sx={{ backgroundColor: 'primary.light' }}> <TableRow> <TableCell>{t('common:name')}</TableCell> - <TableCell align='left'>{t('common:role')}</TableCell> - <TableCell align='left'>{t('common:period')}</TableCell> - <TableCell align='left'>{t('common:ou')}</TableCell> - <TableCell align='left'>{t('common:choice')}</TableCell> + <TableCell align="left">{t('common:role')}</TableCell> + <TableCell align="left">{t('common:period')}</TableCell> + <TableCell align="left">{t('common:ou')}</TableCell> + <TableCell align="left">{t('common:choice')}</TableCell> </TableRow> </TableHead> <TableBody> @@ -165,36 +153,16 @@ const WaitingGuests = ({ persons }: GuestProps) => { ) } -function FrontPage() { - const [persons, setPersons] = useState<Array<PersonInfo>>([]) - - 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() - }, []) +interface FrontPageProps { + guests: Guest[] +} +function FrontPage({ guests }: FrontPageProps) { return ( <Page> <SponsorGuestButtons yourGuestsActive /> - <WaitingGuests persons={persons} /> - <ActiveGuests persons={persons} /> + <WaitingGuests persons={guests} /> + <ActiveGuests persons={guests} /> </Page> ) } diff --git a/frontend/src/routes/sponsor/guestInfo/index.tsx b/frontend/src/routes/sponsor/guestInfo/index.tsx index bb652151..4758303a 100644 --- a/frontend/src/routes/sponsor/guestInfo/index.tsx +++ b/frontend/src/routes/sponsor/guestInfo/index.tsx @@ -1,18 +1,149 @@ import React from 'react' -import { useParams } from 'react-router-dom' +import { Link, useParams } from 'react-router-dom' 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 = { - 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 { id } = useParams<GuestInfoParams>() + const roles = guests.filter((guest) => guest.pid.toString() === pid) + const guestInfo = roles[0] return ( - <Page header="Sponsor info page"> - <p>Display info of guest with id: {id}</p> + <Page> + <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> ) } diff --git a/frontend/src/routes/sponsor/index.tsx b/frontend/src/routes/sponsor/index.tsx index b3cdb0aa..ecfd155d 100644 --- a/frontend/src/routes/sponsor/index.tsx +++ b/frontend/src/routes/sponsor/index.tsx @@ -1,17 +1,54 @@ -import React from 'react' +import React, { useEffect, useState } from 'react' import { Route } from 'react-router-dom' import FrontPage from 'routes/sponsor/frontpage' import GuestInfo from 'routes/sponsor/guestInfo' +import { FetchedGuest, Guest } from 'interfaces' 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 ( <> - <Route path="/sponsor/guest/:id"> - <GuestInfo /> + <Route path="/sponsor/guest/:pid"> + <GuestInfo guests={guests} /> </Route> <Route exact path="/sponsor"> - <FrontPage /> + <FrontPage guests={guests} /> </Route> </> ) diff --git a/greg/models.py b/greg/models.py index 430a80be..98d101fe 100644 --- a/greg/models.py +++ b/greg/models.py @@ -76,6 +76,13 @@ class Person(BaseModel): type=Identity.IdentityType.PRIVATE_MOBILE_NUMBER ).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 def is_registered(self) -> bool: """ diff --git a/gregsite/settings/dev.py b/gregsite/settings/dev.py index 0305521f..66a3dbf2 100644 --- a/gregsite/settings/dev.py +++ b/gregsite/settings/dev.py @@ -41,6 +41,8 @@ SESSION_COOKIE_SAMESITE = "Lax" # CSRF_COOKIE_HTTPONLY = True # SESSION_COOKIE_HTTPONLY = True +SESSION_COOKIE_AGE = 1209600 # two weeks for easy development + try: from .local import * except ImportError: diff --git a/gregui/api/views/userinfo.py b/gregui/api/views/userinfo.py index aef0b932..3a4a0d41 100644 --- a/gregui/api/views/userinfo.py +++ b/gregui/api/views/userinfo.py @@ -57,8 +57,9 @@ class UserInfoView(APIView): { "first_name": person.first_name, "last_name": person.last_name, - "email": person.email, - "mobile_phone": person.mobile_phone, + "email": person.private_email and person.private_email.value, + "mobile_phone": person.private_mobile + and person.private_mobile.value, } ) roles = person.roles @@ -90,24 +91,18 @@ class UserInfoView(APIView): link = InvitationLink.objects.get(uuid=invite_id) invitation = link.invitation person = invitation.role.person - roles = person.roles - try: - fnr = person.identities.get(type="norwegian_national_id_number").value - except Identity.DoesNotExist: - fnr = None - try: - passport = person.identities.get(type="passport_number").value - except Identity.DoesNotExist: - passport = None + passports = person.identities.filter( + type=Identity.IdentityType.PASSPORT_NUMBER + ).first() content = { "feide_id": None, "first_name": person.first_name, "last_name": person.last_name, - "email": person.email, - "mobile_phone": person.mobile_phone, - "fnr": fnr, - "passport": passport, + "email": person.private_email and person.private_email.value, + "mobile_phone": person.private_mobile and person.private_mobile.value, + "fnr": person.fnr and person.fnr.value, + "passport": passports and passports.value, "roles": [ { "ou_name_nb": role.orgunit_id.name_nb, @@ -122,7 +117,7 @@ class UserInfoView(APIView): "last_name": role.sponsor_id.last_name, }, } - for role in roles.all() + for role in person.roles.all() ], } diff --git a/gregui/views.py b/gregui/views.py index 2bf3fc9d..a16ac09b 100644 --- a/gregui/views.py +++ b/gregui/views.py @@ -96,8 +96,16 @@ class GuestInfoView(APIView): { "roles": [ { + "id": i.id, + "pid": i.person.id, "first": i.person.first_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_en": i.type.name_en, "period": f"{i.start_date} - {i.end_date}", -- GitLab