refactor: learning path

This commit is contained in:
Jannat Patel
2025-08-14 20:26:46 +05:30
parent 78ff2e6d07
commit 625ddac65a
21 changed files with 785 additions and 681 deletions

View File

@@ -1,379 +0,0 @@
<template>
<header
class="sticky top-0 z-10 flex flex-col md:flex-row md:items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
>
<Breadcrumbs :items="breadbrumbs" />
<Button variant="solid" @click="saveProgram()">
{{ __('Save') }}
</Button>
</header>
<div v-if="program.doc" class="pt-5 px-5 w-3/4 mx-auto space-y-10">
<FormControl v-model="program.doc.title" :label="__('Title')" />
<!-- Courses -->
<div>
<div class="flex items-center justify-between mb-2">
<div class="text-lg text-ink-gray-9 font-semibold">
{{ __('Program Courses') }}
</div>
<Button
@click="
() => {
currentForm = 'course'
showDialog = true
}
"
>
<template #prefix>
<Plus class="w-4 h-4" />
</template>
{{ __('Add') }}
</Button>
</div>
<ListView
:columns="courseColumns"
:rows="program.doc.program_courses"
row-key="name"
:options="{
showTooltip: false,
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
>
<ListHeaderItem :item="item" v-for="item in courseColumns" />
</ListHeader>
<ListRows>
<Draggable
:list="program.doc.program_courses"
item-key="name"
group="items"
@end="updateOrder"
class="cursor-move"
>
<template #item="{ element: row }">
<ListRow :row="row" />
</template>
</Draggable>
</ListRows>
<ListSelectBanner>
<template #actions="{ unselectAll, selections }">
<div class="flex gap-2">
<Button
variant="ghost"
@click="remove(selections, unselectAll, 'program_courses')"
>
<Trash2 class="h-4 w-4 stroke-1.5" />
</Button>
</div>
</template>
</ListSelectBanner>
</ListView>
</div>
<!-- Members -->
<div>
<div class="flex items-center justify-between mb-2">
<div class="text-lg text-ink-gray-9 font-semibold">
{{ __('Program Members') }}
</div>
<Button
@click="
() => {
currentForm = 'member'
showDialog = true
}
"
>
<template #prefix>
<Plus class="w-4 h-4" />
</template>
{{ __('Add') }}
</Button>
</div>
<ListView
:columns="memberColumns"
:rows="program.doc.program_members"
row-key="name"
:options="{
showTooltip: false,
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
>
<ListHeaderItem :item="item" v-for="item in memberColumns" />
</ListHeader>
<ListRows>
<ListRow :row="row" v-for="row in program.doc.program_members" />
</ListRows>
<ListSelectBanner>
<template #actions="{ unselectAll, selections }">
<div class="flex gap-2">
<Button
variant="ghost"
@click="remove(selections, unselectAll, 'program_members')"
>
<Trash2 class="h-4 w-4 stroke-1.5" />
</Button>
</div>
</template>
</ListSelectBanner>
</ListView>
</div>
</div>
<Dialog
v-model="showDialog"
:options="{
title:
currentForm == 'course'
? __('New Program Course')
: __('New Program Member'),
actions: [
{
label: __('Add'),
variant: 'solid',
onClick: () =>
currentForm == 'course'
? addProgramCourse(close)
: addProgramMember(close),
},
],
}"
>
<template #body-content>
<Link
v-if="currentForm == 'course'"
v-model="course"
doctype="LMS Course"
:filters="{
disable_self_learning: 1,
}"
:label="__('Program Course')"
:description="
__(
'Only courses for which self learning is disabled can be added to program.'
)
"
/>
<Link
v-if="currentForm == 'member'"
v-model="member"
doctype="User"
:filters="{
ignore_user_type: 1,
}"
:label="__('Program Member')"
:onCreate="(value, close) => openSettings('Members', close)"
/>
</template>
</Dialog>
</template>
<script setup>
import {
Breadcrumbs,
Button,
call,
createDocumentResource,
Dialog,
FormControl,
ListView,
ListRows,
ListRow,
ListHeader,
ListHeaderItem,
ListSelectBanner,
usePageMeta,
toast,
} from 'frappe-ui'
import { computed, ref } from 'vue'
import { Plus, Trash2 } from 'lucide-vue-next'
import { useRouter } from 'vue-router'
import { sessionStore } from '@/stores/session'
import { openSettings } from '@/utils'
import Draggable from 'vuedraggable'
import Link from '@/components/Controls/Link.vue'
const { brand } = sessionStore()
const showDialog = ref(false)
const currentForm = ref(null)
const course = ref(null)
const member = ref(null)
const router = useRouter()
const props = defineProps({
programName: {
type: String,
required: true,
},
})
const program = createDocumentResource({
doctype: 'LMS Program',
name: props.programName,
auto: true,
cache: ['program', props.programName],
})
const addProgramCourse = () => {
program.setValue.submit(
{
program_courses: [
...program.doc.program_courses,
{ course: course.value },
],
},
{
onSuccess(data) {
showDialog.value = false
course.value = null
toast.success(__('Course added to program'))
program.reload()
},
onError(err) {
toast.error(err.messages?.[0] || err)
},
}
)
}
const addProgramMember = () => {
program.setValue.submit(
{
program_members: [
...program.doc.program_members,
{ member: member.value },
],
},
{
onSuccess(data) {
showDialog.value = false
member.value = null
toast.success(__('Member added to program'))
program.reload()
},
onError(err) {
toast.error(err.messages?.[0] || err)
},
}
)
}
const remove = (selections, unselectAll, doctype) => {
selections = Array.from(selections)
program.setValue.submit(
{
[doctype]: program.doc[doctype].filter(
(row) => !selections.includes(row.name)
),
},
{
onSuccess(data) {
unselectAll()
toast.success(__('Items removed successfully'))
program.reload()
},
onError(err) {
toast.error(err.messages?.[0] || err)
},
}
)
}
const updateOrder = (e) => {
let sourceIdx = e.from.dataset.idx
let targetIdx = e.to.dataset.idx
let courses = program.doc.program_courses
courses.splice(targetIdx, 0, courses.splice(sourceIdx, 1)[0])
courses.forEach((course, index) => {
course.idx = index + 1
})
program.setValue.submit(
{
program_courses: courses,
},
{
onSuccess(data) {
toast.success(__('Course moved successfully'))
program.reload()
},
onError(err) {
toast.error(err.messages?.[0] || err)
},
}
)
}
const saveProgram = () => {
call('frappe.model.rename_doc.update_document_title', {
doctype: 'LMS Program',
docname: program.doc.name,
name: program.doc.title,
}).then((data) => {
router.push({ name: 'ProgramForm', params: { programName: data } })
})
}
const courseColumns = computed(() => {
return [
{
label: 'Title',
key: 'course_title',
width: 3,
},
{
label: 'ID',
key: 'course',
width: 3,
},
]
})
const memberColumns = computed(() => {
return [
{
label: 'Member',
key: 'member',
width: 3,
align: 'left',
},
{
label: 'Full Name',
key: 'full_name',
width: 3,
align: 'left',
},
{
label: 'Progress (%)',
key: 'progress',
width: 3,
align: 'right',
},
]
})
const breadbrumbs = computed(() => {
return [
{
label: 'Programs',
route: { name: 'Programs' },
},
{
label: props.programName === 'new' ? 'New Program' : props.programName,
},
]
})
usePageMeta(() => {
return {
title: program.doc?.title,
icon: brand.favicon,
}
})
</script>

View File

@@ -1,216 +0,0 @@
<template>
<header
class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
>
<Breadcrumbs :items="breadbrumbs" />
<Button
v-if="canCreateProgram()"
@click="showDialog = true"
variant="solid"
>
<template #prefix>
<Plus class="h-4 w-4 stroke-1.5" />
</template>
{{ __('New') }}
</Button>
</header>
<div v-if="programs.data?.length" class="pt-5 px-5">
<div v-for="program in programs.data" class="mb-10">
<div class="flex items-center justify-between">
<div class="text-xl text-ink-gray-9 font-semibold">
{{ program.name }}
</div>
<div class="flex items-center space-x-2">
<Badge
v-if="program.members"
variant="subtle"
theme="green"
size="lg"
>
{{ program.members }}
{{ program.members == 1 ? __('member') : __('members') }}
</Badge>
<Badge
v-if="program.progress"
variant="subtle"
theme="blue"
size="lg"
>
{{ program.progress }}{{ __('% completed') }}
</Badge>
<router-link
v-if="user.data?.is_moderator || user.data?.is_instructor"
:to="{
name: 'ProgramForm',
params: { programName: program.name },
}"
>
<Button v-if="!readOnlyMode">
<template #prefix>
<Edit class="h-4 w-4 stroke-1.5" />
</template>
{{ __('Edit') }}
</Button>
</router-link>
</div>
</div>
<div
v-if="program.courses?.length"
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5 mt-5"
>
<div v-for="course in program.courses" class="relative group">
<CourseCard
:course="course"
@click="enrollMember(program.name, course.name)"
class="cursor-pointer"
/>
<div
v-if="lockCourse(course)"
class="absolute inset-0 bg-black-overlay-500 opacity-60 rounded-md"
></div>
<div
v-if="lockCourse(course)"
class="absolute inset-0 flex items-center justify-center"
>
<LockKeyhole class="size-10 text-ink-white" />
</div>
</div>
</div>
<div v-else class="text-sm italic text-ink-gray-5 mt-4">
{{ __('No courses in this program') }}
</div>
</div>
</div>
<EmptyState v-else type="Programs" />
<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 {
Badge,
Breadcrumbs,
Button,
call,
createResource,
Dialog,
FormControl,
usePageMeta,
toast,
} from 'frappe-ui'
import { computed, inject, onMounted, ref } from 'vue'
import { Edit, Plus, LockKeyhole } from 'lucide-vue-next'
import CourseCard from '@/components/CourseCard.vue'
import EmptyState from '@/components/EmptyState.vue'
import { useRouter } from 'vue-router'
import { sessionStore } from '../stores/session'
import { useSettings } from '@/stores/settings'
const { brand } = sessionStore()
const user = inject('$user')
const showDialog = ref(false)
const router = useRouter()
const title = ref('')
const settings = useSettings()
const readOnlyMode = window.read_only_mode
onMounted(() => {
if (
!settings.learningPaths.data &&
!user.data?.is_moderator &&
!user.data?.is_instructor
) {
router.push({ name: 'Courses' })
}
})
const programs = createResource({
url: 'lms.lms.utils.get_programs',
auto: true,
cache: 'programs',
})
const createProgram = (close) => {
call('frappe.client.insert', {
doc: {
doctype: 'LMS Program',
title: title.value,
},
}).then((res) => {
router.push({ name: 'ProgramForm', params: { programName: res.name } })
})
}
const enrollMember = (program, course) => {
call('lms.lms.utils.enroll_in_program_course', {
program: program,
course: course,
})
.then((data) => {
if (data.current_lesson) {
router.push({
name: 'Lesson',
params: {
courseName: course,
chapterNumber: data.current_lesson.split('-')[0],
lessonNumber: data.current_lesson.split('-')[1],
},
})
} else if (data) {
router.push({
name: 'Lesson',
params: {
courseName: course,
chapterNumber: 1,
lessonNumber: 1,
},
})
}
})
.catch((err) => {
toast.error(err.messages?.[0] || err)
})
}
const lockCourse = (course) => {
if (user.data?.is_moderator || user.data?.is_instructor) return false
if (course.membership) return false
if (course.eligible) return false
return true
}
const canCreateProgram = () => {
if (readOnlyMode) return false
if (user.data?.is_moderator || user.data?.is_instructor) return true
return false
}
const breadbrumbs = computed(() => [
{
label: __('Programs'),
},
])
usePageMeta(() => {
return {
title: __('Programs'),
icon: brand.favicon,
}
})
</script>

View File

@@ -0,0 +1,528 @@
<template>
<Dialog
v-model="show"
:options="{
title: __('Program'),
size: '2xl',
actions: [{
label: __('Save'),
variant: 'solid',
onClick: ({ close }: { close: () => void }) => {
saveProgram(close)
}
}]
}"
>
<template #body-content>
<div class="text-base">
<div class="grid grid-cols-1 md:grid-cols-2 gap-5 pb-5">
<FormControl
v-model="program.name"
:label="__('Title')"
type="text"
:required="true"
/>
<div class="flex flex-col space-y-3">
<FormControl
v-model="program.published"
:label="__('Published')"
type="checkbox"
/>
<FormControl
v-model="program.enforce_course_order"
:label="__('Enforce Course Order')"
type="checkbox"
/>
<FormControl
v-model="program.allow_self_enrollment"
:label="__('Allow Self Enrollment')"
type="checkbox"
/>
</div>
</div>
<div class="pb-5">
<div class="flex items-center justify-between mt-5 mb-4">
<div class="text-lg font-semibold">
{{ __('Courses') }}
</div>
<Button @click="openForm('course')">
<template #prefix>
<Plus class="h-4 w-4 stroke-1.5" />
</template>
<span>
{{ __('Add') }}
</span>
</Button>
</div>
<ListView
v-if="programCourses.data.length > 0"
:columns="courseColumns"
:rows="programCourses.data"
:options="{
selectable: true,
resizeColumn: true,
showTooltip: false,
}"
rowKey="name"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
>
<ListHeaderItem :item="item" v-for="item in courseColumns" />
</ListHeader>
<ListRows>
<Draggable
:list="programCourses.data"
item-key="name"
group="items"
@end="updateOrder"
class="cursor-move"
>
<template #item="{ element: row }">
<ListRow :row="row" />
</template>
</Draggable>
</ListRows>
<ListSelectBanner>
<template #actions="{ unselectAll, selections }">
<div class="flex gap-2">
<Button
variant="ghost"
@click="remove(selections, unselectAll, 'courses')"
>
<Trash2 class="h-4 w-4 stroke-1.5" />
</Button>
</div>
</template>
</ListSelectBanner>
</ListView>
<div v-else class="text-ink-gray-7">
{{ __('No courses added yet.') }}
</div>
</div>
<div>
<div class="flex items-center justify-between mt-5 mb-4">
<div class="text-lg font-semibold">
{{ __('Members') }}
</div>
<Button @click="openForm('member')">
<template #prefix>
<Plus class="h-4 w-4 stroke-1.5" />
</template>
{{ __('Add') }}
</Button>
</div>
<ListView
v-if="programMembers.data.length > 0"
:columns="memberColumns"
:rows="programMembers.data"
:options="{
selectable: true,
resizeColumn: true,
}"
rowKey="name"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
>
<ListHeaderItem :item="item" v-for="item in memberColumns" />
</ListHeader>
<ListRows>
<ListRow :row="row" v-for="row in programMembers.data" />
</ListRows>
<ListSelectBanner>
<template #actions="{ unselectAll, selections }">
<div class="flex gap-2">
<Button
variant="ghost"
@click="remove(selections, unselectAll, 'members')"
>
<Trash2 class="h-4 w-4 stroke-1.5" />
</Button>
</div>
</template>
</ListSelectBanner>
</ListView>
<div v-else class="text-ink-gray-7">
{{ __('No members added yet.') }}
</div>
</div>
</div>
<Dialog
v-model="showDialog"
:options="{
title:
currentForm == 'course'
? __('Add Course to Program')
: __('Enroll Member to Program'),
actions: [
{
label: __('Add'),
variant: 'solid',
onClick: ({ close }: { close: () => void }) =>
currentForm == 'course'
? addCourse(close)
: addMember(close),
},
],
}"
>
<template #body-content>
<div @click.stop>
<Link
v-if="currentForm == 'course'"
v-model="course"
doctype="LMS Course"
:label="__('Course')"
/>
<Link
v-if="currentForm == 'member'"
v-model="member"
doctype="User"
:filters="{
ignore_user_type: 1,
}"
:label="__('Program Member')"
:onCreate="(value: string, close: () => void) => openSettings('Members', close)"
/>
</div>
</template>
</Dialog>
</template>
</Dialog>
</template>
<script setup lang="ts">
import {
Button,
createListResource,
Dialog,
FormControl,
ListSelectBanner,
ListView,
ListHeader,
ListHeaderItem,
ListRows,
ListRow,
toast,
} from 'frappe-ui'
import { computed, ref, watch } from 'vue'
import { Plus, Trash2 } from 'lucide-vue-next'
import { Programs, Program } from '@/types/programs'
import { openSettings } from '@/utils'
import Link from '@/components/Controls/Link.vue'
import Draggable from 'vuedraggable'
const show = defineModel<boolean>()
const programs = defineModel<Programs>('programs')
const showDialog = ref(false)
const currentForm = ref<'course' | 'member'>('course')
const course = ref<string>('')
const member = ref<string>('')
const props = withDefaults(
defineProps<{
programName: string | null
}>(),
{
programName: 'new',
}
)
const program = ref<Program>({
name: '',
title: '',
published: false,
enforce_course_order: false,
allow_self_enrollment: false,
program_courses: [],
program_members: [],
})
watch(
() => props.programName,
() => {
setProgramData()
fetchCourses()
fetchMembers()
}
)
const setProgramData = () => {
let isNew = true
programs.value?.data.forEach((p: Program) => {
if (p.name === props.programName) {
isNew = false
program.value = { ...p }
}
})
if (isNew) {
program.value = {
name: '',
title: '',
published: false,
enforce_course_order: false,
allow_self_enrollment: false,
program_courses: [],
program_members: [],
}
}
}
const programCourses = createListResource({
doctype: 'LMS Program Course',
fields: ['course', 'course_title', 'name', 'idx'],
cache: ['programCourses', props.programName],
parent: 'LMS Program',
orderBy: 'idx',
onSuccess(data: ProgramCourse[]) {
program.value.program_courses = data
},
})
const programMembers = createListResource({
doctype: 'LMS Program Member',
fields: ['member', 'full_name', 'progress', 'name'],
cache: ['programMembers', props.programName],
parent: 'LMS Program',
orderBy: 'creation desc',
onSuccess(data: ProgramMember[]) {
program.value.program_members = data
},
})
const fetchCourses = () => {
programCourses.update({
filters: {
parent: props.programName,
parenttype: 'LMS Program',
parentfield: 'program_courses',
},
})
programCourses.reload()
}
const fetchMembers = () => {
programMembers.update({
filters: {
parent: props.programName,
parenttype: 'LMS Program',
parentfield: 'program_members',
},
})
programMembers.reload()
}
const saveProgram = (close: () => void) => {
if (props.programName === 'new') createNewProgram(close)
else updateProgram(close)
}
const createNewProgram = (close: () => void) => {
programs.value.insert.submit(
{
...program.value,
title: program.value.name,
},
{
onSuccess() {
close()
programs.value.reload()
toast.success(__('Program created successfully'))
},
onError(err: any) {
toast.warning(__(err.messages?.[0] || err))
},
}
)
}
const updateProgram = (close: () => void) => {
programs.value.setValue.submit(
{
name: props.programName,
...program.value,
},
{
onSuccess() {
close()
programs.value.reload()
toast.success(__('Program updated successfully'))
},
onError(err: any) {
toast.warning(__(err.messages?.[0] || err))
},
}
)
}
const openForm = (formType: 'course' | 'member') => {
currentForm.value = formType
showDialog.value = true
if (formType === 'course') {
course.value = ''
} else {
member.value = ''
}
}
const addCourse = (close: () => void) => {
if (!course.value) {
toast.warning(__('Please select a course'))
return
}
programCourses.insert.submit(
{
parent: props.programName,
parenttype: 'LMS Program',
parentfield: 'program_courses',
course: course.value,
idx: programCourses.data.length + 1,
},
{
onSuccess() {
updateCounts('course', 'add')
close()
toast.success(__('Course added to program successfully'))
},
onError(err: any) {
toast.warning(__(err.messages?.[0] || err))
},
}
)
}
const addMember = (close: () => void) => {
if (!member.value) {
toast.warning(__('Please select a member'))
return
}
programMembers.insert.submit(
{
parent: props.programName,
parenttype: 'LMS Program',
parentfield: 'program_members',
member: member.value,
},
{
onSuccess() {
updateCounts('member', 'add')
close()
toast.success(__('Member added to program successfully'))
},
onError(err: any) {
toast.warning(__(err.messages?.[0] || err))
},
}
)
}
const updateCounts = async (
type: 'member' | 'course',
action: 'add' | 'remove'
) => {
console.log('update', props.programName)
if (!props.programName) return
let memberCount = programMembers.data?.length || 0
let courseCount = programCourses.data?.length || 0
if (type === 'member') {
memberCount += action === 'add' ? 1 : -1
} else {
courseCount += action === 'add' ? 1 : -1
}
await programs.value.setValue.submit(
{
name: props.programName,
member_count: memberCount,
course_count: courseCount,
},
{
onSuccess() {
setProgramData()
},
onError(err: any) {
toast.warning(__(err.messages?.[0] || err))
},
}
)
}
const updateOrder = async (e: DragEvent) => {
let sourceIdx = e.from.dataset.idx
let targetIdx = e.to.dataset.idx
let courses = programCourses.data
courses.splice(targetIdx, 0, courses.splice(sourceIdx, 1)[0])
for (const [index, course] of courses.entries()) {
programCourses.setValue.submit(
{
name: course.name,
idx: index + 1,
},
{
onError(err: any) {
toast.warning(__(err.messages?.[0] || err))
},
}
)
await wait(100)
}
}
const wait = (ms: number) => new Promise((res) => setTimeout(res, ms))
const remove = async (
selections: string[],
unselectAll: () => void,
type: string
) => {
selections = Array.from(selections)
for (const selection of selections) {
if (type == 'courses') {
await programCourses.delete.submit(selection)
await updateCounts('course', 'remove')
} else {
await programMembers.delete.submit(selection)
await updateCounts('member', 'remove')
}
await programs.value.reload()
await wait(100)
}
unselectAll()
}
const courseColumns = computed(() => {
return [
{
label: 'Title',
key: 'course_title',
width: 1,
},
]
})
const memberColumns = computed(() => {
return [
{
label: 'Member',
key: 'member',
width: 3,
align: 'left',
},
{
label: 'Full Name',
key: 'full_name',
width: 3,
align: 'left',
},
]
})
</script>

View File

@@ -0,0 +1,103 @@
<template>
<header
class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
>
<Breadcrumbs :items="breadcrumbs" />
<Button v-if="canCreateProgram()" @click="openForm('new')" variant="solid">
<template #prefix>
<Plus class="h-4 w-4 stroke-1.5" />
</template>
{{ __('New') }}
</Button>
</header>
<div
v-if="programs.data?.length"
class="grid grid-cols-3 gap-5 py-10 w-3/4 mx-auto"
>
<div
v-for="program in programs.data"
@click="openForm(program.name)"
class="border rounded-md p-3 hover:border-outline-gray-3 cursor-pointer space-y-2"
>
<div class="text-lg font-semibold">
{{ program.name }}
</div>
<div class="flex items-center space-x-1">
<BookOpen class="h-4 w-4 stroke-1.5 mr-1" />
<span>
{{ program.course_count }}
{{ program.course_count == 1 ? __('Course') : __('Courses') }}
</span>
</div>
<div class="flex items-center space-x-1">
<User class="h-4 w-4 stroke-1.5 mr-1" />
<span>
{{ program.member_count || 0 }}
{{ program.member_count == 1 ? __('member') : __('members') }}
</span>
</div>
</div>
</div>
<EmptyState v-else type="Programs" />
<ProgramForm
v-model="showForm"
:programName="currentProgram"
v-model:programs="programs"
/>
</template>
<script setup>
import { Breadcrumbs, Button, usePageMeta, createListResource } from 'frappe-ui'
import { computed, inject, ref } from 'vue'
import { BookOpen, Plus, User } from 'lucide-vue-next'
import EmptyState from '@/components/EmptyState.vue'
import { sessionStore } from '../../stores/session'
import ProgramForm from '@/pages/Programs/ProgramForm.vue'
const { brand } = sessionStore()
const user = inject('$user')
const showForm = ref(false)
const currentProgram = ref(null)
const readOnlyMode = window.read_only_mode
const programs = createListResource({
doctype: 'LMS Program',
cache: ['program'],
fields: [
'name',
'title',
'member_count',
'course_count',
'published',
'enforce_course_order',
'allow_self_enrollment',
],
auto: true,
orderBy: 'creation desc',
})
const canCreateProgram = () => {
if (readOnlyMode) return false
if (user.data?.is_moderator || user.data?.is_instructor) return true
return false
}
const openForm = (programName) => {
if (!canCreateProgram()) return
currentProgram.value = programName
showForm.value = true
}
const breadcrumbs = computed(() => [
{
label: __('Programs'),
},
])
usePageMeta(() => {
return {
title: __('Programs'),
icon: brand.favicon,
}
})
</script>

View File

@@ -0,0 +1,51 @@
interface Program {
name: string;
title: string;
published: boolean;
enforce_course_order: boolean;
allow_self_enrollment: boolean;
program_courses: ProgramCourse[];
program_batches: ProgramMember[];
course_count: number;
member_count: number;
}
interface ProgramCourse {
course: string;
course_title: string;
idx: number;
name: string;
}
interface ProgramMember {
member: string;
full_name: string;
progress: number;
idx: number;
name: string;
}
interface Programs {
data: Program[];
reload: () => void;
hasNextPage: boolean;
next: () => void;
setValue: {
submit: (
data: Program,
options?: { onSuccess?: () => void }
) => void;
};
insert: {
submit: (
data: Program,
options?: { onSuccess?: () => void }
) => void;
};
delete: {
submit: (
name: string,
options?: { onSuccess?: () => void }
) => void;
};
}