From b3125bd77eebd706f0a926f830ff8bec5d0e0910 Mon Sep 17 00:00:00 2001 From: Lola Date: Fri, 21 Aug 2026 17:22:49 -0500 Subject: [PATCH] draft survey + response view page for admins --- src/pages/dash/a/[token]/index.js | 2 + src/pages/dash/a/[token]/surveys.gql | 24 ++++ src/pages/dash/a/[token]/surveys.js | 146 ++++++++++++++++++++++ src/pages/dash/a/[token]/surveys/[id].gql | 22 ++++ src/pages/dash/a/[token]/surveys/[id].js | 107 ++++++++++++++++ 5 files changed, 301 insertions(+) create mode 100644 src/pages/dash/a/[token]/surveys.gql create mode 100644 src/pages/dash/a/[token]/surveys.js create mode 100644 src/pages/dash/a/[token]/surveys/[id].gql create mode 100644 src/pages/dash/a/[token]/surveys/[id].js diff --git a/src/pages/dash/a/[token]/index.js b/src/pages/dash/a/[token]/index.js index e531f47..43297d4 100644 --- a/src/pages/dash/a/[token]/index.js +++ b/src/pages/dash/a/[token]/index.js @@ -12,6 +12,7 @@ import HeadGroup from '@codeday/topocons/Icon/HeadGroup'; import UiWarning from '@codeday/topocons/Icon/UiWarning'; import ShootingStar from '@codeday/topocons/Icon/ShootingStar'; import UiUpload from '@codeday/topocons/Icon/UiUpload'; +import Survey from '@codeday/topocons/Icon/Survey'; import Page from '../../../../components/Page'; import { Icon } from '@chakra-ui/react'; import { BulkImportStudents, ImportMatches } from './index.gql'; @@ -91,6 +92,7 @@ export default function AdminDashboard() { Match Board studentsUploadRef.current?.click()}>Bulk Import Students matchesUploadRef.current?.click()}>Import Matches + Surveys diff --git a/src/pages/dash/a/[token]/surveys.gql b/src/pages/dash/a/[token]/surveys.gql new file mode 100644 index 0000000..a2c39dd --- /dev/null +++ b/src/pages/dash/a/[token]/surveys.gql @@ -0,0 +1,24 @@ +query GetSurveysQuery { + labs { + surveys { + id + name + personType + selfSchema + selfUi + peerSchema + peerUi + menteeSchema + menteeUi + mentorSchema + mentorUi + projectSchema + projectUi + occurrences { + id + visibleAt + dueAt + } + } + } +} diff --git a/src/pages/dash/a/[token]/surveys.js b/src/pages/dash/a/[token]/surveys.js new file mode 100644 index 0000000..a561fe4 --- /dev/null +++ b/src/pages/dash/a/[token]/surveys.js @@ -0,0 +1,146 @@ +import { useRouter } from 'next/router'; +import { apiFetch } from '@codeday/topo/utils'; +import { Content } from '@codeday/topo/Molecule'; +import { Box, Button, Heading, Text } from '@codeday/topo/Atom'; +import { + Accordion, + AccordionButton, + AccordionIcon, + AccordionItem, + AccordionPanel, +} from '@chakra-ui/react'; +import { DateTime } from 'luxon'; +import Page from '../../../../components/Page'; +import RsjForm from '../../../../components/RsjForm'; +import { GetSurveysQuery } from './surveys.gql'; + +const SCHEMA_SECTIONS = [ + { key: 'self', title: 'Self-Reflection' }, + { key: 'peer', title: 'Peer Reflection (about a teammate)' }, + { key: 'mentee', title: 'Mentee Reflection (mentor about a student)' }, + { key: 'mentor', title: 'Mentor Reflection (student about a mentor)' }, + { key: 'project', title: 'Project Reflection' }, +]; + +function fillNameTemplate(schema) { + return JSON.parse(JSON.stringify(schema || {}).replace(/{{name}}/g, 'this person')); +} + +function hasSchema(schema) { + return schema && Object.keys(schema).length > 0; +} + +function getRelevantOccurrence(occurrences) { + if (!occurrences || occurrences.length === 0) return null; + const now = DateTime.now(); + const sorted = [...occurrences].sort((a, b) => DateTime.fromISO(a.dueAt) - DateTime.fromISO(b.dueAt)); + const next = sorted.find((occ) => DateTime.fromISO(occ.dueAt) >= now); + if (next) return { occurrence: next, label: 'Next' }; + return { occurrence: sorted[sorted.length - 1], label: 'Last' }; +} + +const PERSON_TYPE_ORDER = ['STUDENT', 'MENTOR']; + +function personTypeLabel(personType) { + return `${personType.charAt(0)}${personType.slice(1).toLowerCase()} Surveys`; +} + +function groupByPersonType(surveys) { + const groups = new Map(); + surveys.forEach((survey) => { + if (!groups.has(survey.personType)) groups.set(survey.personType, []); + groups.get(survey.personType).push(survey); + }); + return [...groups.entries()].sort(([a], [b]) => { + const ai = PERSON_TYPE_ORDER.indexOf(a); + const bi = PERSON_TYPE_ORDER.indexOf(b); + if (ai === -1 && bi === -1) return a.localeCompare(b); + if (ai === -1) return 1; + if (bi === -1) return -1; + return ai - bi; + }); +} + +function SurveyCard({ survey, token }) { + const relevant = getRelevantOccurrence(survey.occurrences); + return ( + + {survey.name} + + + + + Preview Questions + + + + {SCHEMA_SECTIONS.every(({ key }) => !hasSchema(survey[`${key}Schema`])) && ( + No questions configured. + )} + {SCHEMA_SECTIONS.filter(({ key }) => hasSchema(survey[`${key}Schema`])).map(({ key, title }) => ( + + {title} + {}} + children={true} + /> + + ))} + + + + + {relevant ? ( + + {relevant.label} occurrence — Sent {DateTime.fromISO(relevant.occurrence.visibleAt).toLocaleString(DateTime.DATETIME_MED)} + {' — '} + Due {DateTime.fromISO(relevant.occurrence.dueAt).toLocaleString(DateTime.DATETIME_MED)} + + ) : ( + No occurrences yet. + )} + + + + ); +} + +export default function AdminSurveys({ surveys }) { + const { query } = useRouter(); + + return ( + + + + Surveys + + {surveys.length === 0 && No surveys are configured for this session.} + + {groupByPersonType(surveys).map(([personType, group]) => ( + + + {personTypeLabel(personType)} + + {group.map((survey) => ( + + ))} + + ))} + + + ); +} + +export async function getServerSideProps({ params: { token } }) { + const res = await apiFetch(GetSurveysQuery, {}, { 'X-Labs-Authorization': `Bearer ${token}` }); + return { + props: { + surveys: res.labs.surveys, + }, + }; +} diff --git a/src/pages/dash/a/[token]/surveys/[id].gql b/src/pages/dash/a/[token]/surveys/[id].gql new file mode 100644 index 0000000..fd00a40 --- /dev/null +++ b/src/pages/dash/a/[token]/surveys/[id].gql @@ -0,0 +1,22 @@ +query GetSurveyQuery($id: String!) { + labs { + survey(survey: $id) { + id + name + personType + occurrences { + id + visibleAt + dueAt + surveyResponses { + id + caution + authorMentor { id name } + authorStudent { id name } + mentor { id name } + student { id name } + } + } + } + } +} diff --git a/src/pages/dash/a/[token]/surveys/[id].js b/src/pages/dash/a/[token]/surveys/[id].js new file mode 100644 index 0000000..65923b0 --- /dev/null +++ b/src/pages/dash/a/[token]/surveys/[id].js @@ -0,0 +1,107 @@ +import { useRouter } from 'next/router'; +import { apiFetch } from '@codeday/topo/utils'; +import { Content } from '@codeday/topo/Molecule'; +import { Box, Button, Heading, Text } from '@codeday/topo/Atom'; +import { + Accordion, + AccordionButton, + AccordionIcon, + AccordionItem, + AccordionPanel, + Badge, +} from '@chakra-ui/react'; +import { DateTime } from 'luxon'; +import Page from '../../../../../components/Page'; +import SurveyDetails from '../../../../../components/Dashboard/SurveyDetails'; +import { GetSurveyQuery } from './[id].gql'; + +function getCautionColors(caution) { + if (caution > 0.9) return { bg: 'red.500', color: 'red.50' }; + if (caution > 0.1) return { bg: 'orange.500', color: 'orange.50' }; + return {}; +} + +function responseLabel(sr) { + const author = sr.authorMentor || sr.authorStudent; + const target = sr.student || sr.mentor; + if (!author) return 'Unknown'; + if (target && target.id === author.id) return `${author.name} (Self-Reflection)`; + if (target) return `${author.name} on ${target.name}`; + return author.name; +} + +export default function AdminSurveyDetail({ survey }) { + const { query } = useRouter(); + + if (!survey) { + return ( + + + + Survey not found. + + + ); + } + + return ( + + + + + {survey.name} + {survey.personType} + + + {survey.occurrences.length === 0 && No occurrences yet.} + + + {[...survey.occurrences] + .sort((a, b) => DateTime.fromISO(b.dueAt) - DateTime.fromISO(a.dueAt)) + .map((occ) => ( + + + + Sent {DateTime.fromISO(occ.visibleAt).toLocaleString(DateTime.DATETIME_MED)} + {' — '} + Due {DateTime.fromISO(occ.dueAt).toLocaleString(DateTime.DATETIME_MED)} + {' — '} + {occ.surveyResponses.length} response{occ.surveyResponses.length === 1 ? '' : 's'} + + + + + {occ.surveyResponses.length === 0 ? ( + No responses yet. + ) : ( + + {occ.surveyResponses.map((sr) => ( + + + {responseLabel(sr)} + + + + + + + ))} + + )} + + + ))} + + + + ); +} + +export async function getServerSideProps({ params: { token, id } }) { + const res = await apiFetch(GetSurveyQuery, { id }, { 'X-Labs-Authorization': `Bearer ${token}` }); + return { + props: { + survey: res.labs.survey || null, + }, + }; +}