From 5c8571e57145ef69a20bd1020932cf1d25602f39 Mon Sep 17 00:00:00 2001 From: emmanuel Date: Sat, 21 Oct 2023 07:44:57 +0000 Subject: [PATCH] CLASSROOMIO-T-227: Exercise Statistics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLASSROOMIO-T-227: 🔥 🔥 🔥 🔥 Improvements CLASSROOMIO-T-227:Worked on the Exercise Statistics CLASSROOMIO-T-227:Worked on the Exercise Statistics CLASSROOMIO-T-227:Worked on the Exercise Statistics Co-authored-by: Rotimi Best Co-authored-by: Ifeanyi Nwosu Co-authored-by: Rotimi Best Merge-request: CLASSROOMIO-MR-111 Merged-by: Best Admin --- package.json | 4 + .../Apps/components/Comments/index.svelte | 5 +- .../Apps/components/Poll/service.ts | 87 +++++++++ src/lib/components/Apps/index.svelte | 2 +- .../Lesson/Exercise/Analytics/index.svelte | 31 ---- .../Exercise/Submissions/Individual.svelte | 83 +++++++++ .../Exercise/Submissions/Summary.svelte | 170 ++++++++++++++++++ .../Lesson/Exercise/Submissions/functions.ts | 66 +++++++ .../Lesson/Exercise/Submissions/index.svelte | 69 +++++++ .../Lesson/Exercise/Submissions/store.ts | 4 + .../components/Lesson/Exercise/index.svelte | 122 +++++++------ .../components/Lesson/Exercises/index.svelte | 26 +-- .../components/Lesson/store/exercise.js | 49 +++-- .../components/Org/Settings/Profile.svelte | 3 +- src/lib/utils/functions/course.ts | 13 ++ 15 files changed, 611 insertions(+), 123 deletions(-) create mode 100644 src/lib/components/Apps/components/Poll/service.ts delete mode 100644 src/lib/components/Course/components/Lesson/Exercise/Analytics/index.svelte create mode 100644 src/lib/components/Course/components/Lesson/Exercise/Submissions/Individual.svelte create mode 100644 src/lib/components/Course/components/Lesson/Exercise/Submissions/Summary.svelte create mode 100644 src/lib/components/Course/components/Lesson/Exercise/Submissions/functions.ts create mode 100644 src/lib/components/Course/components/Lesson/Exercise/Submissions/index.svelte create mode 100644 src/lib/components/Course/components/Lesson/Exercise/Submissions/store.ts diff --git a/package.json b/package.json index 9ebaff03..6352af29 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.386.0", + "@carbon/charts-svelte": "^1.13.6", "@sendgrid/mail": "^7.7.0", "@sentry/browser": "^7.61.0", "@sentry/integrations": "^7.61.0", @@ -32,6 +33,9 @@ "canvas-confetti": "^1.6.0", "cookie-parser": "^1.4.6", "copy-to-clipboard": "^3.3.3", + "d3": "^7.8.5", + "d3-cloud": "^1.2.7", + "d3-sankey": "^0.12.3", "form-data": "^4.0.0", "hotkeys-js": "^3.11.2", "js-yaml": "^4.1.0", diff --git a/src/lib/components/Apps/components/Comments/index.svelte b/src/lib/components/Apps/components/Comments/index.svelte index 440e1dff..c4060285 100644 --- a/src/lib/components/Apps/components/Comments/index.svelte +++ b/src/lib/components/Apps/components/Comments/index.svelte @@ -25,6 +25,7 @@ let comments: LessonComment[] = []; let groupmember: GroupPerson | undefined; let isSaving: boolean = false; + let isFetched = false; interface FetchComments { comment: string; @@ -155,6 +156,7 @@ console.error('error'); return; } + console.log('fetch comments', data); comments = data.map((lessonComment) => { return { @@ -167,6 +169,7 @@ : lessonComment.groupmember.profile.fullname }; }); + isFetched = true; scrollToBottom(); } @@ -185,7 +188,7 @@ supabase.removeChannel(lessonCommentChannel); }); - $: fetchComments($group.people, $lesson.id); + $: !isFetched && fetchComments($group.people, $lesson.id); diff --git a/src/lib/components/Apps/components/Poll/service.ts b/src/lib/components/Apps/components/Poll/service.ts new file mode 100644 index 00000000..2eec29c1 --- /dev/null +++ b/src/lib/components/Apps/components/Poll/service.ts @@ -0,0 +1,87 @@ +import { snackbar } from '$lib/components/Snackbar/store'; +import type { PollType } from './types'; +import { getSupabase } from '$lib/utils/functions/supabase'; +import type { GroupPerson } from '$lib/utils/types'; + +const supabase = getSupabase(); + +export async function createPoll(poll: PollType, groupmember: GroupPerson, courseId: string) { + if (!groupmember) return; + + const { data: newPolls, error } = await supabase + .from('apps_poll') + .insert({ + question: poll.question, + authorId: groupmember.id, + isPublic: poll.isPublic, + status: poll.question, + expiration: poll.expiration, + courseId + }) + .select(); + + if (!newPolls || error) { + snackbar.error('Error creating poll'); + console.error(error); + return; + } + + const [newPoll] = newPolls; + + if (!newPoll.id) return; + poll = { + poll, + ...newPoll + }; + + poll.options = await Promise.all( + poll.options.map(async (option) => { + const { data: newPollOptions, error: newPollOptionsError } = await supabase + .from('apps_poll_option') + .insert({ + poll_id: newPoll.id, + label: option.label + }) + .select(); + + if (!newPollOptions || newPollOptionsError) { + return option; + } + + const [newPollOption] = newPollOptions; + option.id = newPollOption.id; + + return option; + }) + ); + + return poll; +} + +export async function fetchPolls(courseId: string) { + const {} = await supabase + .from('app_poll') + .select( + ` + id, + question, + isPublic, + status, + expiration + groupmember:authorId( + profile!inner( + id, + fullname, + avatar_url + ) + ) + apps_poll_option!inner( + id, + label, + ) + ` + ) + .match({ + courseId + }); +} diff --git a/src/lib/components/Apps/index.svelte b/src/lib/components/Apps/index.svelte index e3288b44..085e4d9d 100644 --- a/src/lib/components/Apps/index.svelte +++ b/src/lib/components/Apps/index.svelte @@ -145,7 +145,7 @@ {#if !!$apps.selectedApp}
{#key $apps.selectedApp} diff --git a/src/lib/components/Course/components/Lesson/Exercise/Analytics/index.svelte b/src/lib/components/Course/components/Lesson/Exercise/Analytics/index.svelte deleted file mode 100644 index 7883cbcf..00000000 --- a/src/lib/components/Course/components/Lesson/Exercise/Analytics/index.svelte +++ /dev/null @@ -1,31 +0,0 @@ - - -{#if isLoading} - Loading data -{:else} - Analytics data -{/if} diff --git a/src/lib/components/Course/components/Lesson/Exercise/Submissions/Individual.svelte b/src/lib/components/Course/components/Lesson/Exercise/Submissions/Individual.svelte new file mode 100644 index 00000000..26357912 --- /dev/null +++ b/src/lib/components/Course/components/Lesson/Exercise/Submissions/Individual.svelte @@ -0,0 +1,83 @@ + + +{#if isLoading} + Loading data +{:else if $submissions?.length} +
+ {#each $submissions as student, i} + + {/each} +
+ +

+ {$submissions[studentSelected].submitted_by.profile.fullname}'s Individual Answers +

+ {#if $submissions[studentSelected]} + {#if $questionnaire.questions} + {#each $questionnaire.questions as q, i} +
+

{i + 1}. {q.title}

+ {#if q.question_type_id !== 3} + {#each q.options as option, i} +
+ +

{option.label}

+
+ {/each} + {/if} + {#if q.question_type_id === 3} +
+ {#if $submissions[studentSelected].answers.map((ans) => ans.open_answer)} +

+ {$submissions[studentSelected].answers.map((ans) => ans.open_answer)} +

+ {:else} +

No answer

+ {/if} +
+ {/if} +
+ {/each} + {/if} + {/if} +{/if} diff --git a/src/lib/components/Course/components/Lesson/Exercise/Submissions/Summary.svelte b/src/lib/components/Course/components/Lesson/Exercise/Submissions/Summary.svelte new file mode 100644 index 00000000..6b928104 --- /dev/null +++ b/src/lib/components/Course/components/Lesson/Exercise/Submissions/Summary.svelte @@ -0,0 +1,170 @@ + + +{#if isLoading} + Loading data +{:else if browser} +
+

Question Charts

+ {#each transformedQuestions as q} +
+

{q.title}

+ {#if q.type === 1} + + {:else if q.type === 2} + + {:else} +
+
    + {#each q.chartData as answer (answer)} + {#if answer.group} +
    +
  • {answer.group}
  • +
    + {/if} + {/each} +
+
+ {/if} +
+ {/each} +
+{/if} diff --git a/src/lib/components/Course/components/Lesson/Exercise/Submissions/functions.ts b/src/lib/components/Course/components/Lesson/Exercise/Submissions/functions.ts new file mode 100644 index 00000000..d6e0f6b1 --- /dev/null +++ b/src/lib/components/Course/components/Lesson/Exercise/Submissions/functions.ts @@ -0,0 +1,66 @@ +import { ScaleTypes, Alignments } from '@carbon/charts-svelte'; +import type { BarChartOptions, PieChartOptions } from '@carbon/charts-svelte'; + +export const pieOptions: PieChartOptions = { + resizable: true, + legend: { + alignment: Alignments.CENTER + }, + pie: { + alignment: Alignments.CENTER + }, + height: '400px' +}; + +export const barOptions: BarChartOptions = { + axes: { + left: { + mapsTo: 'value' + }, + bottom: { + mapsTo: 'group', + scaleType: ScaleTypes['LABELS'] + } + }, + height: '400px' +}; + +export function getChartOptions(isLoading: boolean) { + const barOptions: BarChartOptions = { + axes: { + left: { + mapsTo: 'value' + }, + bottom: { + mapsTo: 'group', + scaleType: ScaleTypes['LABELS'] + } + }, + height: '400px' + }; + + const pieOptions: PieChartOptions = { + resizable: true, + legend: { + alignment: Alignments.CENTER + }, + pie: { + alignment: Alignments.CENTER + }, + height: '400px' + }; + + if (isLoading) { + barOptions.data = { + loading: true + }; + pieOptions.data = { + loading: true + }; + } + + return { + barOptions, + pieOptions + }; +} diff --git a/src/lib/components/Course/components/Lesson/Exercise/Submissions/index.svelte b/src/lib/components/Course/components/Lesson/Exercise/Submissions/index.svelte new file mode 100644 index 00000000..f844a21e --- /dev/null +++ b/src/lib/components/Course/components/Lesson/Exercise/Submissions/index.svelte @@ -0,0 +1,69 @@ + + +
+
+
+

{$submissions.length} Submissions

+
+
+ {#each tabs as tab} + + {/each} +
+
+ {#if currentTab === tabs[0].value} + + {:else} + + {/if} +
diff --git a/src/lib/components/Course/components/Lesson/Exercise/Submissions/store.ts b/src/lib/components/Course/components/Lesson/Exercise/Submissions/store.ts new file mode 100644 index 00000000..c9fd24a1 --- /dev/null +++ b/src/lib/components/Course/components/Lesson/Exercise/Submissions/store.ts @@ -0,0 +1,4 @@ +import { writable } from 'svelte/store'; +import type { ExerciseSubmissions } from '$lib/utils/types'; + +export const submissions = writable([]); diff --git a/src/lib/components/Course/components/Lesson/Exercise/index.svelte b/src/lib/components/Course/components/Lesson/Exercise/index.svelte index e629414f..2daf2043 100644 --- a/src/lib/components/Course/components/Lesson/Exercise/index.svelte +++ b/src/lib/components/Course/components/Lesson/Exercise/index.svelte @@ -10,6 +10,14 @@ reset, questionnaireOrder } from '../store/exercise'; + import { + ContentSwitcher, + Switch, + Breadcrumb, + BreadcrumbItem, + OverflowMenu, + OverflowMenuItem + } from 'carbon-components-svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import PageBody from '$lib/components/PageBody/index.svelte'; import IconButton from '$lib/components/IconButton/index.svelte'; @@ -20,11 +28,10 @@ import { upsertExercise } from '$lib/utils/services/courses'; import UpdateDescription from './UpdateDescription.svelte'; import { snackbar } from '$lib/components/Snackbar/store'; - import { OverflowMenu, OverflowMenuItem } from 'carbon-components-svelte'; - import Analytics from './Analytics/index.svelte'; - import { ContentSwitcher, Switch } from 'carbon-components-svelte'; + import Analytics from './Submissions/index.svelte'; export let exerciseId = ''; + export let path = ''; export let goBack = () => {}; export let isStudent = true; @@ -65,60 +72,71 @@ reset(); }); - $: $questionnaire.questions.length < 1 && handleAddQuestion(); + $: $questionnaire?.questions?.length < 1 && handleAddQuestion(); + console.log('question', $questionnaire.questions); - - - - - - +
+ + All Exercises + + {$questionnaire.title} + + + + + + + + + + {#if selectedIndex === 0} +
+
+ + (preview = !preview)} + contained={preview} + toolTipProps={{ + title: 'Preview', + direction: 'bottom', + hotkeys: [] + }} + > + {#if preview} + + {:else} + + {/if} + + handleAddQuestion()} size="small"> + + + + ($questionnaireOrder.open = true)} + /> + (shouldDeleteExercise = true)} + /> + +
+
+ {/if} +
+
{#if selectedIndex === 0} -
- -
- - (preview = !preview)} - contained={preview} - toolTipProps={{ - title: 'Preview', - direction: 'bottom', - hotkeys: [] - }} - > - {#if preview} - - {:else} - - {/if} - - handleAddQuestion()} size="small"> - - - - ($questionnaireOrder.open = true)} - /> - (shouldDeleteExercise = true)} - /> - -
-
-
diff --git a/src/lib/components/Course/components/Lesson/Exercises/index.svelte b/src/lib/components/Course/components/Lesson/Exercises/index.svelte index 32097da8..fe2587bf 100644 --- a/src/lib/components/Course/components/Lesson/Exercises/index.svelte +++ b/src/lib/components/Course/components/Lesson/Exercises/index.svelte @@ -2,6 +2,7 @@ import { goto } from '$app/navigation'; import { onMount } from 'svelte'; import Box from '$lib/components/Box/index.svelte'; + import { Breadcrumb, BreadcrumbItem } from 'carbon-components-svelte'; import { Moon } from 'svelte-loading-spinners'; import Backdrop from '$lib/components/Backdrop/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; @@ -89,6 +90,7 @@ .select( ` id, title, description, due_by, + totalSubmissions:submission(count), questions:question( *, options:option(*), @@ -114,15 +116,16 @@ data.questions = data.questions.sort((a, b) => a.order - b.order); } else if (data) { data.questions = []; - } else { - data = {}; } - data.is_title_dirty = false; - data.is_description_dirty = false; - data.is_due_by_dirty = false; - - questionnaire.set(data); + questionnaire.set({ + ...(data || {}), + is_title_dirty: false, + is_description_dirty: false, + is_due_by_dirty: false, + questions: Array.isArray(data.questions) ? data.questions : [], + totalSubmissions: data?.totalSubmissions?.[0]?.count || 0 + }); isQuestionnaireFetching.update(() => false); isFetching = false; @@ -146,7 +149,7 @@ {/if} {#if exerciseId} - + {:else} + + All Exercises + - (open = !open)} /> + (open = !open)} /> -