From 701afd23e0b9df1d0acad8b7a166e272aa402b4d Mon Sep 17 00:00:00 2001
From: Andreas Ellewsen <ae@uio.no>
Date: Wed, 27 Oct 2021 10:51:03 +0200
Subject: [PATCH] Add role adding page to guest profiles

Code for guest routes moved to their own folder so that we don't need
multiple api calls.
Serializer class for roles has been simplified with methods for
validation of each field and a separate one used in invites without
requiring the person field.

Resolves: GREG-61
---
 frontend/public/locales/en/common.json        |  17 +-
 frontend/public/locales/nb/common.json        |  17 +-
 frontend/public/locales/nn/common.json        |  17 +-
 frontend/src/hooks/useRoleTypes/index.tsx     |   1 +
 .../routes/components/sponsorInfoButtons.tsx  |  10 +-
 .../sponsor/{ => guest}/guestInfo/index.tsx   |  85 ++----
 .../{ => guest}/guestRoleInfo/index.tsx       |  26 +-
 frontend/src/routes/sponsor/guest/index.tsx   |  73 +++++
 .../sponsor/guest/newGuestRole/index.tsx      | 274 ++++++++++++++++++
 frontend/src/routes/sponsor/index.tsx         |  12 +-
 gregui/api/serializers/invitation.py          |   4 +-
 gregui/api/serializers/role.py                | 111 +++++--
 gregui/api/serializers/roletype.py            |   2 +-
 gregui/api/urls.py                            |   2 +
 gregui/api/views/person.py                    |   3 +
 gregui/api/views/role.py                      |  34 ++-
 gregui/tests/api/test_invite_guest.py         |   9 +-
 gregui/urls.py                                |   2 -
 18 files changed, 559 insertions(+), 140 deletions(-)
 rename frontend/src/routes/sponsor/{ => guest}/guestInfo/index.tsx (62%)
 rename frontend/src/routes/sponsor/{ => guest}/guestRoleInfo/index.tsx (90%)
 create mode 100644 frontend/src/routes/sponsor/guest/index.tsx
 create mode 100644 frontend/src/routes/sponsor/guest/newGuestRole/index.tsx

diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json
index 5ff11a78..ad3e60bf 100644
--- a/frontend/public/locales/en/common.json
+++ b/frontend/public/locales/en/common.json
@@ -17,6 +17,8 @@
     "roleStartDate": "From",
     "roleEndDate": "To",
     "comment": "Comment",
+    "contact": "Contact person",
+    "searchable": "Available in search?",
     "email": "E-mail",
     "fullName": "Full name",
     "mobilePhone": "Mobile phone",
@@ -25,14 +27,21 @@
     "countryCallingCode": "Country code"
   },
   "sponsor": {
-    "contactInfo": "Contact information",
-    "roleInfo": "Guest role- and period information",
+    "addRole": "Add role",
     "roleInfoText": "You can change the start and end dates for the role.",
     "choose": "Choose",
     "details": "Details",
     "modifyEnd": "Change end date",
-    "endNow": "End role",
-    "overviewGuest": "Guest overview"
+    "endNow": "End role"
+  },
+  "guestInfo": {
+    "contactInfo": "Contact information",
+    "roleInfoHead": "Roles and periods",
+    "roleInfoBody": "You can only change roles that you have given"
+  },
+  "guest": {
+    "headerText": "Add new role and period.",
+    "bodyText": "Here you can add a new role to the same guest"
   },
   "register": {
     "registerHeading": "Register new guest",
diff --git a/frontend/public/locales/nb/common.json b/frontend/public/locales/nb/common.json
index 4b2b4ae2..41828b97 100644
--- a/frontend/public/locales/nb/common.json
+++ b/frontend/public/locales/nb/common.json
@@ -17,6 +17,8 @@
     "roleStartDate": "Fra",
     "roleEndDate": "Til",
     "comment": "Kommentar",
+    "contact": "Kontaktperson",
+    "searchable": "Synlig i søk?",
     "email": "E-post",
     "fullName": "Fullt navn",
     "mobilePhone": "Mobilnummer",
@@ -25,14 +27,21 @@
     "countryCallingCode": "Landkode"
   },
   "sponsor": {
-    "contactInfo": "Kontaktinformasjon",
-    "roleInfo": "Gjesterolle- og periodeinformasjon",
+    "addRole": "Legg til rolle",
     "roleInfoText": "Her kan du endre på start- og sluttdato for gjesterollen eller avslutte perioden",
     "choose": "Velg",
     "details": "Detaljer",
     "modifyEnd": "Endre sluttdato",
-    "endNow": "Avslutt rolle",
-    "overviewGuest": "Oversikt over gjest"
+    "endNow": "Avslutt rolle"
+  },
+  "guestInfo": {
+    "contactInfo": "Kontaktinformasjon",
+    "roleInfoHead": "Roller og perioder",
+    "roleInfoBody": "Du kan bare endre på gjesteroller som du er vert for"
+  },
+  "guest": {
+    "headerText": "Legg til ny rolle og periode",
+    "bodyText": "Her kan du legge til en ny rolle på samme gjest"
   },
   "register": {
     "registerHeading": "Registrer ny gjest",
diff --git a/frontend/public/locales/nn/common.json b/frontend/public/locales/nn/common.json
index 23ff342f..af1b8aac 100644
--- a/frontend/public/locales/nn/common.json
+++ b/frontend/public/locales/nn/common.json
@@ -18,6 +18,8 @@
     "roleStartDate": "Frå",
     "roleEndDate": "Til",
     "comment": "Kommentar",
+    "contact": "Kontaktperson",
+    "searchable": "Synleg i søk?",
     "email": "E-post",
     "fullName": "Fullt namn",
     "mobilePhone": "Mobilnummer",
@@ -26,14 +28,21 @@
     "countryCallingCode": "Landkode"
   },
   "sponsor": {
-    "contactInfo": "Kontaktinformasjon",
-    "roleInfo": "Gjesterolle- og periodeinformasjon",
+    "addRole": "Legg til role",
     "roleInfoText": "Her kan du endre på start- og sluttdato for gjesterollen eller avslutte perioden",
     "choose": "Velg",
     "details": "Detaljer",
     "modifyEnd": "Endre sluttdato",
-    "endNow": "Avslutt rolle",
-    "overviewGuest": "Oversikt over gjest"
+    "endNow": "Avslutt rolle"
+  },
+  "guestInfo": {
+    "contactInfo": "Kontaktinformasjon",
+    "roleInfoHead": "Roller og perioder",
+    "roleInfoBody": "Du kan bare endre på gjesteroller som du er vert for"
+  },
+  "guest": {
+    "headerText": "Legg til ny rolle og periode",
+    "bodyText": "Her kan du legge til en ny rolle på samme gjest"
   },
   "register": {
     "registerHeading": "Registrer ny gjest",
diff --git a/frontend/src/hooks/useRoleTypes/index.tsx b/frontend/src/hooks/useRoleTypes/index.tsx
index 7e915dec..0a61d013 100644
--- a/frontend/src/hooks/useRoleTypes/index.tsx
+++ b/frontend/src/hooks/useRoleTypes/index.tsx
@@ -5,6 +5,7 @@ type RoleTypeData = {
   identifier: string
   name_en: string
   name_nb: string
+  max_days: number
 }
 
 function useRoleTypes(): RoleTypeData[] {
diff --git a/frontend/src/routes/components/sponsorInfoButtons.tsx b/frontend/src/routes/components/sponsorInfoButtons.tsx
index 65c1d5e8..ce50eb18 100644
--- a/frontend/src/routes/components/sponsorInfoButtons.tsx
+++ b/frontend/src/routes/components/sponsorInfoButtons.tsx
@@ -2,14 +2,16 @@ import { IconButton, Theme, Box } from '@mui/material'
 import { Link } from 'react-router-dom'
 import PersonOutlineRoundedIcon from '@mui/icons-material/PersonOutlineRounded'
 import ArrowBackIcon from '@mui/icons-material/ArrowBack'
-import { useTranslation } from 'react-i18next'
 
 interface SponsorInfoButtonsProps {
   to: string
+  name: string
 }
 
-export default function SponsorInfoButtons({ to }: SponsorInfoButtonsProps) {
-  const { t } = useTranslation(['common'])
+export default function SponsorInfoButtons({
+  to,
+  name,
+}: SponsorInfoButtonsProps) {
   return (
     <Box
       sx={{
@@ -46,7 +48,7 @@ export default function SponsorInfoButtons({ to }: SponsorInfoButtonsProps) {
             typography: 'caption',
           }}
         >
-          {t('sponsor.overviewGuest')}
+          {name}
         </Box>
       </Box>
     </Box>
diff --git a/frontend/src/routes/sponsor/guestInfo/index.tsx b/frontend/src/routes/sponsor/guest/guestInfo/index.tsx
similarity index 62%
rename from frontend/src/routes/sponsor/guestInfo/index.tsx
rename to frontend/src/routes/sponsor/guest/guestInfo/index.tsx
index ea8dad32..139e1284 100644
--- a/frontend/src/routes/sponsor/guestInfo/index.tsx
+++ b/frontend/src/routes/sponsor/guest/guestInfo/index.tsx
@@ -1,4 +1,3 @@
-import React, { useEffect, useState } from 'react'
 import { Link, useParams } from 'react-router-dom'
 
 import Page from 'components/page'
@@ -13,10 +12,9 @@ import {
   TableRow,
   Paper,
 } from '@mui/material'
-import { Guest, Role, FetchedRole } from 'interfaces'
+import { Guest, Role } from 'interfaces'
 import SponsorInfoButtons from 'routes/components/sponsorInfoButtons'
 import { format } from 'date-fns'
-import { parseRole } from 'utils'
 
 type GuestInfoParams = {
   pid: string
@@ -25,14 +23,15 @@ interface RoleLineProps {
   role: Role
   pid: string
 }
+type GuestInfoProps = {
+  guest: Guest
+  roles: Role[]
+}
 
 const RoleLine = ({ role, pid }: RoleLineProps) => {
   const [t, i18n] = useTranslation('common')
   return (
-    <TableRow
-      key={role.id}
-      sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
-    >
+    <TableRow sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
       <TableCell align="left">
         {i18n.language === 'en' ? role.name_en : role.name_nb}
       </TableCell>
@@ -56,60 +55,19 @@ const RoleLine = ({ role, pid }: RoleLineProps) => {
   )
 }
 
-export default function GuestInfo() {
+export default function GuestInfo({ guest, roles }: GuestInfoProps) {
   const { pid } = useParams<GuestInfoParams>()
   const [t] = useTranslation(['common'])
-  const [guestInfo, setGuest] = useState<Guest>({
-    pid: '',
-    first: '',
-    last: '',
-    email: '',
-    fnr: '',
-    mobile: '',
-    active: false,
-    registered: false,
-    verified: false,
-    roles: [],
-  })
-  const [roles, setRoles] = useState<Role[]>([])
-
-  const getPerson = async (id: string) => {
-    try {
-      const response = await fetch(`/api/ui/v1/person/${id}`)
-      const rjson = await response.json()
-      if (response.ok) {
-        setGuest({
-          pid: rjson.pid,
-          first: rjson.first,
-          last: rjson.last,
-          email: rjson.email,
-          mobile: rjson.mobile,
-          fnr: rjson.fnr,
-          active: rjson.active,
-          registered: rjson.registered,
-          verified: rjson.verified,
-          roles: rjson.roles,
-        })
-        setRoles(rjson.roles.map((role: FetchedRole) => parseRole(role)))
-      }
-    } catch (error) {
-      console.error(error)
-    }
-  }
-
-  useEffect(() => {
-    getPerson(pid)
-  }, [])
 
   return (
     <Page>
-      <SponsorInfoButtons to="/sponsor" />
-      <h4>{t('sponsor.contactInfo')}</h4>
+      <SponsorInfoButtons to="/sponsor" name={`${guest.first} ${guest.last}`} />
+      <h4>{t('guestInfo.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 align="left">{t('guestInfo.contactInfo')}</TableCell>
               <TableCell />
             </TableRow>
           </TableHead>
@@ -117,25 +75,36 @@ export default function GuestInfo() {
             <TableRow>
               <TableCell align="left">{t('input.fullName')}</TableCell>
               <TableCell align="left">
-                {`${guestInfo.first} ${guestInfo.last}`}
+                {`${guest.first} ${guest.last}`}
               </TableCell>
             </TableRow>
             <TableRow>
               <TableCell align="left">{t('input.email')}</TableCell>
-              <TableCell align="left">{guestInfo.email}</TableCell>
+              <TableCell align="left">{guest.email}</TableCell>
             </TableRow>
             <TableRow>
               <TableCell align="left">{t('input.nationalIdNumber')}</TableCell>
-              <TableCell align="left">{guestInfo.fnr}</TableCell>
+              <TableCell align="left">{guest.fnr}</TableCell>
             </TableRow>
             <TableRow>
               <TableCell align="left">{t('input.mobilePhone')}</TableCell>
-              <TableCell align="left">{guestInfo.mobile}</TableCell>
+              <TableCell align="left">{guest.mobile}</TableCell>
             </TableRow>
           </TableBody>
         </Table>
       </TableContainer>
-      <h4>{t('sponsor.roleInfo')}</h4>
+      <h4>{t('guestInfo.roleInfoHead')}</h4>
+      <h5>
+        {t('guestInfo.roleInfoBody')}
+        <Button
+          variant="contained"
+          component={Link}
+          to={`/sponsor/guest/${pid}/newrole`}
+        >
+          {t('sponsor.addRole')}
+        </Button>
+      </h5>
+
       <TableContainer component={Paper}>
         <Table sx={{ minWidth: 650 }} aria-label="simple table">
           <TableHead sx={{ backgroundColor: 'primary.light' }}>
@@ -148,7 +117,7 @@ export default function GuestInfo() {
           </TableHead>
           <TableBody>
             {roles.map((role) => (
-              <RoleLine pid={pid} role={role} />
+              <RoleLine key={role.id} pid={pid} role={role} />
             ))}
           </TableBody>
         </Table>
diff --git a/frontend/src/routes/sponsor/guestRoleInfo/index.tsx b/frontend/src/routes/sponsor/guest/guestRoleInfo/index.tsx
similarity index 90%
rename from frontend/src/routes/sponsor/guestRoleInfo/index.tsx
rename to frontend/src/routes/sponsor/guest/guestRoleInfo/index.tsx
index fed60946..6445c4ac 100644
--- a/frontend/src/routes/sponsor/guestRoleInfo/index.tsx
+++ b/frontend/src/routes/sponsor/guest/guestRoleInfo/index.tsx
@@ -12,16 +12,17 @@ import {
   TextField,
 } from '@mui/material'
 import Page from 'components/page'
-import { Guest } from 'interfaces'
+import { Guest, Role } from 'interfaces'
 import { useTranslation } from 'react-i18next'
 import { useParams } from 'react-router-dom'
 import SponsorInfoButtons from 'routes/components/sponsorInfoButtons'
 import { DatePicker } from '@mui/lab'
 import { Controller, SubmitHandler, useForm } from 'react-hook-form'
-import { submitJsonOpts } from '../../../utils'
+import { submitJsonOpts } from '../../../../utils'
 
 interface GuestRoleInfoProps {
-  guests: Guest[]
+  guest: Guest
+  roles: Role[]
 }
 const endPeriodPost = (id: string, data: { end_date: Date }) => {
   const payload = {
@@ -56,15 +57,12 @@ type RoleFormData = {
   end_date: Date
 }
 
-export default function GuestRoleInfo({ guests }: GuestRoleInfoProps) {
+export default function GuestRoleInfo({ guest, roles }: GuestRoleInfoProps) {
   const { pid, id } = useParams<GuestRoleInfoParams>()
   const [t, i18n] = useTranslation('common')
 
   // Find the role info relevant for this page
-  const guestInfo = guests.filter((guest) => guest.pid.toString() === pid)[0]
-  const roleInfo = guestInfo.roles.filter(
-    (role) => role.id.toString() === id
-  )[0]
+  const roleInfo = roles.filter((role) => role.id.toString() === id)[0]
 
   // Prepare min and max date values
   const today = new Date()
@@ -106,10 +104,12 @@ export default function GuestRoleInfo({ guests }: GuestRoleInfoProps) {
 
   const { control, handleSubmit } = useForm()
   const onSubmit = handleSubmit(submit)
-
   return (
     <Page>
-      <SponsorInfoButtons to={`/sponsor/guest/${pid}`} />
+      <SponsorInfoButtons
+        to={`/sponsor/guest/${pid}`}
+        name={`${guest.first} ${guest.last}`}
+      />
       <h4>{t('sponsor.roleInfoText')}</h4>
       <form onSubmit={onSubmit}>
         <TableContainer component={Paper}>
@@ -139,9 +139,7 @@ export default function GuestRoleInfo({ guests }: GuestRoleInfoProps) {
                     render={({ field: { onChange, value } }) => (
                       <DatePicker
                         mask="____-__-__"
-                        disabled={
-                          roleInfo.start_date.getDate() <= today.getDate()
-                        }
+                        disabled={roleInfo.start_date <= today}
                         label={t('input.roleStartDate')}
                         value={value}
                         minDate={today}
@@ -162,7 +160,7 @@ export default function GuestRoleInfo({ guests }: GuestRoleInfoProps) {
                       <DatePicker
                         mask="____-__-__"
                         label={t('input.roleEndDate')}
-                        disabled={roleInfo.end_date.getDate() < today.getDate()}
+                        disabled={roleInfo.end_date < today}
                         minDate={today}
                         maxDate={todayPlusMaxDays}
                         value={value}
diff --git a/frontend/src/routes/sponsor/guest/index.tsx b/frontend/src/routes/sponsor/guest/index.tsx
new file mode 100644
index 00000000..81152184
--- /dev/null
+++ b/frontend/src/routes/sponsor/guest/index.tsx
@@ -0,0 +1,73 @@
+import { FetchedRole, Guest, Role } from 'interfaces'
+import { useEffect, useState } from 'react'
+import { Route, useParams } from 'react-router-dom'
+import { parseRole } from 'utils'
+import GuestInfo from './guestInfo'
+import GuestRoleInfo from './guestRoleInfo'
+import NewGuestRole from './newGuestRole'
+
+type GuestInfoParams = {
+  pid: string
+}
+
+function GuestRoutes() {
+  const { pid } = useParams<GuestInfoParams>()
+
+  const [guestInfo, setGuest] = useState<Guest>({
+    pid: '',
+    first: '',
+    last: '',
+    email: '',
+    fnr: '',
+    mobile: '',
+    active: false,
+    registered: false,
+    verified: false,
+    roles: [],
+  })
+  const [roles, setRoles] = useState<Role[]>([])
+
+  const getPerson = async (id: string) => {
+    try {
+      const response = await fetch(`/api/ui/v1/person/${id}`)
+      const rjson = await response.json()
+      if (response.ok) {
+        setGuest({
+          pid: rjson.pid,
+          first: rjson.first,
+          last: rjson.last,
+          email: rjson.email,
+          mobile: rjson.mobile,
+          fnr: rjson.fnr,
+          active: rjson.active,
+          registered: rjson.registered,
+          verified: rjson.verified,
+          roles: rjson.roles,
+        })
+        setRoles(rjson.roles.map((role: FetchedRole) => parseRole(role)))
+      }
+    } catch (error) {
+      console.error(error)
+    }
+  }
+
+  useEffect(() => {
+    getPerson(pid)
+  }, [])
+
+  return (
+    <>
+      <Route path="/sponsor/guest/:pid/roles/:id">
+        <GuestRoleInfo guest={guestInfo} roles={roles} />
+      </Route>
+      <Route exact path="/sponsor/guest/:pid/newrole">
+        <NewGuestRole guest={guestInfo} />
+      </Route>
+      <Route exact path="/sponsor/guest/:pid">
+        <GuestInfo guest={guestInfo} roles={roles} />
+      </Route>
+    </>
+  )
+}
+
+export default GuestRoutes
diff --git a/frontend/src/routes/sponsor/guest/newGuestRole/index.tsx b/frontend/src/routes/sponsor/guest/newGuestRole/index.tsx
new file mode 100644
index 00000000..2cf41c37
--- /dev/null
+++ b/frontend/src/routes/sponsor/guest/newGuestRole/index.tsx
@@ -0,0 +1,274 @@
+import { DatePicker } from '@mui/lab'
+import { addDays } from 'date-fns/fp'
+import {
+  Checkbox,
+  Button,
+  Select,
+  FormControl,
+  InputLabel,
+  MenuItem,
+  Stack,
+  TextField,
+  SelectChangeEvent,
+  FormControlLabel,
+} from '@mui/material'
+import Page from 'components/page'
+import { format } from 'date-fns'
+import useOus, { enSort, nbSort, OuData } from 'hooks/useOus'
+import useRoleTypes, { RoleTypeData } from 'hooks/useRoleTypes'
+import { Guest } from 'interfaces'
+import { useState } from 'react'
+import { Controller, useForm } from 'react-hook-form'
+import { useTranslation } from 'react-i18next'
+import { Link, useParams } from 'react-router-dom'
+import SponsorInfoButtons from 'routes/components/sponsorInfoButtons'
+import { submitJsonOpts } from 'utils'
+
+type AddRoleFormData = {
+  orgunit: number
+  type: string
+  end_date: Date
+  start_date?: Date
+  contact_person_unit?: string
+  comments?: string
+  available_in_search?: boolean
+}
+type AddRolePayload = {
+  orgunit: number
+  person: string
+  type: string
+  end_date: string
+  start_date?: string
+  contact_person_unit?: string
+  comments?: string
+  available_in_search?: boolean
+}
+
+type GuestInfoParams = {
+  pid: string
+}
+
+interface NewGuestRoleProps {
+  guest: Guest
+}
+
+const postRole = (formData: AddRoleFormData, pid: string) => {
+  const payload: AddRolePayload = {
+    orgunit: formData.orgunit,
+    person: pid,
+    type: formData.type,
+    end_date: format(formData.end_date as Date, 'yyyy-MM-dd'),
+  }
+  if (formData.start_date) {
+    payload.start_date = format(formData.start_date as Date, 'yyyy-MM-dd')
+  }
+  if (formData.contact_person_unit) {
+    payload.contact_person_unit = formData.contact_person_unit
+  }
+  if (formData.comments) {
+    payload.comments = formData.comments
+  }
+  if (formData.available_in_search) {
+    payload.available_in_search = formData.available_in_search
+  }
+
+  console.log('submitting', JSON.stringify(payload))
+  fetch('/api/ui/v1/role', submitJsonOpts('POST', payload))
+    .then((res) => {
+      if (!res.ok) {
+        console.log('result', res)
+        return null
+      }
+      console.log('result', res)
+      return res.text()
+    })
+    .then((result) => {
+      if (result !== null) {
+        console.log('result', result)
+      }
+    })
+    .catch((error) => {
+      console.log('error', error)
+    })
+}
+
+function NewGuestRole({ guest }: NewGuestRoleProps) {
+  const {
+    register,
+    control,
+    handleSubmit,
+    formState: { errors },
+    setValue,
+    getValues,
+  } = useForm<AddRoleFormData>()
+
+  const { pid } = useParams<GuestInfoParams>()
+  const onSubmit = handleSubmit(() => {
+    postRole(getValues(), pid)
+  })
+
+  const ous = useOus()
+  const roleTypes = useRoleTypes()
+  const [ouChoice, setOuChoice] = useState<string>('')
+  const [roleTypeChoice, setRoleTypeChoice] = useState<string>('')
+  const [t, i18n] = useTranslation('common')
+  const today = new Date()
+
+  const todayPlusMaxDays = () => {
+    if (roleTypeChoice) {
+      const role = roleTypes.filter(
+        (rt) => rt.id.toString() === roleTypeChoice.toString()
+      )[0]
+      return addDays(role.max_days)(today)
+    }
+    return addDays(0)(today)
+  }
+
+  const roleTypeSort = () => (a: RoleTypeData, b: RoleTypeData) => {
+    if (i18n.language === 'en') {
+      return a.name_nb.localeCompare(b.name_nb)
+    }
+    return a.name_en.localeCompare(b.name_en)
+  }
+  // Handling choices in menus
+  const handleRoleTypeChange = (event: SelectChangeEvent) => {
+    setValue('type', event.target.value)
+    setRoleTypeChoice(event.target.value)
+  }
+  const handleOuChange = (event: SelectChangeEvent) => {
+    if (event.target.value) {
+      setOuChoice(event.target.value)
+      setValue('orgunit', parseInt(event.target.value, 10))
+    }
+  }
+  // Functions for menu items
+  const rolesToItem = (roleType: RoleTypeData) => (
+    <MenuItem key={roleType.id.toString()} value={roleType.id}>
+      {i18n.language === 'en' ? roleType.name_en : roleType.name_nb}
+    </MenuItem>
+  )
+  const ouToItem = (ou: OuData) => (
+    <MenuItem key={ou.id.toString()} value={ou.id}>
+      {i18n.language === 'en' ? ou.en : ou.nb} ({ou.id})
+    </MenuItem>
+  )
+
+  return (
+    <Page>
+      <SponsorInfoButtons
+        to={`/sponsor/guest/${pid}`}
+        name={`${guest.first} ${guest.last}`}
+      />
+      <h3>{t('guest.headerText')}</h3>
+      <h4>{t('guest.bodyText')}</h4>
+      <form onSubmit={onSubmit}>
+        <Stack spacing={2}>
+          <FormControl>
+            <InputLabel id="ou-select-label">{t('input.roleType')}</InputLabel>
+            <Select
+              id="roletype-select"
+              defaultValue=""
+              value={roleTypeChoice}
+              error={!!errors.type}
+              label={t('input.roleType')}
+              onChange={handleRoleTypeChange}
+            >
+              {roleTypes.sort(roleTypeSort()).map((rt) => rolesToItem(rt))}
+            </Select>
+          </FormControl>
+
+          <FormControl>
+            <InputLabel id="ou-select-label">{t('common:ou')}</InputLabel>
+            <Select
+              labelId="ou-select-label"
+              id="ou-select-label"
+              defaultValue=""
+              value={ouChoice.toString()}
+              label={t('common:ou')}
+              onChange={handleOuChange}
+            >
+              {ous.length > 0 ? (
+                ous
+                  .sort(i18n.language === 'en' ? enSort : nbSort)
+                  .map((ou) => ouToItem(ou))
+              ) : (
+                <></>
+              )}
+            </Select>
+          </FormControl>
+          <Controller
+            name="start_date"
+            control={control}
+            defaultValue={today}
+            render={({ field }) => (
+              <DatePicker
+                mask="____-__-__"
+                label={t('input.roleStartDate')}
+                disabled={!roleTypeChoice}
+                value={field.value}
+                minDate={today}
+                maxDate={todayPlusMaxDays()}
+                inputFormat="yyyy-MM-dd"
+                onChange={(value) => {
+                  field.onChange(value)
+                }}
+                renderInput={(params) => <TextField {...params} />}
+              />
+            )}
+          />
+          <Controller
+            name="end_date"
+            control={control}
+            defaultValue={today}
+            render={({ field }) => (
+              <DatePicker
+                mask="____-__-__"
+                label={t('input.roleEndDate')}
+                disabled={!roleTypeChoice}
+                value={field.value}
+                minDate={today}
+                maxDate={todayPlusMaxDays()}
+                inputFormat="yyyy-MM-dd"
+                onChange={(value) => {
+                  field.onChange(value)
+                }}
+                renderInput={(params) => <TextField {...params} />}
+              />
+            )}
+          />
+
+          <TextField
+            id="contact"
+            label={t('input.contact')}
+            multiline
+            rows={5}
+            {...register('contact_person_unit')}
+          />
+          <TextField
+            id="comments"
+            label={t('input.comment')}
+            multiline
+            rows={5}
+            {...register('comments')}
+          />
+          <FormControlLabel
+            control={
+              <Checkbox
+                id="available_in_search"
+                {...register('available_in_search')}
+              />
+            }
+            label={t('input.searchable')}
+          />
+          <Button variant="contained" type="submit">
+            {t('button.save')}
+          </Button>
+          <Button component={Link} to={`/sponsor/guest/${pid}`}>
+            {t('button.cancel')}
+          </Button>
+        </Stack>
+      </form>
+    </Page>
+  )
+}
+export default NewGuestRole
diff --git a/frontend/src/routes/sponsor/index.tsx b/frontend/src/routes/sponsor/index.tsx
index 1df6b21d..d070e2b2 100644
--- a/frontend/src/routes/sponsor/index.tsx
+++ b/frontend/src/routes/sponsor/index.tsx
@@ -1,11 +1,10 @@
-import React, { useEffect, useState } from 'react'
+import { useEffect, useState } from 'react'
 import { Route } from 'react-router-dom'
 
 import FrontPage from 'routes/sponsor/frontpage'
-import GuestInfo from 'routes/sponsor/guestInfo'
-import GuestRoleInfo from 'routes/sponsor/guestRoleInfo'
 import { FetchedGuest, Guest } from 'interfaces'
 import { parseRole } from 'utils'
+import GuestRoutes from './guest'
 
 function Sponsor() {
   const [guests, setGuests] = useState<Guest[]>([])
@@ -44,11 +43,8 @@ function Sponsor() {
 
   return (
     <>
-      <Route path="/sponsor/guest/:pid/roles/:id">
-        <GuestRoleInfo guests={guests} />
-      </Route>
-      <Route exact path="/sponsor/guest/:pid">
-        <GuestInfo />
+      <Route path="/sponsor/guest/:pid">
+        <GuestRoutes />
       </Route>
       <Route exact path="/sponsor">
         <FrontPage guests={guests} />
diff --git a/gregui/api/serializers/invitation.py b/gregui/api/serializers/invitation.py
index c526a986..18aa8926 100644
--- a/gregui/api/serializers/invitation.py
+++ b/gregui/api/serializers/invitation.py
@@ -5,13 +5,13 @@ from django.utils import timezone
 from rest_framework import serializers
 
 from greg.models import Invitation, InvitationLink, Person, Role, Identity
-from gregui.api.serializers.role import RoleSerializerUi
+from gregui.api.serializers.role import InviteRoleSerializerUi
 from gregui.models import GregUserProfile
 
 
 class InviteGuestSerializer(serializers.ModelSerializer):
     email = serializers.EmailField(required=True)
-    role = RoleSerializerUi(required=True)
+    role = InviteRoleSerializerUi(required=True)
     uuid = serializers.UUIDField(read_only=True)
 
     def create(self, validated_data):
diff --git a/gregui/api/serializers/role.py b/gregui/api/serializers/role.py
index f9791220..722c8e8f 100644
--- a/gregui/api/serializers/role.py
+++ b/gregui/api/serializers/role.py
@@ -1,44 +1,99 @@
 import datetime
 from rest_framework import serializers
+from rest_framework.exceptions import ValidationError
+from rest_framework.validators import UniqueTogetherValidator
 
 from greg.models import Role
 
 
 class RoleSerializerUi(serializers.ModelSerializer):
-    def update(self, instance, validated_data):
-        # validate new date is after old date, and old date has not passed
+    """Serializer for the Role model with validation of various fields"""
+
+    class Meta:
+        model = Role
+        fields = [
+            "orgunit",
+            "start_date",
+            "type",
+            "end_date",
+            "contact_person_unit",
+            "comments",
+            "available_in_search",
+            "person",
+        ]
+        validators = [
+            UniqueTogetherValidator(
+                queryset=Role.objects.all(),
+                fields=["person", "type", "orgunit", "start_date", "end_date"],
+            )
+        ]
+
+    def validate_start_date(self, start_date):
         today = datetime.date.today()
-        new_start = validated_data.get("start_date")
-        new_end = validated_data.get("end_date")
-        if new_start:
-            new_start = datetime.datetime.strptime(
-                validated_data["start_date"], "%Y-%m-%d"
-            ).date()
-            if new_start < today:
-                raise serializers.ValidationError("Start date cannot be in the past")
-            if instance.start_date < today and new_start:
-                raise serializers.ValidationError(
-                    "Role has started, cannot change start date"
-                )
-            instance.start_date = new_start
-        if new_end:
-            new_end = datetime.datetime.strptime(
-                validated_data["end_date"], "%Y-%m-%d"
-            ).date()
-            if new_end < today:
-                raise serializers.ValidationError("End date cannot be in the past")
-            if instance.end_date < today and new_end:
+        # New start dates cannot be in the past
+        if start_date < today:
+            raise serializers.ValidationError("Start date cannot be in the past")
+
+        return start_date
+
+    def validate_end_date(self, end_date):
+        """Ensure rules for end_date are followed"""
+        today = datetime.date.today()
+        if end_date < today:
+            raise serializers.ValidationError("End date cannot be in the past")
+        if self.instance and self.instance.end_date < today:
+            raise serializers.ValidationError("Role has ended, cannot change end date")
+        return end_date
+
+    def validate_orgunit(self, unit):
+        """Enforce rules related to orgunit"""
+        sponsor = self.context["sponsor"]
+        units = sponsor.units.all()
+        # Restrict to a sponsor's own units
+        if not units or unit not in units:
+            raise ValidationError(
+                "A sponsor can only make changes to roles at units they are sponsors for."
+            )
+        # If we are updating an existing roles, we must be the sponsor of the role
+        if self.instance and self.instance.sponsor != sponsor:
+            raise ValidationError("You can only edit your own roles.")
+        return unit
+
+    def validate(self, attrs):
+        """Validate things that need access to multiple fields"""
+        # Ensure end date is not further into the future than the role type allows
+        today = datetime.date.today()
+        if self.instance:
+            max_days = today + datetime.timedelta(days=self.instance.type.max_days)
+        else:
+            max_days = today + datetime.timedelta(days=attrs["type"].max_days)
+        if attrs["end_date"] > max_days:
+            raise serializers.ValidationError(
+                f"New end date too far into the future for this type. Must be before {max_days.strftime('%Y-%m-%d')}"
+            )
+        # Ensure end date is after start date if start date is set
+        if self.instance:
+            start_date = attrs.get("start_date") or self.instance.start_date
+            end_date = attrs.get("end_date") or self.instance.end_date
+            if start_date and end_date < start_date:
                 raise serializers.ValidationError(
-                    "Role has ended, cannot change end date"
+                    "End date cannot be before start date."
                 )
-            max_days = today + datetime.timedelta(days=instance.type.max_days)
-            if new_end > max_days:
+        else:
+            if attrs.get("start_date") and attrs["end_date"] < attrs["start_date"]:
                 raise serializers.ValidationError(
-                    f"New end date too far into the future. Must be before {max_days.strftime('%Y-%m-%d')}"
+                    "End date cannot be before start date."
                 )
-            instance.end_date = new_end
+        return attrs
+
+
+class InviteRoleSerializerUi(RoleSerializerUi):
+    """
+    Serializer for the role part of an invite.
 
-        return instance
+    This one exists so that we don't have to specify the person argument on the role.
+    Simply reuses all the logic of the parent class except requiring the person field.
+    """
 
     class Meta:
         model = Role
diff --git a/gregui/api/serializers/roletype.py b/gregui/api/serializers/roletype.py
index 5f1857d6..d2adecea 100644
--- a/gregui/api/serializers/roletype.py
+++ b/gregui/api/serializers/roletype.py
@@ -6,4 +6,4 @@ from greg.models import RoleType
 class RoleTypeSerializerUi(ModelSerializer):
     class Meta:
         model = RoleType
-        fields = ("id", "identifier", "name_en", "name_nb")
+        fields = ("id", "identifier", "name_en", "name_nb", "max_days")
diff --git a/gregui/api/urls.py b/gregui/api/urls.py
index db1d56db..af6ff1ff 100644
--- a/gregui/api/urls.py
+++ b/gregui/api/urls.py
@@ -8,10 +8,12 @@ from gregui.api.views.invitation import (
     InvitedGuestView,
 )
 from gregui.api.views.person import PersonSearchView, PersonView
+from gregui.api.views.role import RoleInfoViewSet
 from gregui.api.views.roletypes import RoleTypeViewSet
 from gregui.api.views.unit import UnitsViewSet
 
 router = DefaultRouter(trailing_slash=False)
+router.register(r"role", RoleInfoViewSet, basename="role")
 
 urlpatterns = router.urls
 urlpatterns += [
diff --git a/gregui/api/views/person.py b/gregui/api/views/person.py
index 79326f42..e5953a28 100644
--- a/gregui/api/views/person.py
+++ b/gregui/api/views/person.py
@@ -29,6 +29,9 @@ class PersonView(APIView):
             "email": person.private_email and person.private_email.value,
             "mobile": person.private_mobile and person.private_mobile.value,
             "fnr": person.fnr and "".join((person.fnr.value[:-5], "*****")),
+            "active": person.is_registered and person.is_verified,
+            "registered": person.is_registered,
+            "verified": person.is_verified,
             "roles": [
                 {
                     "id": role.id,
diff --git a/gregui/api/views/role.py b/gregui/api/views/role.py
index 47b2fbf9..7424b285 100644
--- a/gregui/api/views/role.py
+++ b/gregui/api/views/role.py
@@ -1,30 +1,46 @@
 from django.db import transaction
-from rest_framework import status
+from rest_framework import serializers, status
 from rest_framework.authentication import BasicAuthentication, SessionAuthentication
-from rest_framework.generics import GenericAPIView
+from rest_framework.viewsets import ModelViewSet
 from rest_framework.permissions import IsAuthenticated
 from rest_framework.response import Response
 
 from greg.models import Role
 from greg.permissions import IsSponsor
 from gregui.api.serializers.role import RoleSerializerUi
-from gregui.api.serializers.roletype import RoleTypeSerializerUi
+from gregui.models import GregUserProfile
 
 
-class RoleInfoView(GenericAPIView):
+class RoleInfoViewSet(ModelViewSet):
     queryset = Role.objects.all()
     authentication_classes = [SessionAuthentication, BasicAuthentication]
     permission_classes = [IsAuthenticated, IsSponsor]
     serializer_class = RoleSerializerUi
 
-    def patch(self, request, id):
-        role = Role.objects.get(id=id)
-
+    def partial_update(self, request, pk):
+        role = Role.objects.get(pk=pk)
+        sponsor = GregUserProfile.objects.get(user=self.request.user).sponsor
         with transaction.atomic():
             serializer = self.serializer_class(
-                instance=role, data=request.data, partial=True
+                instance=role,
+                data=request.data,
+                partial=True,
+                context={"sponsor": sponsor},
             )
             serializer.is_valid(raise_exception=True)
-            instance = serializer.update(role, request.data)
+            instance = serializer.update(role, serializer.validated_data)
             instance.save()
         return Response(status=status.HTTP_200_OK)
+
+    def create(self, request):
+        sponsor = GregUserProfile.objects.get(user=self.request.user).sponsor
+        with transaction.atomic():
+            serializer = self.serializer_class(
+                data=request.data,
+                context={
+                    "sponsor": sponsor,
+                },
+            )
+            serializer.is_valid(raise_exception=True)
+            serializer.save(sponsor=sponsor)
+        return Response(status=status.HTTP_201_CREATED)
diff --git a/gregui/tests/api/test_invite_guest.py b/gregui/tests/api/test_invite_guest.py
index 46f8b108..87554ec7 100644
--- a/gregui/tests/api/test_invite_guest.py
+++ b/gregui/tests/api/test_invite_guest.py
@@ -1,3 +1,4 @@
+import datetime
 import pytest
 from rest_framework import status
 from rest_framework.reverse import reverse
@@ -14,8 +15,12 @@ def test_invite_guest(client, user_sponsor, unit_foo, role_type_foo):
         "last_name": "Bar",
         "email": "test@example.com",
         "role": {
-            "start_date": "2019-08-06",
-            "end_date": "2019-08-10",
+            "start_date": (
+                datetime.datetime.today() + datetime.timedelta(days=1)
+            ).strftime("%Y-%m-%d"),
+            "end_date": (
+                datetime.datetime.today() + datetime.timedelta(days=10)
+            ).strftime("%Y-%m-%d"),
             "orgunit": unit_foo.id,
             "type": role_type_foo.id,
         },
diff --git a/gregui/urls.py b/gregui/urls.py
index 5ef2297a..cc6fa884 100644
--- a/gregui/urls.py
+++ b/gregui/urls.py
@@ -5,7 +5,6 @@ from django.urls import path
 from django.urls.resolvers import URLResolver
 
 from gregui.api import urls as api_urls
-from gregui.api.views.role import RoleInfoView
 from gregui.api.views.userinfo import UserInfoView
 from gregui.views import OusView, GuestInfoView
 from . import views
@@ -23,5 +22,4 @@ urlpatterns: List[URLResolver] = [
     path("api/ui/v1/userinfo/", UserInfoView.as_view()),  # type: ignore
     path("api/ui/v1/ous/", OusView.as_view()),
     path("api/ui/v1/guests/", GuestInfoView.as_view()),
-    path("api/ui/v1/role/<int:id>", RoleInfoView.as_view()),
 ]
-- 
GitLab