feat: learning paths

This commit is contained in:
Jannat Patel
2024-11-18 16:15:27 +05:30
parent dcf5c72cad
commit e1a78382c3
19 changed files with 535 additions and 9 deletions

View File

@@ -183,6 +183,17 @@ const addQuizzes = () => {
}
}
const addPrograms = () => {
if (isInstructor.value || isModerator.value) {
sidebarLinks.value.push({
label: 'Programs',
icon: 'Route',
to: 'Programs',
activeFor: ['Programs', 'ProgramForm'],
})
}
}
const openPageModal = (link) => {
showPageModal.value = true
pageToEdit.value = link
@@ -215,6 +226,7 @@ watch(userResource, () => {
isModerator.value = userResource.data.is_moderator
isInstructor.value = userResource.data.is_instructor
addQuizzes()
addPrograms()
}
})

View File

@@ -108,9 +108,31 @@ const tabsStructure = computed(() => {
hideLabel: true,
items: [
{
label: 'Members',
description: 'Manage the members of your learning system',
icon: 'UserRoundPlus',
label: 'General',
icon: 'Wrench',
fields: [
{
label: 'Enable Learning Paths',
name: 'enable_learning_paths',
description:
'This will change the default flow of the system and enforce students to go through programs assigned to them in the correct order.',
type: 'checkbox',
},
{
label: 'Send calendar invite for evaluations',
name: 'send_calendar_invite_for_evaluations',
description:
'If enabled and Google Calendar of the evaluator is set in the system, students will receive calendar invites to remind them of their evaluations.',
type: 'checkbox',
},
{
label: 'Unsplash Access Key',
name: 'unsplash_access_key',
description:
'Optional. If this is set, students can pick a cover image from the unsplash library for their profile page. Refer the docs to know more https://unsplash.com/documentation#getting-started.',
type: 'text',
},
],
},
],
},
@@ -156,9 +178,14 @@ const tabsStructure = computed(() => {
],
},
{
label: 'Settings',
hideLabel: true,
label: 'Lists',
hideLabel: false,
items: [
{
label: 'Members',
description: 'Manage the members of your learning system',
icon: 'UserRoundPlus',
},
{
label: 'Categories',
description: 'Manage the members of your learning system',

View File

@@ -29,6 +29,7 @@
<script setup>
import { Button, Badge } from 'frappe-ui'
import SettingFields from '@/components/SettingFields.vue'
import { showToast } from '@/utils'
const props = defineProps({
fields: {
@@ -54,7 +55,14 @@ const update = () => {
props.data.doc[f.name] = f.value
}
})
props.data.save.submit()
props.data.save.submit(
{},
{
onError(err) {
showToast('Error', err.messages?.[0] || err, 'x')
},
}
)
}
</script>

View File

@@ -90,6 +90,7 @@
:type="field.type"
:rows="field.rows"
:options="field.options"
:description="field.description"
/>
</div>
</div>
@@ -100,7 +101,7 @@
import { FormControl, FileUploader, Button, Switch } from 'frappe-ui'
import { computed } from 'vue'
import { getFileSize, validateFile } from '@/utils'
import { X, FileText } from 'lucide-vue-next'
import { X } from 'lucide-vue-next'
import Link from '@/components/Controls/Link.vue'
import CodeEditor from '@/components/Controls/CodeEditor.vue'

View File

@@ -0,0 +1,88 @@
<template>
<header
class="sticky top-0 z-10 flex flex-col md:flex-row md:items-center justify-between border-b bg-white px-3 py-2.5 sm:px-5"
>
<Breadcrumbs :items="breadbrumbs" />
<Button variant="solid">
{{ __('Save') }}
</Button>
</header>
<div v-if="program.doc" class="w-1/2 mx-auto pt-10 space-y-5">
<FormControl :label="__('Title')" v-model="program.doc.title" />
<div>
<div>
<div class="font-semibold">
{{ __('Program Courses') }}
</div>
<div></div>
</div>
<ListView
v-if="program.doc.program_courses.length"
:columns="courseColumns"
:rows="program.doc.program_courses"
row-key="name"
:options="{ showTooltip: false, selectable: false }"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-gray-100 p-2"
>
<ListHeaderItem :item="item" v-for="item in courseColumns">
</ListHeaderItem>
</ListHeader>
<ListRows>
<ListRow :row="row" />
</ListRows>
</ListView>
</div>
</div>
</template>
<script setup>
import {
Breadcrumbs,
Button,
createDocumentResource,
FormControl,
ListView,
ListRows,
ListRow,
ListHeader,
ListHeaderItem,
} from 'frappe-ui'
import { computed } from 'vue'
const props = defineProps({
programName: {
type: String,
required: true,
},
})
const program = createDocumentResource({
doctype: 'LMS Program',
name: props.programName,
auto: true,
cache: ['program', props.programName],
})
console.log(program)
const courseColumns = computed(() => [
{
label: 'Course',
key: 'course_title',
width: '2',
},
])
const breadbrumbs = computed(() => {
return [
{
label: 'Programs',
to: { name: 'Programs' },
},
{
label: props.programName === 'new' ? 'New Program' : props.programName,
},
]
})
</script>

View File

@@ -0,0 +1,164 @@
<template>
<header
class="sticky top-0 z-10 flex flex-col md:flex-row md:items-center justify-between border-b bg-white px-3 py-2.5 sm:px-5"
>
<Breadcrumbs :items="breadbrumbs" />
<Button variant="solid" @click="showDialog = true">
<template #prefix>
<Plus class="w-4 h-4" />
</template>
{{ __('New Program') }}
</Button>
</header>
<div class="pt-5 px-5">
<div v-if="programs.data?.length">
<ListView
:columns="programColumns"
:rows="programs.data"
row-key="name"
:options="{ showTooltip: false, selectable: false }"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-gray-100 p-2"
>
<ListHeaderItem :item="item" v-for="item in programColumns">
</ListHeaderItem>
</ListHeader>
<ListRows>
<router-link
v-for="row in programs.data"
:to="{
name: 'ProgramForm',
params: {
programName: row.name,
},
}"
>
<ListRow :row="row" />
</router-link>
</ListRows>
</ListView>
</div>
<div
v-else
class="text-center p-5 text-gray-600 mt-52 w-3/4 md:w-1/2 mx-auto space-y-2"
>
<Route class="size-10 mx-auto stroke-1 text-gray-500" />
<div class="text-xl font-medium">
{{ __('No programs found') }}
</div>
<div class="leading-5">
{{
__(
'Program lets you create learning paths and assign them to your students. To create one, click on the "New Program" button above.'
)
}}
</div>
</div>
</div>
<Dialog
v-model="showDialog"
:options="{
title: __('New Program'),
actions: [
{
label: __('Create'),
variant: 'solid',
onClick: () => createProgram(close),
},
],
}"
>
<template #body-content>
<FormControl :label="__('Title')" v-model="title" />
</template>
</Dialog>
</template>
<script setup>
import {
Breadcrumbs,
Button,
createListResource,
Dialog,
FormControl,
ListView,
ListRows,
ListRow,
ListHeader,
ListHeaderItem,
} from 'frappe-ui'
import { computed, inject, onMounted, ref } from 'vue'
import { Plus, Route } from 'lucide-vue-next'
import { useRouter } from 'vue-router'
const user = inject('$user')
const router = useRouter()
const showDialog = ref(false)
const title = ref('')
onMounted(() => {
if (!user.data?.is_moderator) {
router.push({ name: 'Courses' })
}
})
const programs = createListResource({
doctype: 'LMS Program',
fields: ['title', 'name', 'program_courses'],
auto: true,
cache: 'programs',
transform(data) {
return data.map((program) => {
console.log(program)
program.program_courses = program.program_courses?.length
return program
})
},
})
const createProgram = async (close) => {
programs.insert.submit(
{
title: title.value,
},
{
onSuccess(data) {
showDialog.value = false
router.push({ name: 'ProgramForm', params: { programName: data.name } })
},
}
)
}
const breadbrumbs = computed(() => [
{
label: 'Programs',
route: {
name: 'Programs',
},
},
])
const programColumns = computed(() => {
return [
{
label: __('Title'),
key: 'title',
width: 2,
},
{
label: __('Courses'),
key: 'program_courses',
width: 1,
align: 'center',
},
{
label: __('Members'),
key: 'program_members',
width: 1,
align: 'center',
},
]
})
</script>

View File

@@ -182,6 +182,17 @@ const routes = [
component: () => import('@/pages/QuizSubmission.vue'),
props: true,
},
{
path: '/programs',
name: 'Programs',
component: () => import('@/pages/Programs.vue'),
},
{
path: '/programs/:programName',
name: 'ProgramForm',
component: () => import('@/pages/ProgramForm.vue'),
props: true,
},
]
let router = createRouter({