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