diff --git a/backend/endpoints/hpcQuestion/controller/getRandomQuestion.js b/backend/endpoints/hpcQuestion/controller/getRandomQuestion.js index dfa34da..2d6d60e 100644 --- a/backend/endpoints/hpcQuestion/controller/getRandomQuestion.js +++ b/backend/endpoints/hpcQuestion/controller/getRandomQuestion.js @@ -9,7 +9,7 @@ module.exports = (db) => { */ return async (req, res) => { try { - if (Math.random() >= 0.35) { + if (Math.random() >= 0.25) { return res.status(200).json({ success: true, body: null diff --git a/frontend/app/components/ClusterSettings.jsx b/frontend/app/components/ClusterSettings.jsx index 589578e..d4e1787 100644 --- a/frontend/app/components/ClusterSettings.jsx +++ b/frontend/app/components/ClusterSettings.jsx @@ -359,27 +359,23 @@ export default function ClusterSettingsPage() { return (
-
-
+
+
{/* Header */} -
-
-
- +

{cluster.name}

Cluster settings and overview

{!hasChecks ? ( -
+
No checks have been performed for this cluster
) : ( -
+
{status ? ( Status: Healthy @@ -398,7 +394,7 @@ export default function ClusterSettingsPage() {
)} -
+
id: {clusterId} diff --git a/frontend/app/components/Form.jsx b/frontend/app/components/Form.jsx index c40f84e..16f853d 100644 --- a/frontend/app/components/Form.jsx +++ b/frontend/app/components/Form.jsx @@ -542,17 +542,10 @@ export default function Form() { return (
-
-
+
+ {/* Header */} -
-
-
- +

Process Documentation

@@ -1015,7 +1008,7 @@ export default function Form() { type="submit" disabled={submitting || steps.length === 0 || !canSubmit} aria-disabled={submitting || steps.length === 0} - className="mt-10 w-full cursor-pointer rounded-2xl border border-blue-200/25 bg-gradient-to-r from-blue-500 via-blue-500 to-indigo-600 py-4 text-lg font-semibold text-white shadow-xl shadow-blue-950/30 transition duration-200 hover:-translate-y-0.5 hover:border-blue-100/40 hover:from-blue-400 hover:via-blue-500 hover:to-indigo-500 focus:outline-none focus:ring-2 focus:ring-blue-200/60 focus:ring-offset-2 focus:ring-offset-slate-950 active:translate-y-0 disabled:cursor-not-allowed disabled:translate-y-0 disabled:opacity-50" + className="mt-10 w-full cursor-pointer rounded-4xl border border-slate-200/25 bg-gradient-to-r from-blue-500 via-blue-500 to-indigo-600 py-4 text-lg font-semibold text-white shadow-xl shadow-blue-950/30 transition duration-200 hover:-translate-y-0.5 hover:border-blue-100/40 hover:from-blue-400 hover:via-blue-500 hover:to-indigo-500 focus:outline-none focus:ring-2 focus:ring-blue-200/60 focus:ring-offset-2 focus:ring-offset-slate-950 active:translate-y-0 disabled:cursor-not-allowed disabled:translate-y-0 disabled:opacity-50" > {submitting ? 'Submitting...' : 'Submit Report'} diff --git a/frontend/app/components/Header.jsx b/frontend/app/components/Header.jsx index e7df27f..87ac385 100644 --- a/frontend/app/components/Header.jsx +++ b/frontend/app/components/Header.jsx @@ -102,8 +102,8 @@ export default function Header() { return (
-
-
+
+
-
-
+
+
+
{/* Header */} -
-
+
+

+ Portal -

- -

Portal

+

Access schedules and submit reports in one place.

-
- {/* Schedule */} - -
diff --git a/frontend/app/components/Name.jsx b/frontend/app/components/Name.jsx index 76f48d8..f7c81ec 100644 --- a/frontend/app/components/Name.jsx +++ b/frontend/app/components/Name.jsx @@ -10,8 +10,20 @@ import { IoIosArrowForward } from 'react-icons/io'; export default function Name() { const [people, setPeople] = useState([]); const [selectedId, setSelectedId] = useState(''); + const [teams, setTeams] = useState([]); const router = useRouter(); + const [selectedTeamId, setSelectedTeamId] = useState(''); + const [peopleInTeam, setPeopleInTeam] = useState([]); + + const colours = [ + 'from-blue-300 via-blue-500 to-blue-900', + 'from-emerald-300 via-emerald-500 to-emerald-900', + 'from-purple-300 via-purple-500 to-purple-900', + 'from-pink-300 via-pink-500 to-pink-900', + 'from-amber-300 via-amber-500 to-amber-900' + ]; + useEffect(() => { async function getName() { const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/people`); @@ -21,28 +33,47 @@ export default function Name() { getName(); }, []); + useEffect(() => { + async function getTeams() { + const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/teams`); + const data = await res.json(); + setTeams(data.body); + } + getTeams(); + }, []); + + const handleTeamSelect = (e) => { + e.preventDefault(); + + const teamId = e.currentTarget.value; + const matches = people.filter((p) => p.teamId === teamId); + + setSelectedTeamId(teamId); + setPeopleInTeam(matches); + + console.log(matches); + }; + + const handleBackToTeams = () => { + setSelectedTeamId(''); + setPeopleInTeam([]); + }; + const handleSubmit = (e) => { e.preventDefault(); - Cookies.set('selectedPersonId', selectedId); + const personId = e.currentTarget.value; + + Cookies.set('selectedPersonId', personId); router.push('/personalSchedule'); }; return (
-
+
{/* Header */} -
-
-
- -

Welcome

- -

Select your name to continue.

-
- + {/*
- + */} + + {!selectedTeamId ? ( +
+
+

+ Select your team. +

+ +

Select your team to continue.

+
+ +
+ {teams.map((t) => { + const colour = colours[Math.floor(Math.random() * colours.length)]; + + return ( + + ); + })} +
+
+ ) : ( +
+
+

+ Select your name. +

+ +

Select your name to continue.

+
+ +
+ {peopleInTeam.map((p) => { + const colour = colours[Math.floor(Math.random() * colours.length)]; + + return ( + + ); + })} +
+ +
+ )}
diff --git a/frontend/app/components/Options.jsx b/frontend/app/components/Options.jsx index 4febf3f..08f4c5a 100644 --- a/frontend/app/components/Options.jsx +++ b/frontend/app/components/Options.jsx @@ -386,14 +386,10 @@ export default function Options() { return (
-
-
+
+
{/* Header */} -
-
-
- +

Administration

diff --git a/frontend/app/components/Overview.jsx b/frontend/app/components/Overview.jsx index d82203a..0380030 100644 --- a/frontend/app/components/Overview.jsx +++ b/frontend/app/components/Overview.jsx @@ -1,7 +1,26 @@ 'use client'; -import { GrOverview } from 'react-icons/gr'; import { useState, useEffect } from 'react'; +import DatePicker from 'react-datepicker'; +import { FaCalendarAlt } from 'react-icons/fa'; + +function dateToInputValue(date) { + if (!date) return ''; + + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + + return `${year}-${month}-${day}`; +} + +function inputValueToDate(value) { + if (!value) return null; + + const [year, month, day] = value.split('-').map(Number); + + return new Date(year, month - 1, day); +} export default function Overview() { const [date, setDate] = useState(''); @@ -52,6 +71,9 @@ export default function Overview() { loadReportFromDate(); }, [date]); + useEffect(() => { + console.log(report); + }, [report]); // Calculate duration function calculateDuration(start, end) { const diffMs = end.getTime() - start.getTime(); @@ -75,65 +97,52 @@ export default function Overview() { // Page return (

-
-
+
+
{/* Header */} -
-
-
- +

Reports Overview

- View a break down of a days reports. + View a break down of a day's reports.

- {/* Day picker */} -
-
-
-

Custom Date Range

-

- Leave empty to use the current week. -

-
+

+ Select a date and view the status of all reports for that day: +

- {date && ( - - )} -
+
+
+
+
+
+
+ + setDate(dateToInputValue(selectedDate)) + } -
- setDate(e.target.value)} - className={`block w-full min-w-0 max-w-[15rem] rounded-lg border border-slate-600 bg-slate-800/80 px-2.5 py-2 text-xs [color-scheme:dark] sm:max-w-full sm:rounded-xl sm:px-4 sm:py-3 sm:text-base ${ - date ? 'text-white' : 'text-transparent sm:text-white' - }`} - /> - {!date && ( - - Start date - - )} + dateFormat="dd/MM/yyyy" + placeholderText={new Date().toLocaleDateString('en-GB')} + className="w-full rounded-full cursor-pointer border border-white/10 bg-slate-900 px-4 py-3 text-white outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-500/30" + calendarClassName="results-datepicker" + popperClassName="z-50" + /> + +
+
+
+
{/* Body */} -
+
{!report.id ? (

- No overview to available + No overview available

) : (
diff --git a/frontend/app/components/PersonalSchedule.jsx b/frontend/app/components/PersonalSchedule.jsx index 89939b5..de4f58b 100644 --- a/frontend/app/components/PersonalSchedule.jsx +++ b/frontend/app/components/PersonalSchedule.jsx @@ -209,14 +209,10 @@ export default function PersonalSchedule() { return (
-
-
+
+
{/* Header */} -
-
-
- +

{name}'s Clusters

diff --git a/frontend/app/components/PoolSettingsPage.jsx b/frontend/app/components/PoolSettingsPage.jsx index 7464428..86027eb 100644 --- a/frontend/app/components/PoolSettingsPage.jsx +++ b/frontend/app/components/PoolSettingsPage.jsx @@ -137,15 +137,9 @@ export default function PoolClustersPage() { return (
-
- -
-
-
-
-
- +
+
+

{pool.name}

@@ -181,7 +175,7 @@ export default function PoolClustersPage() { type="button" disabled={!selectedClusterId} onClick={handleAddCluster} - className={`rounded-xl px-5 py-3 font-semibold text-white transition ${ + className={`rounded-xl px-5 py-3 cursor-pointer font-semibold text-white transition ${ selectedClusterId ? 'bg-blue-600 hover:bg-blue-500' : 'cursor-not-allowed bg-slate-600 opacity-50' @@ -202,7 +196,7 @@ export default function PoolClustersPage() { key={cluster.id} type="button" onClick={() => setSelectedClusterId(selected ? '' : cluster.id)} - className={`rounded-2xl border p-5 text-left transition ${ + className={`rounded-2xl border cursor-pointer p-5 text-left transition ${ selected ? 'border-blue-400 bg-blue-500/20 shadow-lg shadow-blue-500/20' : 'border-white/10 bg-slate-800/70 hover:border-blue-300/50 hover:bg-slate-700/70' diff --git a/frontend/app/components/Report.jsx b/frontend/app/components/Report.jsx index d6d4308..cd0e814 100644 --- a/frontend/app/components/Report.jsx +++ b/frontend/app/components/Report.jsx @@ -105,8 +105,8 @@ export default function Report() { if (!report) { return (

-
-
+
+

Report Not Found

@@ -129,8 +129,8 @@ export default function Report() { return (
-
-
+
+
{/* Header */}
@@ -306,16 +306,12 @@ export default function Report() {

Tester

-

- {report.person} #{report.personId} -

+

{report.person}

Cluster

-

- {report.cluster} #{report.clusterId} -

+

{report.cluster}

diff --git a/frontend/app/components/Results.jsx b/frontend/app/components/Results.jsx index d2666e4..0bf6edb 100644 --- a/frontend/app/components/Results.jsx +++ b/frontend/app/components/Results.jsx @@ -3,8 +3,37 @@ import { useEffect, useMemo, useState } from 'react'; import { useRouter } from 'next/navigation'; import { Listbox, ListboxButton, ListboxOptions, ListboxOption } from '@headlessui/react'; -import { FaChartBar, FaDatabase, FaUser } from 'react-icons/fa'; +import { FaDatabase, FaUser } from 'react-icons/fa'; import { IoIosArrowForward } from 'react-icons/io'; +import DatePicker from 'react-datepicker'; +import { FaCalendarAlt } from 'react-icons/fa'; + +function dateToInputValue(date) { + if (!date) return ''; + + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + + return `${year}-${month}-${day}`; +} + +function inputValueToDate(value) { + if (!value) return null; + + const [year, month, day] = value.split('-').map(Number); + + return new Date(year, month - 1, day); +} + +function addDays(date, days) { + if (!date) return null; + + const nextDate = new Date(date); + nextDate.setDate(nextDate.getDate() + days); + + return nextDate; +} export default function ResultsPage() { const router = useRouter(); @@ -45,45 +74,47 @@ export default function ResultsPage() { loadClusters(); }, []); - // ---------------------------- - // Reset when switching MODE - // ---------------------------- - useEffect(() => { + function resetResults() { setPage(1); setReports([]); setPagination(null); + } + + function handleModeChange(nextMode) { + if (nextMode === mode) return; - if (mode === 'week') { + setMode(nextMode); + resetResults(); + setStartDate(''); + setEndDate(''); + + if (nextMode === 'week') { setSelectedClusterId(null); - setStartDate(''); - setEndDate(''); } + } - if (mode === 'cluster') { - setStartDate(''); - setEndDate(''); - } - }, [mode]); + function handleClusterChange(clusterId) { + setSelectedClusterId(clusterId); + resetResults(); + setStartDate(''); + setEndDate(''); + } - // ---------------------------- - // Reset when switching cluster - // ---------------------------- - useEffect(() => { - if (mode === 'cluster') { - setPage(1); - setReports([]); - setPagination(null); - setStartDate(''); - setEndDate(''); - } - }, [mode, selectedClusterId]); + function handleClusterStartDateChange(date) { + setStartDate(dateToInputValue(date)); + setPage(1); + } - // ---------------------------- - // Reset page when date changes - // ---------------------------- - useEffect(() => { + function handleClusterEndDateChange(date) { + setEndDate(dateToInputValue(date)); + setPage(1); + } + + function handleWeekStartDateChange(date) { + setStartDate(dateToInputValue(date)); + setEndDate(dateToInputValue(addDays(date, 6))); setPage(1); - }, [startDate, endDate, mode]); + } // ---------------------------- // Fetch reports @@ -172,24 +203,20 @@ export default function ResultsPage() { return (
-
-
+
+
{/* HEADER */} -
-
-
+
+

Report Explorer

-

Report Explorer

- -

Cluster or weekly report view

+

Find reports by cluster or by week.

{/* MODE TOGGLE */}
-
+
-
-
-
- {mode !== 'cluster' ? ( -
-

Custom Date Range

-

- Leave empty to use the current week. -

-
- ) : ( -
-

- Cluster Date Range (optional) -

-

- Filter cluster reports by date range -

-
- )} -
- - {(startDate || endDate) && ( - - )} -
- -
-
- setStartDate(e.target.value)} - className={`block w-full min-w-0 max-w-[15rem] rounded-lg border border-slate-600 bg-slate-800/80 px-2.5 py-2 text-xs [color-scheme:dark] sm:max-w-full sm:rounded-xl sm:px-4 sm:py-3 sm:text-base ${ - startDate ? 'text-white' : 'text-transparent sm:text-white' - }`} - /> - {!startDate && ( - - Start date - - )} -
+ {mode === 'cluster' ? ( +
+
+ {/* Cluster */} +
+ + + +
+ + {selectedCluster?.name ?? 'Select a cluster...'} + + + ▼ + + + + + {clusters + .filter((c) => c.name) + .map((cluster) => ( + + {cluster.name} + + ))} + +
+
+
-
- setEndDate(e.target.value)} - className={`block w-full min-w-0 max-w-[15rem] rounded-lg border border-slate-600 bg-slate-800/80 px-2.5 py-2 text-xs [color-scheme:dark] sm:max-w-full sm:rounded-xl sm:px-4 sm:py-3 sm:text-base ${ - endDate ? 'text-white' : 'text-transparent sm:text-white' - }`} - /> - {!endDate && ( - - End date - - )} + {/* Date range */} +
+ + +
+
+
+ + +
+
+ +
+
+ + + +
+
+
+
-
- - {/* CLUSTER SELECT */} - {mode === 'cluster' && ( -
- - - -
- - - {selectedCluster?.name || 'Select a cluster...'} - - - - ▼ - - - - - {clusters.map((c) => ( - - {c.name} - - ))} - + ) : ( + <> +

+ Select a start date and get all results for the next 7 days: +

+ +
+
+
+
+
+
+ + +
+
+
+
- -
+
+ )} {/* LOADING */} - {loading &&

Loading reports...

} + {loading &&

Loading reports...

} {/* EMPTY STATE */} {!loading && reports.length === 0 && ( -

No reports found

+

No reports found

)} {/* RESULTS */} -
+
{Object.entries(grouped).map(([date, items]) => (
router.push(`/report?id=${r.id}`)} className={[ - 'w-full group text-left', - 'rounded-xl border px-3 py-3 sm:px-4 sm:py-4', + 'group w-full text-left', + 'rounded-3xl border px-3 py-3 sm:px-4 sm:py-4', 'transition-all duration-200', - 'hover:shadow-lg hover:-translate-y-[1px] cursor-pointer', + 'cursor-pointer hover:-translate-y-[1px] hover:shadow-lg', 'active:scale-[0.99]', passed ? 'border-green-400/30 bg-green-500/20' @@ -364,7 +397,7 @@ export default function ResultsPage() {
@@ -377,7 +410,7 @@ export default function ResultsPage() { {mode === 'week' && ( - +
diff --git a/frontend/app/components/Schedule.jsx b/frontend/app/components/Schedule.jsx index 5c44888..5310215 100644 --- a/frontend/app/components/Schedule.jsx +++ b/frontend/app/components/Schedule.jsx @@ -207,7 +207,7 @@ export default function Schedule() { return (
-
+
{/* Header */}
@@ -318,7 +318,7 @@ export default function Schedule() { {/* Day header */}
diff --git a/frontend/app/components/TeamsSettingsPage.jsx b/frontend/app/components/TeamsSettingsPage.jsx index d915089..21a16b2 100644 --- a/frontend/app/components/TeamsSettingsPage.jsx +++ b/frontend/app/components/TeamsSettingsPage.jsx @@ -7,6 +7,25 @@ import { Listbox, ListboxButton, ListboxOptions, ListboxOption } from '@headless import { FaLayerGroup, FaUser, FaUsers } from 'react-icons/fa'; import { IoIosSettings } from 'react-icons/io'; +function timeInputToInt(value) { + if (!value) return null; + + const [hours, minutes] = value.split(':').map(Number); + + if ( + Number.isNaN(hours) || + Number.isNaN(minutes) || + hours < 0 || + hours > 23 || + minutes < 0 || + minutes > 59 + ) { + return null; + } + + return hours + minutes / 60; +} + function timeNumberToInput(value) { const numericValue = Number(value); @@ -373,15 +392,9 @@ export default function TeamSettingsPage() { return (
-
- -
-
-
-
-
- +
+
+

{team.name}

@@ -540,7 +553,7 @@ export default function TeamSettingsPage() { key={pool.id} className="flex items-center justify-between rounded-xl border border-slate-600 bg-slate-800/80 px-4 py-3 text-white" > -

+
{/* Full card click target */} -

+

{pool.name}

@@ -569,7 +582,7 @@ export default function TeamSettingsPage() { href={`/clusters?id=${cluster.id}`} className="relative z-20 pointer-events-auto w-fit" > - + {cluster.name} @@ -578,7 +591,7 @@ export default function TeamSettingsPage() {
)} -

+

View pool settings

@@ -586,7 +599,7 @@ export default function TeamSettingsPage() { diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 21b7a06..c9c981a 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -1,4 +1,5 @@ @import 'tailwindcss'; +@import 'react-datepicker/dist/react-datepicker.css'; @plugin "@tailwindcss/typography"; :root { @@ -25,3 +26,61 @@ body { color: var(--foreground); font-family: Arial, Helvetica, sans-serif; } + +@keyframes fadeUp { + 0% { + opacity: 0; + transform: translateY(16px) scale(0.98); + filter: blur(6px); + } + + 100% { + opacity: 1; + transform: translateY(0) scale(1); + filter: blur(0); + } +} + +.animate-fade-up { + animation: fadeUp 1000ms ease-out both; +} + +.results-datepicker { + border: 1px solid rgba(255, 255, 255, 0.1) !important; + border-radius: 0.75rem !important; + background: rgb(15 23 42) !important; + color: blue !important; + box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.5); + overflow: hidden; +} + +.results-datepicker .react-datepicker__header { + background: rgb(15 23 42) !important; + border-bottom: 1px solid rgba(255, 255, 255, 0.1) !important; +} + +.results-datepicker .react-datepicker__current-month, +.results-datepicker .react-datepicker__day-name, +.results-datepicker .react-datepicker__day { + color: rgb(105, 159, 229) !important; +} + +.results-datepicker .react-datepicker__day:hover { + background: rgba(37, 99, 235, 0.25) !important; + border-radius: 9999px; +} + +.results-datepicker .react-datepicker__day--selected, +.results-datepicker .react-datepicker__day--keyboard-selected { + background: rgb(8, 0, 163) !important; + color: white !important; + border-radius: 9999px; +} + +.results-datepicker .react-datepicker__day--disabled { + color: rgb(71 85 105) !important; +} + +.results-datepicker .react-datepicker__triangle { + display: none; +} diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 6e81fbe..b1f919e 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -29,8 +29,8 @@ export default function RootLayout({ children }: { children: React.ReactNode })
-
-
{children}
+
+
{children}
diff --git a/frontend/app/overview/page.tsx b/frontend/app/overview/page.tsx index 3cb6079..1e8873f 100644 --- a/frontend/app/overview/page.tsx +++ b/frontend/app/overview/page.tsx @@ -4,7 +4,7 @@ export const metadata = { title: 'Reports Overview' }; -export default function PersonalSchedulePage() { +export default function OverviewPage() { return (
diff --git a/frontend/public/images/test.jpg b/frontend/public/images/test.jpg new file mode 100644 index 0000000..e4f2873 Binary files /dev/null and b/frontend/public/images/test.jpg differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..1599b8f --- /dev/null +++ b/package.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "date-fns": "^4.4.0", + "react": "^19.2.7", + "react-date-range": "^2.0.1" + } +}