From 034c73d57b1f004987a6d71ff9108af609d97d90 Mon Sep 17 00:00:00 2001
From: Sivert Kronen Hatteberg <sivert.hatteberg@usit.uio.no>
Date: Wed, 8 Feb 2023 10:46:47 +0100
Subject: [PATCH] Add filter on unit for the guests table

---
 frontend/public/locales/en/common.json        |   1 +
 frontend/public/locales/nb/common.json        |   1 +
 frontend/public/locales/nn/common.json        |   1 +
 .../src/routes/sponsor/frontpage/index.tsx    | 307 ++++++++++++++----
 4 files changed, 255 insertions(+), 55 deletions(-)

diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json
index 26411b65..65bb63ea 100644
--- a/frontend/public/locales/en/common.json
+++ b/frontend/public/locales/en/common.json
@@ -107,6 +107,7 @@
   "foundNoGuests": "Found no guests",
   "sentInvitations": "Sent invitations",
   "placeholder": "Search for guest",
+  "chooseUnits": "Choose unit(s)",
   "sentInvitationsDescription": "Invitations awaiting response from guest.",
   "noInvitations": "No invitations",
   "status": "Status",
diff --git a/frontend/public/locales/nb/common.json b/frontend/public/locales/nb/common.json
index 6780ea9e..908dd470 100644
--- a/frontend/public/locales/nb/common.json
+++ b/frontend/public/locales/nb/common.json
@@ -107,6 +107,7 @@
   "foundNoGuests": "Fant ingen gjester",
   "sentInvitations": "Sendte invitasjoner",
   "placeholder": "Søk etter gjest",
+  "chooseUnits": "Velg avdeling(er)",
   "sentInvitationsDescription": "Invitasjoner som venter på at gjesten skal ferdigstille registreringen.",
   "noInvitations": "Ingen invitasjoner",
   "status": "Status",
diff --git a/frontend/public/locales/nn/common.json b/frontend/public/locales/nn/common.json
index c8751c92..2119651f 100644
--- a/frontend/public/locales/nn/common.json
+++ b/frontend/public/locales/nn/common.json
@@ -107,6 +107,7 @@
   "foundNoGuests": "Fann ingen gjester",
   "sentInvitations": "Sendte invitasjonar",
   "placeholder": "Søk etter gjest",
+  "chooseUnits": "Vel avdeling(ar)",
   "sentInvitationsDescription": "Invitasjonar som venter på at gjesten skal ferdigstille registreringa.",
   "noInvitations": "Ingen invitasjonar",
   "status": "Status",
diff --git a/frontend/src/routes/sponsor/frontpage/index.tsx b/frontend/src/routes/sponsor/frontpage/index.tsx
index 33833ef8..fe091143 100644
--- a/frontend/src/routes/sponsor/frontpage/index.tsx
+++ b/frontend/src/routes/sponsor/frontpage/index.tsx
@@ -1,3 +1,5 @@
+import { useEffect, useState } from 'react'
+
 import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'
 import NotificationsActiveIcon from '@mui/icons-material/NotificationsActive'
 import {
@@ -5,8 +7,14 @@ import {
   AccordionDetails,
   AccordionSummary,
   Button,
+  Chip,
+  FormControl,
   InputAdornment,
+  InputLabel,
+  MenuItem,
   Paper,
+  Select,
+  SelectChangeEvent,
   Table,
   TableBody,
   TableCell,
@@ -18,7 +26,6 @@ import {
   Typography,
 } from '@mui/material'
 import { Box, styled } from '@mui/system'
-import { useState } from 'react'
 import SearchIcon from '@mui/icons-material/Search'
 
 import Loading from 'components/loading'
@@ -48,6 +55,7 @@ interface GuestTableProps {
   guests: Guest[]
   emptyText: string
   marginWidth: number
+  unitFilters: string[]
 }
 
 interface FrontPageProps {
@@ -98,7 +106,7 @@ const StyledTableHead = styled(TableHead)(({ theme }) => ({
   borderRadius: '0',
 }))
 
-const calculateStatus = (person: Guest, role: Role): [string, number] => {
+function calculateStatus(person: Guest, role: Role): [string, number] {
   const today = new Date()
   today.setHours(0, 0, 0, 0)
   let status = ''
@@ -125,7 +133,7 @@ const calculateStatus = (person: Guest, role: Role): [string, number] => {
   return [status, days]
 }
 
-const Status = ({ person, role }: StatusProps) => {
+function Status({ person, role }: StatusProps) {
   const { t } = useTranslation('common')
   const [status, days] = calculateStatus(person, role)
 
@@ -265,7 +273,10 @@ function sortByStatus(guests: Guest[], direction: SortDirection): GuestRole[] {
     .concat(expiringRoleAndPerson)
 }
 
-function sortGuestsByRoleName(guests: Guest[], direction: SortDirection) {
+function sortGuestsByRoleName(
+  guests: Guest[],
+  direction: SortDirection
+): GuestRole[] {
   return createGuestsRoles(guests).sort((a, b) => {
     const aRoleName = getRoleName(a.role).toLowerCase()
     const bRoleName = getRoleName(b.role).toLowerCase()
@@ -280,7 +291,10 @@ function sortGuestsByRoleName(guests: Guest[], direction: SortDirection) {
   })
 }
 
-function sortGuestsByDepartmentName(guests: Guest[], direction: SortDirection) {
+function sortGuestsByDepartmentName(
+  guests: Guest[],
+  direction: SortDirection
+): GuestRole[] {
   return createGuestsRoles(guests).sort((a, b) => {
     const aRoleOUName = getRoleOuName(a.role).toLowerCase()
     const bRoleOUName = getRoleOuName(b.role).toLowerCase()
@@ -298,26 +312,43 @@ function sortGuestsByDepartmentName(guests: Guest[], direction: SortDirection) {
 function sortGuestsAndRoles(
   guests: Guest[],
   sortField: SortField,
-  direction: SortDirection
-): { guest: Guest; role: Role }[] {
+  direction: SortDirection,
+  unitFilters?: string[]
+): GuestRole[] {
+  let guestRoles: GuestRole[]
+
   switch (sortField) {
     case 'department':
-      return sortGuestsByDepartmentName(guests, direction)
+      guestRoles = sortGuestsByDepartmentName(guests, direction)
+      break
     case 'endDate':
-      return sortGuestRolesByEndDate(createGuestsRoles(guests), direction)
+      guestRoles = sortGuestRolesByEndDate(createGuestsRoles(guests), direction)
+      break
     case 'name':
-      return sortGuestsByName(guests, direction)
+      guestRoles = sortGuestsByName(guests, direction)
+      break
     case 'role':
-      return sortGuestsByRoleName(guests, direction)
+      guestRoles = sortGuestsByRoleName(guests, direction)
+      break
     case 'status':
-      return sortByStatus(guests, direction)
+      guestRoles = sortByStatus(guests, direction)
+      break
     default:
       // Fallback to original sort
-      return createGuestsRoles(guests)
+      guestRoles = createGuestsRoles(guests)
+      break
+  }
+
+  if (unitFilters && unitFilters.length > 0) {
+    return guestRoles.filter(({ role }) =>
+      unitFilters.includes(role.ou_id.toString())
+    )
   }
+
+  return guestRoles
 }
 
-const PersonLine = ({ person, role }: PersonLineProps) => {
+function PersonLine({ person, role }: PersonLineProps) {
   const [t] = useTranslation(['common'])
 
   return (
@@ -343,7 +374,52 @@ const PersonLine = ({ person, role }: PersonLineProps) => {
   )
 }
 
-const GuestTable = ({ guests, emptyText, marginWidth }: GuestTableProps) => {
+function PersonTableBody({
+  guests,
+  orderBy,
+  direction,
+  unitFilters,
+}: {
+  guests: Guest[]
+  orderBy: SortField
+  direction: SortDirection
+  unitFilters: string[] | undefined
+}) {
+  const { t } = useTranslation('common')
+
+  const filteredSortedGuests = sortGuestsAndRoles(
+    guests,
+    orderBy,
+    direction,
+    unitFilters
+  )
+  if (filteredSortedGuests.length === 0) {
+    return (
+      <StyledTableRow>
+        <TableCell>{t('foundNoGuests')}</TableCell>
+      </StyledTableRow>
+    )
+  }
+
+  return (
+    <>
+      {filteredSortedGuests.map((personRole) => (
+        <PersonLine
+          key={`${personRole.guest.first} ${personRole.guest.last} ${personRole.role.id}`}
+          role={personRole.role}
+          person={personRole.guest}
+        />
+      ))}
+    </>
+  )
+}
+
+function GuestTable({
+  guests,
+  emptyText,
+  marginWidth,
+  unitFilters,
+}: GuestTableProps) {
   const { t } = useTranslation('common')
 
   const [direction, setDirection] = useState<SortDirection>('asc')
@@ -425,13 +501,12 @@ const GuestTable = ({ guests, emptyText, marginWidth }: GuestTableProps) => {
         </StyledTableHead>
         <TableBody>
           {guests.length > 0 ? (
-            sortGuestsAndRoles(guests, orderBy, direction).map((personRole) => (
-              <PersonLine
-                key={`${personRole.guest.first} ${personRole.guest.last} ${personRole.role.id}`}
-                role={personRole.role}
-                person={personRole.guest}
-              />
-            ))
+            <PersonTableBody
+              guests={guests}
+              orderBy={orderBy}
+              direction={direction}
+              unitFilters={unitFilters}
+            />
           ) : (
             <StyledTableRow>
               <TableCell>{emptyText}</TableCell>
@@ -443,7 +518,7 @@ const GuestTable = ({ guests, emptyText, marginWidth }: GuestTableProps) => {
   )
 }
 
-const InvitedGuests = ({ persons }: GuestProps) => {
+function InvitedGuests({ persons }: GuestProps) {
   const [activeExpanded, setActiveExpanded] = useState(false)
 
   // Show guests that have not responded to the invite yet
@@ -477,6 +552,7 @@ const InvitedGuests = ({ persons }: GuestProps) => {
         <GuestTable
           guests={guests}
           emptyText={t('common:noInvitations')}
+          unitFilters={[]}
           marginWidth={650}
         />
       </AccordionDetails>
@@ -484,32 +560,138 @@ const InvitedGuests = ({ persons }: GuestProps) => {
   )
 }
 
-const ActiveGuests = ({ persons }: GuestProps) => {
-  const [activeExpanded, setActiveExpanded] = useState(false)
-  const [searchHasInput, setSearchHasInput] = useState(false)
+const ITEM_HEIGHT = 48
+const ITEM_PADDING_TOP = 8
+const MenuProps = {
+  PaperProps: {
+    style: {
+      maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
+      width: 400,
+    },
+  },
+}
+
+interface UnitFilterSelectProps {
+  guests: Guest[]
+  onChange: (units: string[]) => void
+}
+
+function UnitFilterSelect({ guests, onChange }: UnitFilterSelectProps) {
+  //
+  const [selectedUnits, setSelectedUnits] = useState<string[]>([])
+  const { t } = useTranslation(['common'])
+
+  useEffect(() => {
+    onChange(selectedUnits)
+  }, [selectedUnits, onChange])
+
+  const handleChange = (event: SelectChangeEvent<typeof selectedUnits>) => {
+    const {
+      target: { value },
+    } = event
+    setSelectedUnits(
+      // On autofill we get a stringified value.
+      typeof value === 'string' ? value.split(',') : value
+    )
+  }
+
+  const units: { [key: string]: string } = {}
+  guests.forEach((guest) =>
+    guest.roles.forEach((role) => {
+      if (!(role.ou_id in units)) {
+        units[role.ou_id.toString()] = getRoleOuName(role)
+      }
+    })
+  )
+
+  if (units) {
+    return (
+      <div>
+        <FormControl sx={{ m: 1, width: 450 }}>
+          <InputLabel id="ou-select-label">{t('chooseUnits')}</InputLabel>
+          <Select
+            labelId="ou-select-label"
+            id="ou-select"
+            value={selectedUnits}
+            onChange={handleChange}
+            multiple
+            label={t('chooseUnits')}
+            renderValue={(selected) => (
+              <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
+                {Object.entries(selected).map(([, id]) => (
+                  <Chip
+                    key={id}
+                    label={units[id]}
+                    onDelete={() =>
+                      setSelectedUnits(selectedUnits.filter((u) => id !== u))
+                    }
+                    onMouseDown={(event) => {
+                      event.stopPropagation()
+                    }}
+                  />
+                ))}
+              </Box>
+            )}
+            MenuProps={MenuProps}
+          >
+            {Object.entries(units).map(([id, name]) => (
+              <MenuItem key={id} value={id}>
+                {name}
+              </MenuItem>
+            ))}
+          </Select>
+        </FormControl>
+      </div>
+    )
+  }
+  return null
+}
+
+function ActiveGuests({ persons }: GuestProps) {
+  const [activeExpanded, setActiveExpanded] = useState<boolean>(false)
   const [searchGuests, setSearchGuests] = useState<Guest[]>([])
+  const [selectedUnits, setSelectedUnits] = useState<string[]>([])
+  const [searchInput, setSearchInput] = useState<string>('')
+  const [searching, setSearching] = useState<boolean>(false)
+
+  const [t] = useTranslation(['common'])
 
   // Show all verified guests
   let guests = persons.length > 0 ? persons : []
   if (guests.length > 0) {
     guests = guests.filter((person) => person.verified)
   }
-  const [t] = useTranslation(['common'])
 
-  const getSponsorGuests = (event: React.ChangeEvent<HTMLInputElement>) => {
-    if (event.target.value) {
-      setSearchHasInput(true)
-      const guestSearch: Guest[] = guests.filter((guest) =>
-        `${guest.first.toLowerCase()} ${guest.last.toLowerCase()}`.includes(
-          event.target.value.toLowerCase()
+  // Wait 1s after last change to start the search.
+  useEffect(() => {
+    if (searchInput === '') {
+      if (searchGuests) {
+        setSearchGuests([])
+      }
+      return () => {}
+    }
+    setSearching(true)
+    const delaySearch = setTimeout(() => {
+      setSearchGuests(
+        guests.filter((guest) =>
+          `${guest.first.toLowerCase()} ${guest.last.toLowerCase()}`.includes(
+            searchInput
+          )
         )
       )
-      setSearchGuests(guestSearch)
+      setSearching(false)
+    }, 1000)
+    return () => clearTimeout(delaySearch)
+  }, [searchInput])
+
+  const getSponsorGuests = (event: React.ChangeEvent<HTMLInputElement>) => {
+    if (event.target.value) {
+      setSearchInput(event.target.value.toLowerCase())
     } else {
-      setSearchHasInput(false)
-      setSearchGuests([])
+      setSearchInput('')
     }
   }
+
   return (
     <StyledAccordion
       expanded={activeExpanded}
@@ -536,30 +718,44 @@ const ActiveGuests = ({ persons }: GuestProps) => {
             marginBottom: '1rem',
           }}
         >
-          <TextField
-            variant="standard"
-            InputProps={{
-              endAdornment: (
-                <InputAdornment position="end">
-                  <SearchIcon />
-                </InputAdornment>
-              ),
+          <Box
+            sx={{
+              display: 'flex',
+              justifyContent: 'space-between',
+              alignItems: 'flex-start',
             }}
-            placeholder={t('placeholder')}
-            onChange={getSponsorGuests}
-          />
-          {!searchHasInput ? (
-            <GuestTable
+          >
+            <FormControl sx={{ m: 1, width: 300 }}>
+              <TextField
+                InputProps={{
+                  endAdornment: (
+                    <InputAdornment position="end">
+                      <SearchIcon />
+                    </InputAdornment>
+                  ),
+                }}
+                placeholder={t('placeholder')}
+                onChange={getSponsorGuests}
+              />
+            </FormControl>
+            <UnitFilterSelect
               guests={guests}
-              emptyText={t('common:noActiveGuests')}
-              marginWidth={1000}
+              onChange={(units: string[]) => setSelectedUnits(units)}
             />
-          ) : (
+          </Box>
+          {!searching ? (
             <GuestTable
-              guests={searchGuests}
-              emptyText={t('common:foundNoGuests')}
+              guests={searchInput ? searchGuests : guests}
+              emptyText={
+                searchInput
+                  ? t('common:foundNoGuests')
+                  : t('common:noActiveGuests')
+              }
               marginWidth={1000}
+              unitFilters={selectedUnits}
             />
+          ) : (
+            <Loading />
           )}
         </Box>
       </AccordionDetails>
@@ -567,7 +763,7 @@ const ActiveGuests = ({ persons }: GuestProps) => {
   )
 }
 
-const WaitingGuests = ({ persons }: GuestProps) => {
+function WaitingGuests({ persons }: GuestProps) {
   const [waitingExpanded, setWaitingExpanded] = useState(false)
 
   // Show guests that have completed the registration but are not verified yet
@@ -610,6 +806,7 @@ const WaitingGuests = ({ persons }: GuestProps) => {
           guests={guests}
           emptyText={t('common:noWaitingGuests')}
           marginWidth={650}
+          unitFilters={[]}
         />
       </AccordionDetails>
     </StyledAccordion>
-- 
GitLab