Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/pages/dash/a/[token]/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -91,6 +92,7 @@ export default function AdminDashboard() {
<ActionButton icon={Bolt} href={`/dash/a/${query.token}/match`}>Match Board</ActionButton>
<ActionButton icon={UiUpload} onClick={() => studentsUploadRef.current?.click()}>Bulk Import Students</ActionButton>
<ActionButton icon={UiUpload} onClick={() => matchesUploadRef.current?.click()}>Import Matches</ActionButton>
<ActionButton icon={Survey} href={`/dash/a/${query.token}/surveys`}>Surveys</ActionButton>
</Grid>
</Content>
</Page>
Expand Down
24 changes: 24 additions & 0 deletions src/pages/dash/a/[token]/surveys.gql
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
146 changes: 146 additions & 0 deletions src/pages/dash/a/[token]/surveys.js
Original file line number Diff line number Diff line change
@@ -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 (
<Box mb={10}>
<Heading as="h4" fontSize="xl" mb={3}>{survey.name}</Heading>

<Accordion allowMultiple mb={4}>
<AccordionItem>
<AccordionButton>
<Box flex="1" textAlign="left">Preview Questions</Box>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4}>
{SCHEMA_SECTIONS.every(({ key }) => !hasSchema(survey[`${key}Schema`])) && (
<Text color="gray.500">No questions configured.</Text>
)}
{SCHEMA_SECTIONS.filter(({ key }) => hasSchema(survey[`${key}Schema`])).map(({ key, title }) => (
<Box key={key} mb={6} bgColor={"bg"}>
<Heading as="h5" fontSize="lg" mb={2}>{title}</Heading>
<RsjForm
schema={fillNameTemplate(survey[`${key}Schema`])}
uiSchema={fillNameTemplate(survey[`${key}Ui`])}
formData={{}}
onChange={() => {}}
children={true}
/>
</Box>
))}
</AccordionPanel>
</AccordionItem>
</Accordion>

{relevant ? (
<Text mb={2}>
{relevant.label} occurrence — Sent {DateTime.fromISO(relevant.occurrence.visibleAt).toLocaleString(DateTime.DATETIME_MED)}
{' — '}
Due {DateTime.fromISO(relevant.occurrence.dueAt).toLocaleString(DateTime.DATETIME_MED)}
</Text>
) : (
<Text mb={2} color="gray.500">No occurrences yet.</Text>
)}

<Button as="a" href={`/dash/a/${token}/surveys/${survey.id}`}>
View all occurrences &amp; responses ({survey.occurrences.length}) &raquo;
</Button>
</Box>
);
}

export default function AdminSurveys({ surveys }) {
const { query } = useRouter();

return (
<Page title="Surveys">
<Content mt={-8}>
<Button as="a" href={`/dash/a/${query.token}`}>&laquo; Back</Button>
<Heading as="h2" fontSize="5xl" mb={8} mt={4}>Surveys</Heading>

{surveys.length === 0 && <Text>No surveys are configured for this session.</Text>}

{groupByPersonType(surveys).map(([personType, group]) => (
<Box key={personType} mb={12}>
<Heading as="h3" fontSize="2xl" mb={4} pb={2} borderBottomWidth={2}>
{personTypeLabel(personType)}
</Heading>
{group.map((survey) => (
<SurveyCard key={survey.id} survey={survey} token={query.token} />
))}
</Box>
))}
</Content>
</Page>
);
}

export async function getServerSideProps({ params: { token } }) {
const res = await apiFetch(GetSurveysQuery, {}, { 'X-Labs-Authorization': `Bearer ${token}` });
return {
props: {
surveys: res.labs.surveys,
},
};
}
22 changes: 22 additions & 0 deletions src/pages/dash/a/[token]/surveys/[id].gql
Original file line number Diff line number Diff line change
@@ -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 }
}
}
}
}
}
107 changes: 107 additions & 0 deletions src/pages/dash/a/[token]/surveys/[id].js
Original file line number Diff line number Diff line change
@@ -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 (
<Page title="Survey">
<Content mt={-8}>
<Button as="a" href={`/dash/a/${query.token}/surveys`}>&laquo; Back</Button>
<Text mt={4}>Survey not found.</Text>
</Content>
</Page>
);
}

return (
<Page title={survey.name}>
<Content mt={-8}>
<Button as="a" href={`/dash/a/${query.token}/surveys`}>&laquo; Back</Button>
<Heading as="h2" fontSize="5xl" mb={8} mt={4}>
{survey.name}
<Badge ml={3} verticalAlign="middle" fontSize="lg">{survey.personType}</Badge>
</Heading>

{survey.occurrences.length === 0 && <Text color="gray.500">No occurrences yet.</Text>}

<Accordion allowMultiple>
{[...survey.occurrences]
.sort((a, b) => DateTime.fromISO(b.dueAt) - DateTime.fromISO(a.dueAt))
.map((occ) => (
<AccordionItem key={occ.id}>
<AccordionButton>
<Box flex="1" textAlign="left">
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'}
</Box>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4}>
{occ.surveyResponses.length === 0 ? (
<Text color="gray.500">No responses yet.</Text>
) : (
<Accordion allowMultiple>
{occ.surveyResponses.map((sr) => (
<AccordionItem key={sr.id}>
<AccordionButton {...getCautionColors(sr.caution)}>
<Box flex="1" textAlign="left">{responseLabel(sr)}</Box>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4}>
<SurveyDetails token={query.token} id={sr.id} />
</AccordionPanel>
</AccordionItem>
))}
</Accordion>
)}
</AccordionPanel>
</AccordionItem>
))}
</Accordion>
</Content>
</Page>
);
}

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,
},
};
}