2
.github/workflows/ui-tests.yml
vendored
2
.github/workflows/ui-tests.yml
vendored
@@ -105,7 +105,7 @@ jobs:
|
||||
- name: cypress pre-requisites
|
||||
run: |
|
||||
cd ~/frappe-bench/apps/lms
|
||||
yarn add cypress@^10 --no-lockfile
|
||||
yarn add cypress@^10 --no-lockfile -W
|
||||
|
||||
- name: UI Tests
|
||||
run: cd ~/frappe-bench/ && bench --site lms.test run-ui-tests lms --headless
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
module.exports = {
|
||||
export default {
|
||||
parserPreset: "conventional-changelog-conventionalcommits",
|
||||
rules: {
|
||||
"subject-empty": [2, "never"],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const { defineConfig } = require("cypress");
|
||||
import { defineConfig } from "cypress";
|
||||
|
||||
module.exports = defineConfig({
|
||||
export default defineConfig({
|
||||
projectId: "vandxn",
|
||||
adminPassword: "admin",
|
||||
testUser: "frappe@example.com",
|
||||
|
||||
1
frontend/components.d.ts
vendored
1
frontend/components.d.ts
vendored
@@ -47,6 +47,7 @@ declare module 'vue' {
|
||||
Discussions: typeof import('./src/components/Discussions.vue')['default']
|
||||
EditCoverImage: typeof import('./src/components/Modals/EditCoverImage.vue')['default']
|
||||
EditProfile: typeof import('./src/components/Modals/EditProfile.vue')['default']
|
||||
EmptyState: typeof import('./src/components/EmptyState.vue')['default']
|
||||
EvaluationModal: typeof import('./src/components/Modals/EvaluationModal.vue')['default']
|
||||
Evaluators: typeof import('./src/components/Evaluators.vue')['default']
|
||||
Event: typeof import('./src/components/Modals/Event.vue')['default']
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"name": "frappe-ui-frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"serve": "vite preview",
|
||||
@@ -26,7 +27,7 @@
|
||||
"codemirror-editor-vue3": "^2.8.0",
|
||||
"dayjs": "^1.11.6",
|
||||
"feather-icons": "^4.28.0",
|
||||
"frappe-ui": "^0.1.134",
|
||||
"frappe-ui": "^0.1.143",
|
||||
"highlight.js": "^11.11.1",
|
||||
"lucide-vue-next": "^0.383.0",
|
||||
"markdown-it": "^14.0.0",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
module.exports = {
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<template>
|
||||
<Layout>
|
||||
<router-view />
|
||||
</Layout>
|
||||
<Dialogs />
|
||||
<Toasts />
|
||||
<FrappeUIProvider>
|
||||
<Layout>
|
||||
<router-view />
|
||||
</Layout>
|
||||
<Dialogs />
|
||||
</FrappeUIProvider>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Toasts } from 'frappe-ui'
|
||||
import { FrappeUIProvider } from 'frappe-ui'
|
||||
import { Dialogs } from '@/utils/dialogs'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useScreenSize } from './utils/composables'
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
@click="redirectToWebsite()"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip :text="__('Help')">
|
||||
<Tooltip v-if="showOnboarding" :text="__('Help')">
|
||||
<CircleHelp
|
||||
class="size-4 stroke-1.5 text-ink-gray-7 cursor-pointer"
|
||||
@click="
|
||||
|
||||
@@ -191,10 +191,11 @@ import {
|
||||
FileUploader,
|
||||
FormControl,
|
||||
TextEditor,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import { computed, inject, onMounted, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { FileText, X } from 'lucide-vue-next'
|
||||
import { showToast, getFileSize } from '@/utils'
|
||||
import { getFileSize } from '@/utils'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const submissionFile = ref(null)
|
||||
@@ -284,7 +285,7 @@ const submissionResource = createDocumentResource({
|
||||
doctype: 'LMS Assignment Submission',
|
||||
name: props.submissionName,
|
||||
onError(err) {
|
||||
showToast(__('Error'), __(err.messages?.[0] || err), 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
auto: false,
|
||||
cache: [user.data?.name, props.assignmentID],
|
||||
@@ -338,7 +339,7 @@ const submitAssignment = () => {
|
||||
},
|
||||
{
|
||||
onSuccess(data) {
|
||||
showToast(__('Success'), __('Changes saved successfully'), 'check')
|
||||
toast.success(__('Changes saved successfully'))
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -352,7 +353,7 @@ const addNewSubmission = () => {
|
||||
{},
|
||||
{
|
||||
onSuccess(data) {
|
||||
showToast('Success', 'Assignment submitted successfully.', 'check')
|
||||
toast.success(__('Assignment submitted successfully'))
|
||||
if (router.currentRoute.value.name == 'AssignmentSubmission') {
|
||||
router.push({
|
||||
name: 'AssignmentSubmission',
|
||||
@@ -370,7 +371,7 @@ const addNewSubmission = () => {
|
||||
submissionResource.reload()
|
||||
},
|
||||
onError(err) {
|
||||
showToast('Error', err.messages?.[0] || err, 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -86,9 +86,9 @@ import {
|
||||
ListRows,
|
||||
ListView,
|
||||
ListRowItem,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import { Plus, Trash2 } from 'lucide-vue-next'
|
||||
import { showToast } from '@/utils'
|
||||
const readOnlyMode = window.read_only_mode
|
||||
|
||||
const showCourseModal = ref(false)
|
||||
@@ -152,7 +152,7 @@ const removeCourses = (selections, unselectAll) => {
|
||||
{
|
||||
onSuccess(data) {
|
||||
courses.reload()
|
||||
showToast(__('Success'), __('Courses deleted successfully'), 'check')
|
||||
toast.success(__('Courses deleted successfully'))
|
||||
unselectAll()
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
<div v-if="batch.data" class="border-2 rounded-md p-5 lg:w-72">
|
||||
<div
|
||||
v-if="batch.data.seat_count && seats_left > 0"
|
||||
class="text-xs bg-green-100 text-green-700 float-right px-2 py-0.5 rounded-md"
|
||||
class="text-sm bg-green-100 text-green-700 px-2 py-1 rounded-md"
|
||||
:class="
|
||||
batch.data.amount || batch.data.courses.length
|
||||
? 'float-right'
|
||||
: 'w-fit mb-4'
|
||||
"
|
||||
>
|
||||
{{ seats_left }}
|
||||
<span v-if="seats_left > 1">
|
||||
@@ -117,9 +122,9 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import { inject, computed } from 'vue'
|
||||
import { Badge, Button, createResource } from 'frappe-ui'
|
||||
import { Badge, Button, createResource, toast } from 'frappe-ui'
|
||||
import { BookOpen, Clock, Globe } from 'lucide-vue-next'
|
||||
import { formatNumberIntoCurrency, formatTime, showToast } from '@/utils'
|
||||
import { formatNumberIntoCurrency, formatTime } from '@/utils'
|
||||
import DateRange from '@/components/Common/DateRange.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
@@ -151,11 +156,7 @@ const enrollInBatch = () => {
|
||||
{},
|
||||
{
|
||||
onSuccess(data) {
|
||||
showToast(
|
||||
__('Success'),
|
||||
__('You have been enrolled in this batch'),
|
||||
'check'
|
||||
)
|
||||
toast.success(__('You have been enrolled in this batch'))
|
||||
router.push({
|
||||
name: 'Batch',
|
||||
params: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="">
|
||||
<div v-if="batch.data" class="">
|
||||
<div class="w-full flex items-center justify-between pb-4">
|
||||
<div class="font-medium text-ink-gray-7">
|
||||
{{ __('Statistics') }}
|
||||
@@ -46,7 +46,7 @@
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="font-semibold">
|
||||
{{ batch.courses?.length }}
|
||||
{{ batch.data.courses?.length }}
|
||||
</span>
|
||||
<span>
|
||||
{{ __('Courses') }}
|
||||
@@ -201,9 +201,10 @@
|
||||
</div>
|
||||
|
||||
<StudentModal
|
||||
:batch="props.batch.name"
|
||||
:batch="props.batch.data.name"
|
||||
v-model="showStudentModal"
|
||||
v-model:reloadStudents="students"
|
||||
v-model:batchModal="props.batch"
|
||||
/>
|
||||
<BatchStudentProgress
|
||||
:student="selectedStudent"
|
||||
@@ -223,6 +224,7 @@ import {
|
||||
ListRows,
|
||||
ListView,
|
||||
ListRowItem,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import {
|
||||
BookOpen,
|
||||
@@ -234,7 +236,6 @@ import {
|
||||
} from 'lucide-vue-next'
|
||||
import { ref, watch } from 'vue'
|
||||
import StudentModal from '@/components/Modals/StudentModal.vue'
|
||||
import { showToast } from '@/utils'
|
||||
import ProgressBar from '@/components/ProgressBar.vue'
|
||||
import BatchStudentProgress from '@/components/Modals/BatchStudentProgress.vue'
|
||||
import ApexChart from 'vue3-apexcharts'
|
||||
@@ -258,15 +259,15 @@ const props = defineProps({
|
||||
|
||||
const students = createResource({
|
||||
url: 'lms.lms.utils.get_batch_students',
|
||||
cache: ['students', props.batch.name],
|
||||
params: {
|
||||
batch: props.batch?.name,
|
||||
batch: props.batch?.data?.name,
|
||||
},
|
||||
auto: true,
|
||||
onSuccess(data) {
|
||||
chartData.value = getChartData()
|
||||
showProgressChart.value =
|
||||
data.length && (props.batch?.courses?.length || assessmentCount.value)
|
||||
data.length &&
|
||||
(props.batch?.data?.courses?.length || assessmentCount.value)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -323,7 +324,8 @@ const removeStudents = (selections, unselectAll) => {
|
||||
{
|
||||
onSuccess(data) {
|
||||
students.reload()
|
||||
showToast(__('Success'), __('Students deleted successfully'), 'check')
|
||||
props.batch.reload()
|
||||
toast.success(__('Students deleted successfully'))
|
||||
unselectAll()
|
||||
},
|
||||
}
|
||||
@@ -434,7 +436,7 @@ const certificationCount = createResource({
|
||||
params: {
|
||||
doctype: 'LMS Certificate',
|
||||
filters: {
|
||||
batch_name: props.batch.name,
|
||||
batch_name: props.batch?.data?.name,
|
||||
},
|
||||
},
|
||||
auto: true,
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
{{ label }}
|
||||
</div>
|
||||
<Button @click="() => showCategoryForm()">
|
||||
<template #icon>
|
||||
<template #prefix>
|
||||
<Plus v-if="!showForm" class="h-3 w-3 stroke-1.5" />
|
||||
<X v-else class="h-3 w-3 stroke-1.5" />
|
||||
</template>
|
||||
{{ showForm ? __('Close') : __('New') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -28,12 +29,11 @@
|
||||
</div>
|
||||
|
||||
<div class="overflow-y-scroll">
|
||||
<div class="text-base divide-y space-y-2">
|
||||
<div class="text-base space-y-2">
|
||||
<FormControl
|
||||
:value="cat.category"
|
||||
type="text"
|
||||
v-for="cat in categories.data"
|
||||
class=""
|
||||
@change.stop="(e) => update(cat.name, e.target.value)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -92,7 +92,10 @@
|
||||
{{ option.label }}
|
||||
</div>
|
||||
<div
|
||||
v-if="option.description"
|
||||
v-if="
|
||||
option.description &&
|
||||
option.description != option.label
|
||||
"
|
||||
class="text-xs text-ink-gray-7"
|
||||
v-html="option.description"
|
||||
></div>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="w-full !justify-start"
|
||||
label="Create New"
|
||||
:label="__('Create New')"
|
||||
@click="attrs.onCreate(value, close)"
|
||||
>
|
||||
<template #prefix>
|
||||
|
||||
@@ -4,78 +4,91 @@
|
||||
{{ label }}
|
||||
<span class="text-ink-red-3" v-if="required">*</span>
|
||||
</label>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<Button
|
||||
ref="emails"
|
||||
v-for="value in values"
|
||||
:key="value"
|
||||
:label="value"
|
||||
theme="gray"
|
||||
variant="subtle"
|
||||
class="rounded-md word-break-all"
|
||||
@keydown.delete.capture.stop="removeLastValue"
|
||||
>
|
||||
<template #suffix>
|
||||
<X @click="removeValue(value)" class="h-4 w-4 stroke-1.5" />
|
||||
</template>
|
||||
</Button>
|
||||
<div class="">
|
||||
<Combobox v-model="selectedValue" nullable>
|
||||
<Popover class="w-full" v-model:show="showOptions">
|
||||
<template #target="{ togglePopover }">
|
||||
<ComboboxInput
|
||||
ref="search"
|
||||
class="search-input form-input w-full focus-visible:!ring-0"
|
||||
type="text"
|
||||
:value="query"
|
||||
@change="
|
||||
(e) => {
|
||||
query = e.target.value
|
||||
showOptions = true
|
||||
}
|
||||
"
|
||||
autocomplete="off"
|
||||
@focus="() => togglePopover()"
|
||||
@keydown.delete.capture.stop="removeLastValue"
|
||||
/>
|
||||
</template>
|
||||
<template #body="{ isOpen }">
|
||||
<div v-show="isOpen">
|
||||
<div
|
||||
class="mt-1 rounded-lg bg-surface-white py-1 text-base border-2"
|
||||
<div class="w-full">
|
||||
<Combobox v-model="selectedValue" nullable>
|
||||
<Popover class="w-full" v-model:show="showOptions">
|
||||
<template #target="{ togglePopover }">
|
||||
<ComboboxInput
|
||||
ref="search"
|
||||
class="search-input form-input w-full focus-visible:!ring-0"
|
||||
type="text"
|
||||
:value="query"
|
||||
@change="
|
||||
(e) => {
|
||||
query = e.target.value
|
||||
showOptions = true
|
||||
}
|
||||
"
|
||||
autocomplete="off"
|
||||
@focus="() => togglePopover()"
|
||||
@keydown.delete.capture.stop="removeLastValue"
|
||||
/>
|
||||
</template>
|
||||
<template #body="{ isOpen, close }">
|
||||
<div v-show="isOpen">
|
||||
<div
|
||||
class="mt-1 rounded-lg bg-surface-white py-1 text-base border-2"
|
||||
>
|
||||
<ComboboxOptions
|
||||
class="my-1 min-h-[6rem] max-h-[12rem] overflow-y-auto px-1.5"
|
||||
static
|
||||
>
|
||||
<ComboboxOptions
|
||||
class="my-1 max-h-[12rem] overflow-y-auto px-1.5"
|
||||
static
|
||||
<ComboboxOption
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
:value="option"
|
||||
v-slot="{ active }"
|
||||
>
|
||||
<ComboboxOption
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
:value="option"
|
||||
v-slot="{ active }"
|
||||
<li
|
||||
:class="[
|
||||
'flex cursor-pointer items-center rounded px-2 py-1 text-base',
|
||||
{ 'bg-surface-gray-2': active },
|
||||
]"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
'flex cursor-pointer items-center rounded px-2 py-1 text-base',
|
||||
{ 'bg-surface-gray-2': active },
|
||||
]"
|
||||
>
|
||||
<div class="flex flex-col gap-1 p-1">
|
||||
<div class="text-base font-medium text-ink-gray-8">
|
||||
{{ option.description }}
|
||||
</div>
|
||||
<div class="text-sm text-ink-gray-5">
|
||||
{{ option.value }}
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 p-1">
|
||||
<div class="text-base font-medium text-ink-gray-8">
|
||||
{{ option.description }}
|
||||
</div>
|
||||
</li>
|
||||
</ComboboxOption>
|
||||
</ComboboxOptions>
|
||||
</div>
|
||||
<div class="text-sm text-ink-gray-5">
|
||||
{{ option.value }}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ComboboxOption>
|
||||
<div
|
||||
v-if="attrs.onCreate"
|
||||
class="absolute bottom-2 left-1 w-[98%] pt-2 bg-white border-t"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="w-full !justify-start"
|
||||
:label="__('Create New')"
|
||||
@click="attrs.onCreate(close)"
|
||||
>
|
||||
<template #prefix>
|
||||
<Plus class="h-4 w-4 stroke-1.5" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</ComboboxOptions>
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
</Combobox>
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
</Combobox>
|
||||
</div>
|
||||
<div v-if="values.length" class="grid grid-cols-2 gap-2 mt-1">
|
||||
<div
|
||||
v-for="value in values"
|
||||
class="flex items-center justify-between break-all bg-surface-gray-2 text-ink-gray-7 word-wrap p-2 rounded-md mr-2"
|
||||
>
|
||||
<span class="break-all">
|
||||
{{ value }}
|
||||
</span>
|
||||
<X
|
||||
class="size-4 stroke-1.5 cursor-pointer"
|
||||
@click="removeValue(value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <ErrorMessage class="mt-2 pl-2" v-if="error" :message="error" /> -->
|
||||
@@ -90,9 +103,9 @@ import {
|
||||
ComboboxOption,
|
||||
} from '@headlessui/vue'
|
||||
import { createResource, Popover, Button } from 'frappe-ui'
|
||||
import { ref, computed, nextTick } from 'vue'
|
||||
import { ref, computed, nextTick, useAttrs } from 'vue'
|
||||
import { watchDebounced } from '@vueuse/core'
|
||||
import { X } from 'lucide-vue-next'
|
||||
import { X, Plus } from 'lucide-vue-next'
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
@@ -124,7 +137,7 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const values = defineModel()
|
||||
|
||||
const attrs = useAttrs()
|
||||
const emails = ref([])
|
||||
const search = ref(null)
|
||||
const error = ref(null)
|
||||
|
||||
@@ -146,8 +146,8 @@
|
||||
<script setup>
|
||||
import { BookOpen, Users, Star, GraduationCap } from 'lucide-vue-next'
|
||||
import { computed, inject } from 'vue'
|
||||
import { Badge, Button, createResource } from 'frappe-ui'
|
||||
import { showToast, formatAmount } from '@/utils/'
|
||||
import { Badge, Button, createResource, toast } from 'frappe-ui'
|
||||
import { formatAmount } from '@/utils/'
|
||||
import { capture } from '@/telemetry'
|
||||
import { useRouter } from 'vue-router'
|
||||
import CertificationLinks from '@/components/CertificationLinks.vue'
|
||||
@@ -172,11 +172,7 @@ const video_link = computed(() => {
|
||||
|
||||
function enrollStudent() {
|
||||
if (!user.data) {
|
||||
showToast(
|
||||
__('Please Login'),
|
||||
__('You need to login first to enroll for this course'),
|
||||
'alert-circle'
|
||||
)
|
||||
toast.success(__('You need to login first to enroll for this course'))
|
||||
setTimeout(() => {
|
||||
window.location.href = `/login?redirect-to=${window.location.pathname}`
|
||||
}, 1000)
|
||||
@@ -192,11 +188,7 @@ function enrollStudent() {
|
||||
capture('enrolled_in_course', {
|
||||
course: props.course.data.name,
|
||||
})
|
||||
showToast(
|
||||
__('Success'),
|
||||
__('You have been enrolled in this course'),
|
||||
'check'
|
||||
)
|
||||
toast.success(__('You have been enrolled in this course'))
|
||||
setTimeout(() => {
|
||||
router.push({
|
||||
name: 'Lesson',
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Button, createResource, Tooltip } from 'frappe-ui'
|
||||
import { Button, createResource, Tooltip, toast } from 'frappe-ui'
|
||||
import { getCurrentInstance, inject, ref } from 'vue'
|
||||
import Draggable from 'vuedraggable'
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'
|
||||
@@ -162,7 +162,6 @@ import {
|
||||
} from 'lucide-vue-next'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import ChapterModal from '@/components/Modals/ChapterModal.vue'
|
||||
import { showToast } from '@/utils'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -215,7 +214,7 @@ const deleteLesson = createResource({
|
||||
},
|
||||
onSuccess() {
|
||||
outline.reload()
|
||||
showToast('Success', 'Lesson deleted successfully', 'check')
|
||||
toast.success(__('Lesson deleted successfully'))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -230,7 +229,7 @@ const updateLessonIndex = createResource({
|
||||
}
|
||||
},
|
||||
onSuccess() {
|
||||
showToast('Success', 'Lesson moved successfully', 'check')
|
||||
toast.success(__('Lesson moved successfully'))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -288,7 +287,7 @@ const deleteChapter = createResource({
|
||||
},
|
||||
onSuccess() {
|
||||
outline.reload()
|
||||
showToast('Success', 'Chapter deleted successfully', 'check')
|
||||
toast.success(__('Chapter deleted successfully'))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -317,11 +316,7 @@ const redirectToChapter = (chapter) => {
|
||||
event.preventDefault()
|
||||
if (props.allowEdit) return
|
||||
if (!user.data) {
|
||||
showToast(
|
||||
__('You are not enrolled'),
|
||||
__('Please enroll for this course to view this lesson'),
|
||||
'alert-circle'
|
||||
)
|
||||
toast.success(__('Please enroll for this course to view this lesson'))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -93,12 +93,11 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { createResource, TextEditor, Button, Dropdown } from 'frappe-ui'
|
||||
import { createResource, TextEditor, Button, Dropdown, toast } from 'frappe-ui'
|
||||
import { timeAgo } from '../utils'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import { ChevronLeft, MoreHorizontal } from 'lucide-vue-next'
|
||||
import { ref, inject, onMounted } from 'vue'
|
||||
import { createToast } from '../utils'
|
||||
|
||||
const showTopics = defineModel('showTopics')
|
||||
const newReply = ref('')
|
||||
@@ -192,14 +191,7 @@ const postReply = () => {
|
||||
replies.reload()
|
||||
},
|
||||
onError(err) {
|
||||
createToast({
|
||||
title: 'Error',
|
||||
text: err.messages?.[0] || err,
|
||||
icon: 'x',
|
||||
iconClasses: 'bg-surface-red-5 text-ink-white rounded-md p-px',
|
||||
position: 'top-center',
|
||||
timeout: 10,
|
||||
})
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
24
frontend/src/components/EmptyState.vue
Normal file
24
frontend/src/components/EmptyState.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center justify-center mt-60">
|
||||
<GraduationCap class="size-10 mx-auto stroke-1 text-ink-gray-5" />
|
||||
<div class="text-lg font-semibold text-ink-gray-7 mb-2.5">
|
||||
{{ __('No {0}').format(type?.toLowerCase()) }}
|
||||
</div>
|
||||
<div
|
||||
class="leading-5 text-base w-2/5 text-base text-center text-ink-gray-7"
|
||||
>
|
||||
{{
|
||||
__(
|
||||
'There are no {0} currently. Keep an eye out, fresh learning experiences are on the way!'
|
||||
).format(type?.toLowerCase())
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { BookOpen, GraduationCap } from 'lucide-vue-next'
|
||||
|
||||
const props = defineProps({
|
||||
type: String,
|
||||
})
|
||||
</script>
|
||||
@@ -17,10 +17,11 @@
|
||||
:debounce="300"
|
||||
/>
|
||||
<Button @click="() => (showForm = !showForm)">
|
||||
<template #icon>
|
||||
<Plus v-if="!showForm" class="h-3 w-3 stroke-1.5" />
|
||||
<X v-else class="h-3 w-3 stroke-1.5" />
|
||||
<template #prefix>
|
||||
<Plus v-if="!showForm" class="size-4 stroke-1.5" />
|
||||
<X v-else class="size-4 stroke-1.5" />
|
||||
</template>
|
||||
{{ showForm ? __('Close') : __('New') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,10 +17,11 @@
|
||||
:debounce="300"
|
||||
/>
|
||||
<Button @click="() => (showForm = !showForm)">
|
||||
<template #icon>
|
||||
<Plus v-if="!showForm" class="h-3 w-3 stroke-1.5" />
|
||||
<X v-else class="h-3 w-3 stroke-1.5" />
|
||||
<template #prefix>
|
||||
<Plus v-if="!showForm" class="size-4 stroke-1.5" />
|
||||
<X v-else class="size-4 stroke-1.5" />
|
||||
</template>
|
||||
{{ showForm ? __('Close') : __('New') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
<div class="mb-4">
|
||||
<div class="mb-1.5 text-sm text-ink-gray-5">
|
||||
{{ __('Announcement') }}
|
||||
<span class="text-ink-red-3">*</span>
|
||||
</div>
|
||||
<TextEditor
|
||||
:fixedMenu="true"
|
||||
@@ -43,9 +44,8 @@
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Dialog, Input, TextEditor, createResource } from 'frappe-ui'
|
||||
import { Dialog, Input, TextEditor, createResource, toast } from 'frappe-ui'
|
||||
import { reactive } from 'vue'
|
||||
import { showToast } from '@/utils/'
|
||||
|
||||
const show = defineModel()
|
||||
|
||||
@@ -87,22 +87,21 @@ const makeAnnouncement = (close) => {
|
||||
{
|
||||
validate() {
|
||||
if (!props.students.length) {
|
||||
return 'No students in this batch'
|
||||
return __('No students in this batch')
|
||||
}
|
||||
if (!announcement.subject) {
|
||||
return 'Subject is required'
|
||||
return __('Subject is required')
|
||||
}
|
||||
if (!announcement.announcement) {
|
||||
return __('Announcement is required')
|
||||
}
|
||||
},
|
||||
onSuccess() {
|
||||
close()
|
||||
showToast(
|
||||
__('Success'),
|
||||
__('Announcement has been sent successfully'),
|
||||
'check'
|
||||
)
|
||||
toast.success(__('Announcement has been sent successfully'))
|
||||
},
|
||||
onError(err) {
|
||||
showToast(__('Error'), __(err.messages?.[0] || err), 'alert-circle')
|
||||
toast.error(__(err.messages?.[0] || err))
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -25,21 +25,39 @@
|
||||
v-model="assessment"
|
||||
:doctype="assessmentType"
|
||||
:label="__('Assessment')"
|
||||
:onCreate="
|
||||
(value, close) => {
|
||||
close()
|
||||
if (assessmentType === 'LMS Quiz') {
|
||||
router.push({
|
||||
name: 'QuizForm',
|
||||
params: {
|
||||
quizID: 'new',
|
||||
},
|
||||
})
|
||||
} else if (assessmentType === 'LMS Assignment') {
|
||||
router.push({
|
||||
name: 'Assignments',
|
||||
})
|
||||
}
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Dialog, FormControl, createResource } from 'frappe-ui'
|
||||
import { Dialog, FormControl, createResource, toast } from 'frappe-ui'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { showToast } from '@/utils'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const show = defineModel()
|
||||
const assessmentType = ref(null)
|
||||
const assessment = ref(null)
|
||||
const assessments = defineModel('assessments')
|
||||
const router = useRouter()
|
||||
|
||||
const props = defineProps({
|
||||
batch: {
|
||||
@@ -70,7 +88,7 @@ const addAssessment = (close) => {
|
||||
{
|
||||
onSuccess(data) {
|
||||
assessments.value.reload()
|
||||
showToast(__('Success'), __('Assessment added successfully'), 'check')
|
||||
toast.success(__('Assessment added successfully'))
|
||||
close()
|
||||
},
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
@change="(val) => (assignment.question = val)"
|
||||
:editable="true"
|
||||
:fixedMenu="true"
|
||||
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem]"
|
||||
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem] max-h-[18rem] overflow-y-auto"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -64,9 +64,8 @@
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { Button, Dialog, FormControl, TextEditor } from 'frappe-ui'
|
||||
import { Button, Dialog, FormControl, TextEditor, toast } from 'frappe-ui'
|
||||
import { computed, reactive, watch } from 'vue'
|
||||
import { showToast } from '@/utils'
|
||||
|
||||
const show = defineModel()
|
||||
const assignments = defineModel<Assignments>('assignments')
|
||||
@@ -123,11 +122,7 @@ const saveAssignment = () => {
|
||||
{
|
||||
onSuccess() {
|
||||
show.value = false
|
||||
showToast(
|
||||
__('Success'),
|
||||
__('Assignment created successfully'),
|
||||
'check'
|
||||
)
|
||||
toast.success(__('Assignment created successfully'))
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -140,11 +135,7 @@ const saveAssignment = () => {
|
||||
{
|
||||
onSuccess() {
|
||||
show.value = false
|
||||
showToast(
|
||||
__('Success'),
|
||||
__('Assignment updated successfully'),
|
||||
'check'
|
||||
)
|
||||
toast.success(__('Assignment updated successfully'))
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -19,32 +19,43 @@
|
||||
v-model="course"
|
||||
:label="__('Course')"
|
||||
:required="true"
|
||||
:onCreate="
|
||||
(value, close) => {
|
||||
close()
|
||||
router.push({
|
||||
name: 'CourseForm',
|
||||
params: {
|
||||
courseName: 'new',
|
||||
},
|
||||
})
|
||||
}
|
||||
"
|
||||
/>
|
||||
<Link
|
||||
doctype="Course Evaluator"
|
||||
v-model="evaluator"
|
||||
:label="__('Evaluator')"
|
||||
:onCreate="(value, close) => openSettings(close)"
|
||||
:onCreate="(value, close) => openSettings('Evaluators', close)"
|
||||
class="mt-4"
|
||||
/>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Dialog, createResource } from 'frappe-ui'
|
||||
import { Dialog, createResource, toast } from 'frappe-ui'
|
||||
import { ref, inject } from 'vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import { showToast } from '@/utils'
|
||||
import { useOnboarding } from 'frappe-ui/frappe'
|
||||
import { useSettings } from '@/stores/settings'
|
||||
import { openSettings } from '@/utils'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const show = defineModel()
|
||||
const course = ref(null)
|
||||
const evaluator = ref(null)
|
||||
const user = inject('$user')
|
||||
const courses = defineModel('courses')
|
||||
const router = useRouter()
|
||||
const { updateOnboardingStep } = useOnboarding('learning')
|
||||
const settingsStore = useSettings()
|
||||
|
||||
const props = defineProps({
|
||||
batch: {
|
||||
@@ -83,15 +94,9 @@ const addCourse = (close) => {
|
||||
evaluator.value = null
|
||||
},
|
||||
onError(err) {
|
||||
showToast('Error', err.message[0] || err, 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const openSettings = (close) => {
|
||||
close()
|
||||
settingsStore.activeTab = 'Evaluators'
|
||||
settingsStore.isSettingsOpen = true
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -14,7 +14,13 @@
|
||||
<div class="text-xl font-semibold">
|
||||
{{ student.full_name }}
|
||||
</div>
|
||||
<Badge :theme="student.progress === 100 ? 'green' : 'red'">
|
||||
<Badge
|
||||
v-if="
|
||||
Object.keys(student.assessments).length ||
|
||||
Object.keys(student.courses).length
|
||||
"
|
||||
:theme="student.progress === 100 ? 'green' : 'red'"
|
||||
>
|
||||
{{ student.progress }}% {{ __('Complete') }}
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -26,7 +32,10 @@
|
||||
|
||||
<div class="space-y-8">
|
||||
<!-- Assessments -->
|
||||
<div class="space-y-2 text-sm">
|
||||
<div
|
||||
v-if="Object.keys(student.assessments).length"
|
||||
class="space-y-2 text-sm"
|
||||
>
|
||||
<div class="flex items-center border-b pb-1 font-medium">
|
||||
<span class="flex-1">
|
||||
{{ __('Assessment') }}
|
||||
@@ -73,7 +82,10 @@
|
||||
</div>
|
||||
|
||||
<!-- Courses -->
|
||||
<div class="space-y-2 text-sm">
|
||||
<div
|
||||
v-if="Object.keys(student.courses).length"
|
||||
class="space-y-2 text-sm"
|
||||
>
|
||||
<div class="flex items-center border-b pb-1 font-medium">
|
||||
<span class="flex-1">
|
||||
{{ __('Courses') }}
|
||||
|
||||
@@ -62,9 +62,8 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import { inject, reactive } from 'vue'
|
||||
import { createResource, Dialog, FormControl, Switch } from 'frappe-ui'
|
||||
import { createResource, Dialog, FormControl, Switch, toast } from 'frappe-ui'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import { showToast } from '@/utils'
|
||||
|
||||
const show = defineModel()
|
||||
const dayjs = inject('$dayjs')
|
||||
@@ -112,13 +111,13 @@ const generateCertificates = (close) => {
|
||||
},
|
||||
{
|
||||
onError(err) {
|
||||
showToast(__('Error'), err.messages?.[0] || err, 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
close()
|
||||
showToast(__('Success'), __('Certificates generated successfully'), 'check')
|
||||
toast.success(__('Certificates generated successfully'))
|
||||
}
|
||||
|
||||
const getCourses = () => {
|
||||
|
||||
@@ -76,9 +76,10 @@ import {
|
||||
FileUploader,
|
||||
FormControl,
|
||||
Switch,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import { reactive, watch, inject } from 'vue'
|
||||
import { showToast, getFileSize } from '@/utils/'
|
||||
import { getFileSize } from '@/utils/'
|
||||
import { capture } from '@/telemetry'
|
||||
import { FileText, X } from 'lucide-vue-next'
|
||||
import { useOnboarding } from 'frappe-ui/frappe'
|
||||
@@ -150,21 +151,17 @@ const addChapter = async (close) => {
|
||||
onSuccess(data) {
|
||||
cleanChapter()
|
||||
outline.value.reload()
|
||||
showToast(
|
||||
__('Success'),
|
||||
__('Chapter added successfully'),
|
||||
'check'
|
||||
)
|
||||
toast.success(__('Chapter added successfully'))
|
||||
},
|
||||
onError(err) {
|
||||
showToast(__('Error'), err.messages?.[0] || err, 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
}
|
||||
)
|
||||
close()
|
||||
},
|
||||
onError(err) {
|
||||
showToast(__('Error'), err.messages?.[0] || err, 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -196,11 +193,11 @@ const editChapter = (close) => {
|
||||
},
|
||||
onSuccess() {
|
||||
outline.value.reload()
|
||||
showToast(__('Success'), __('Chapter updated successfully'), 'check')
|
||||
toast.success(__('Chapter updated successfully'))
|
||||
close()
|
||||
},
|
||||
onError(err) {
|
||||
showToast(__('Error'), err.messages?.[0] || err, 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -34,9 +34,15 @@
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Dialog, FormControl, TextEditor, createResource } from 'frappe-ui'
|
||||
import {
|
||||
Dialog,
|
||||
FormControl,
|
||||
TextEditor,
|
||||
createResource,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import { reactive } from 'vue'
|
||||
import { showToast, singularize } from '@/utils'
|
||||
import { singularize } from '@/utils'
|
||||
|
||||
const topics = defineModel('reloadTopics')
|
||||
|
||||
@@ -115,7 +121,7 @@ const submitTopic = (close) => {
|
||||
)
|
||||
},
|
||||
onError(err) {
|
||||
showToast('Error', err.message, 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -93,10 +93,11 @@ import {
|
||||
Button,
|
||||
createResource,
|
||||
TextEditor,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import { reactive, watch } from 'vue'
|
||||
import { FileText, X } from 'lucide-vue-next'
|
||||
import { getFileSize, showToast, escapeHTML } from '@/utils'
|
||||
import { getFileSize, escapeHTML } from '@/utils'
|
||||
|
||||
const reloadProfile = defineModel('reloadProfile')
|
||||
|
||||
@@ -155,7 +156,7 @@ const saveProfile = (close) => {
|
||||
reloadProfile.value.reload()
|
||||
},
|
||||
onError(err) {
|
||||
showToast('Error', err.messages?.[0] || err, 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
<script setup>
|
||||
import { Dialog, createResource, Select, FormControl } from 'frappe-ui'
|
||||
import { reactive, watch, inject } from 'vue'
|
||||
import { createToast, formatTime } from '@/utils/'
|
||||
import { formatTime } from '@/utils/'
|
||||
|
||||
const user = inject('$user')
|
||||
const dayjs = inject('$dayjs')
|
||||
@@ -148,14 +148,7 @@ function submitEvaluation(close) {
|
||||
unavailabilityMessage = false
|
||||
}
|
||||
|
||||
createToast({
|
||||
title: unavailabilityMessage ? __('Evaluator is Unavailable') : '',
|
||||
text: message,
|
||||
icon: unavailabilityMessage ? 'alert-circle' : 'x',
|
||||
iconClasses: 'bg-yellow-600 text-ink-white rounded-md p-px',
|
||||
position: 'top-center',
|
||||
timeout: 10,
|
||||
})
|
||||
toast.warn(__('Evaluator is unavailable'))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -144,6 +144,7 @@ import {
|
||||
Tabs,
|
||||
Tooltip,
|
||||
Textarea,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import {
|
||||
User,
|
||||
@@ -157,7 +158,7 @@ import {
|
||||
ClipboardList,
|
||||
} from 'lucide-vue-next'
|
||||
import { inject, reactive, watch, ref, computed } from 'vue'
|
||||
import { formatTime, showToast } from '@/utils'
|
||||
import { formatTime } from '@/utils'
|
||||
import Rating from '@/components/Controls/Rating.vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
|
||||
@@ -252,7 +253,7 @@ const saveEvaluation = () => {
|
||||
} else {
|
||||
show.value = false
|
||||
}
|
||||
showToast(__('Success'), __('Evaluation saved successfully'), 'check')
|
||||
toast.success(__('Evaluation saved successfully'))
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -307,7 +308,7 @@ const saveCertificate = () => {
|
||||
{},
|
||||
{
|
||||
onSuccess: () => {
|
||||
showToast(__('Success'), __('Certificate saved successfully'), 'check')
|
||||
toast.success(__('Certificate saved successfully'))
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -64,10 +64,10 @@
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Dialog, FileUploader, Button, createResource } from 'frappe-ui'
|
||||
import { Dialog, FileUploader, Button, createResource, toast } from 'frappe-ui'
|
||||
import { FileText } from 'lucide-vue-next'
|
||||
import { ref, inject } from 'vue'
|
||||
import { createToast, getFileSize } from '@/utils/'
|
||||
import { getFileSize } from '@/utils/'
|
||||
|
||||
const resume = ref(null)
|
||||
const show = defineModel()
|
||||
@@ -112,24 +112,12 @@ const submitResume = (close) => {
|
||||
}
|
||||
},
|
||||
onSuccess() {
|
||||
createToast({
|
||||
title: 'Success',
|
||||
text: 'Your application has been submitted',
|
||||
icon: 'check',
|
||||
iconClasses: 'bg-surface-green-3 text-ink-white rounded-md p-px',
|
||||
})
|
||||
toast.success('Your application has been submitted successfully')
|
||||
application.value.reload()
|
||||
close()
|
||||
},
|
||||
onError(err) {
|
||||
createToast({
|
||||
title: 'Error',
|
||||
text: err.messages?.[0] || err,
|
||||
icon: 'x',
|
||||
iconClasses: 'bg-surface-red-5 text-ink-white rounded-md p-px',
|
||||
position: 'top-center',
|
||||
timeout: 10,
|
||||
})
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -94,9 +94,10 @@ import {
|
||||
Tooltip,
|
||||
FormControl,
|
||||
Autocomplete,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import { reactive, inject, onMounted } from 'vue'
|
||||
import { getTimezones, createToast, getUserTimezone } from '@/utils/'
|
||||
import { getTimezones, getUserTimezone } from '@/utils/'
|
||||
|
||||
const liveClasses = defineModel('reloadLiveClasses')
|
||||
const show = defineModel()
|
||||
@@ -202,14 +203,7 @@ const submitLiveClass = (close) => {
|
||||
close()
|
||||
},
|
||||
onError(err) {
|
||||
createToast({
|
||||
title: 'Error',
|
||||
text: err.messages?.[0] || err,
|
||||
icon: 'x',
|
||||
iconClasses: 'bg-surface-red-5 text-ink-white rounded-md p-px',
|
||||
position: 'top-center',
|
||||
timeout: 10,
|
||||
})
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -30,11 +30,10 @@
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Dialog, createResource } from 'frappe-ui'
|
||||
import { Dialog, createResource, toast } from 'frappe-ui'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import { reactive, watch } from 'vue'
|
||||
import IconPicker from '@/components/Controls/IconPicker.vue'
|
||||
import { showToast } from '@/utils'
|
||||
|
||||
const sidebar = defineModel('reloadSidebar')
|
||||
const show = defineModel()
|
||||
@@ -78,10 +77,10 @@ const addWebPage = (close) => {
|
||||
onSuccess() {
|
||||
sidebar.value.reload()
|
||||
close()
|
||||
showToast('Success', 'Web page added to sidebar', 'check')
|
||||
toast.success(__('Web page added to sidebar'))
|
||||
},
|
||||
onError(err) {
|
||||
showToast('Error', err.message[0] || err, 'x')
|
||||
toast.error(err.message[0] || err)
|
||||
close()
|
||||
},
|
||||
}
|
||||
|
||||
@@ -121,10 +121,10 @@ import {
|
||||
createResource,
|
||||
Switch,
|
||||
Button,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import { computed, watch, reactive, ref, inject } from 'vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import { showToast } from '@/utils'
|
||||
import { useOnboarding } from 'frappe-ui/frappe'
|
||||
|
||||
const show = defineModel()
|
||||
@@ -260,7 +260,7 @@ const addQuestion = () => {
|
||||
})
|
||||
},
|
||||
onError(err) {
|
||||
showToast(__('Error'), __(err.messages?.[0] || err), 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -278,12 +278,12 @@ const addQuestionRow = (question) => {
|
||||
updateOnboardingStep('create_first_quiz')
|
||||
|
||||
show.value = false
|
||||
showToast(__('Success'), __('Question added successfully'), 'check')
|
||||
toast.success(__('Question added successfully'))
|
||||
quiz.value.reload()
|
||||
show.value = false
|
||||
},
|
||||
onError(err) {
|
||||
showToast(__('Error'), __(err.messages?.[0] || err), 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
show.value = false
|
||||
},
|
||||
}
|
||||
@@ -328,18 +328,14 @@ const updateQuestion = () => {
|
||||
{
|
||||
onSuccess() {
|
||||
show.value = false
|
||||
showToast(
|
||||
__('Success'),
|
||||
__('Question updated successfully'),
|
||||
'check'
|
||||
)
|
||||
toast.success(__('Question updated successfully'))
|
||||
quiz.value.reload()
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
onError(err) {
|
||||
showToast(__('Error'), __(err.messages?.[0] || err), 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -32,10 +32,9 @@
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Dialog, Textarea, createResource } from 'frappe-ui'
|
||||
import { Dialog, Textarea, createResource, toast } from 'frappe-ui'
|
||||
import { reactive } from 'vue'
|
||||
import Rating from '@/components/Controls/Rating.vue'
|
||||
import { createToast } from '@/utils/'
|
||||
|
||||
const show = defineModel()
|
||||
const reviews = defineModel('reloadReviews')
|
||||
@@ -78,11 +77,7 @@ function submitReview(close) {
|
||||
hasReviewed.value.reload()
|
||||
},
|
||||
onError(err) {
|
||||
createToast({
|
||||
text: err.messages?.[0] || err,
|
||||
icon: 'x',
|
||||
iconClasses: 'text-ink-red-4 bg-surface-red-4',
|
||||
})
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
})
|
||||
close()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Dialog v-model="show" :options="{ size: '4xl' }">
|
||||
<Dialog v-model="show" :options="{ size: '5xl' }">
|
||||
<template #body>
|
||||
<div class="flex h-[calc(100vh_-_8rem)]">
|
||||
<div class="flex w-52 shrink-0 flex-col bg-surface-gray-2 p-2">
|
||||
|
||||
@@ -19,19 +19,25 @@
|
||||
doctype="User"
|
||||
v-model="student"
|
||||
:filters="{ ignore_user_type: 1 }"
|
||||
:onCreate="
|
||||
(value, close) => {
|
||||
openSettings('Members', close)
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Dialog, createResource } from 'frappe-ui'
|
||||
import { Dialog, createResource, toast } from 'frappe-ui'
|
||||
import { ref, inject } from 'vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import { showToast } from '@/utils'
|
||||
import { useOnboarding } from 'frappe-ui/frappe'
|
||||
import { openSettings } from '@/utils'
|
||||
|
||||
const students = defineModel('reloadStudents')
|
||||
const batchModal = defineModel('batchModal')
|
||||
const student = ref()
|
||||
const user = inject('$user')
|
||||
const { updateOnboardingStep } = useOnboarding('learning')
|
||||
@@ -66,11 +72,12 @@ const addStudent = (close) => {
|
||||
updateOnboardingStep('add_batch_student')
|
||||
|
||||
students.value.reload()
|
||||
batchModal.value.reload()
|
||||
student.value = null
|
||||
close()
|
||||
},
|
||||
onError(err) {
|
||||
showToast(__('Error'), __(err.messages?.[0] || err), 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -291,9 +291,9 @@ import {
|
||||
ListView,
|
||||
TextEditor,
|
||||
FormControl,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import { ref, watch, reactive, inject, computed } from 'vue'
|
||||
import { createToast, showToast } from '@/utils/'
|
||||
import { CheckCircle, XCircle, MinusCircle } from 'lucide-vue-next'
|
||||
import { timeAgo } from '@/utils'
|
||||
import { useRouter } from 'vue-router'
|
||||
@@ -494,12 +494,7 @@ const getAnswers = () => {
|
||||
const checkAnswer = () => {
|
||||
let answers = getAnswers()
|
||||
if (!answers.length) {
|
||||
createToast({
|
||||
title: 'Please select an option',
|
||||
icon: 'alert-circle',
|
||||
iconClasses: 'text-yellow-600 bg-yellow-100 rounded-full',
|
||||
position: 'top-center',
|
||||
})
|
||||
toast.warn(__('Please select an option'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -589,7 +584,7 @@ const createSubmission = () => {
|
||||
const errorTitle = err?.message || ''
|
||||
if (errorTitle.includes('MaximumAttemptsExceededError')) {
|
||||
const errorMessage = err.messages?.[0] || err
|
||||
showToast(__('Error'), __(errorMessage), 'x')
|
||||
toast.error(__(errorMessage))
|
||||
setTimeout(() => {
|
||||
window.location.reload()
|
||||
}, 3000)
|
||||
|
||||
@@ -27,9 +27,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Button, Badge } from 'frappe-ui'
|
||||
import { Button, Badge, toast } from 'frappe-ui'
|
||||
import SettingFields from '@/components/SettingFields.vue'
|
||||
import { showToast } from '@/utils'
|
||||
|
||||
const props = defineProps({
|
||||
fields: {
|
||||
@@ -61,7 +60,7 @@ const update = () => {
|
||||
{},
|
||||
{
|
||||
onError(err) {
|
||||
showToast(__('Error'), err.messages?.[0] || err, 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -21,14 +21,28 @@
|
||||
</header>
|
||||
|
||||
<div class="md:w-3/4 md:mx-auto py-5 mx-5">
|
||||
<div class="grid grid-cols-3 gap-5 mb-5">
|
||||
<FormControl v-model="titleFilter" :placeholder="__('Search by title')" />
|
||||
<FormControl
|
||||
v-model="typeFilter"
|
||||
type="select"
|
||||
:options="assignmentTypes"
|
||||
:placeholder="__('Type')"
|
||||
/>
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<div
|
||||
v-if="assignmentCount"
|
||||
class="text-xl font-semibold text-ink-gray-7 mb-4"
|
||||
>
|
||||
{{ __('{0} Assignments').format(assignmentCount) }}
|
||||
</div>
|
||||
<div
|
||||
v-if="assignments.data?.length || assigmentCount > 0"
|
||||
class="grid grid-cols-2 gap-5"
|
||||
>
|
||||
<FormControl
|
||||
v-model="titleFilter"
|
||||
:placeholder="__('Search by title')"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="typeFilter"
|
||||
type="select"
|
||||
:options="assignmentTypes"
|
||||
:placeholder="__('Type')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ListView
|
||||
v-if="assignments.data?.length"
|
||||
@@ -46,22 +60,7 @@
|
||||
}"
|
||||
>
|
||||
</ListView>
|
||||
<div
|
||||
v-else
|
||||
class="text-center p-5 text-ink-gray-5 mt-52 w-3/4 md:w-1/2 mx-auto space-y-2"
|
||||
>
|
||||
<Pencil class="size-10 mx-auto stroke-1 text-ink-gray-4" />
|
||||
<div class="text-xl font-medium">
|
||||
{{ __('No assignments found') }}
|
||||
</div>
|
||||
<div class="leading-5">
|
||||
{{
|
||||
__(
|
||||
'You have not created any assignments yet. To create a new assignment, click on the "New" button above.'
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<EmptyState v-else type="Assignments" />
|
||||
<div
|
||||
v-if="assignments.data && assignments.hasNextPage"
|
||||
class="flex justify-center my-5"
|
||||
@@ -81,16 +80,18 @@
|
||||
import {
|
||||
Breadcrumbs,
|
||||
Button,
|
||||
call,
|
||||
createListResource,
|
||||
FormControl,
|
||||
ListView,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { computed, inject, onMounted, ref, watch } from 'vue'
|
||||
import { Plus, Pencil } from 'lucide-vue-next'
|
||||
import { Plus } from 'lucide-vue-next'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import AssignmentForm from '@/components/Modals/AssignmentForm.vue'
|
||||
import EmptyState from '@/components/EmptyState.vue'
|
||||
|
||||
const user = inject('$user')
|
||||
const dayjs = inject('$dayjs')
|
||||
@@ -98,6 +99,7 @@ const titleFilter = ref('')
|
||||
const typeFilter = ref('')
|
||||
const showAssignmentForm = ref(false)
|
||||
const assignmentID = ref('new')
|
||||
const assignmentCount = ref(0)
|
||||
const { brand } = sessionStore()
|
||||
const router = useRouter()
|
||||
const readOnlyMode = window.read_only_mode
|
||||
@@ -106,7 +108,7 @@ onMounted(() => {
|
||||
if (!user.data?.is_moderator && !user.data?.is_instructor) {
|
||||
router.push({ name: 'Courses' })
|
||||
}
|
||||
|
||||
getAssignmentCount()
|
||||
titleFilter.value = router.currentRoute.value.query.title
|
||||
typeFilter.value = router.currentRoute.value.query.type
|
||||
})
|
||||
@@ -179,6 +181,14 @@ const assignmentColumns = computed(() => {
|
||||
]
|
||||
})
|
||||
|
||||
const getAssignmentCount = () => {
|
||||
call('frappe.client.get_count', {
|
||||
doctype: 'LMS Assignment',
|
||||
}).then((data) => {
|
||||
assignmentCount.value = data
|
||||
})
|
||||
}
|
||||
|
||||
const assignmentTypes = computed(() => {
|
||||
let types = ['', 'Document', 'Image', 'PDF', 'URL', 'Text']
|
||||
return types.map((type) => {
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
<BatchDashboard :batch="batch" :isStudent="isStudent" />
|
||||
</div>
|
||||
<div v-else-if="tab.label == 'Dashboard'">
|
||||
<BatchStudents :batch="batch.data" />
|
||||
<BatchStudents :batch="batch" />
|
||||
</div>
|
||||
<div v-else-if="tab.label == 'Classes'">
|
||||
<LiveClass :batch="batch.data.name" />
|
||||
@@ -357,6 +357,9 @@ watch(tabIndex, () => {
|
||||
|
||||
const canMakeAnnouncement = () => {
|
||||
if (readOnlyMode) return false
|
||||
|
||||
if (!batch.data?.students?.length) return false
|
||||
|
||||
return user.data?.is_moderator || user.data?.is_evaluator
|
||||
}
|
||||
|
||||
|
||||
@@ -6,67 +6,45 @@
|
||||
<Breadcrumbs :items="breadcrumbs" />
|
||||
</header>
|
||||
<div class="m-5 pb-10">
|
||||
<div>
|
||||
<div class="text-3xl font-semibold text-ink-gray-9">
|
||||
{{ batch.data.title }}
|
||||
</div>
|
||||
<div class="my-3 leading-6 text-ink-gray-7">
|
||||
{{ batch.data.description }}
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col gap-2 lg:gap-0 lg:flex-row lg:items-center space-x-0 md:space-x-5 lg:w-1/2"
|
||||
>
|
||||
<div
|
||||
v-if="batch.data?.courses?.length"
|
||||
class="flex items-center text-ink-gray-7"
|
||||
>
|
||||
<BookOpen class="h-4 w-4 mr-2 stroke-1.5" />
|
||||
<span> {{ batch.data?.courses?.length }} {{ __('Courses') }} </span>
|
||||
<div class="flex justify-between w-full">
|
||||
<div class="md:w-2/3">
|
||||
<div class="text-3xl font-semibold text-ink-gray-9">
|
||||
{{ batch.data.title }}
|
||||
</div>
|
||||
<span v-if="batch.data?.courses?.length" class="hidden lg:block"
|
||||
>·</span
|
||||
>
|
||||
<DateRange
|
||||
:startDate="batch.data.start_date"
|
||||
:endDate="batch.data.end_date"
|
||||
/>
|
||||
<span class="hidden lg:block" v-if="batch.data.start_date"
|
||||
>·</span
|
||||
>
|
||||
<div class="flex items-center text-ink-gray-7">
|
||||
<Clock class="h-4 w-4 mr-2 stroke-1.5" />
|
||||
<span>
|
||||
{{ formatTime(batch.data.start_time) }} -
|
||||
{{ formatTime(batch.data.end_time) }}
|
||||
</span>
|
||||
<div class="my-3 leading-6 text-ink-gray-7">
|
||||
{{ batch.data.description }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex avatar-group overlap mt-3">
|
||||
<div
|
||||
class="h-6 mr-1"
|
||||
:class="{
|
||||
'avatar-group overlap': batch.data.instructors.length > 1,
|
||||
}"
|
||||
>
|
||||
<UserAvatar
|
||||
v-for="instructor in batch.data.instructors"
|
||||
:user="instructor"
|
||||
/>
|
||||
<div class="flex avatar-group overlap">
|
||||
<div
|
||||
class="h-6 mr-1"
|
||||
:class="{
|
||||
'avatar-group overlap': batch.data.instructors.length > 1,
|
||||
}"
|
||||
>
|
||||
<UserAvatar
|
||||
v-for="instructor in batch.data.instructors"
|
||||
:user="instructor"
|
||||
/>
|
||||
</div>
|
||||
<CourseInstructors :instructors="batch.data.instructors" />
|
||||
</div>
|
||||
<CourseInstructors :instructors="batch.data.instructors" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid lg:grid-cols-[60%,20%] gap-4 lg:gap-20 mt-10">
|
||||
<div class="order-2 lg:order-none">
|
||||
<div
|
||||
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-6"
|
||||
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-10"
|
||||
v-html="batch.data.batch_details"
|
||||
></div>
|
||||
</div>
|
||||
<div class="hidden md:block">
|
||||
<BatchOverlay :batch="batch" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="grid lg:grid-cols-[60%,20%] gap-4 lg:gap-20 mt-10">
|
||||
<div class="order-2 lg:order-none">
|
||||
|
||||
</div>
|
||||
<div class="order-1 lg:order-none">
|
||||
<BatchOverlay :batch="batch" />
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
<div v-if="batch.data.courses.length">
|
||||
<div class="flex items-center mt-10">
|
||||
<div class="text-2xl font-semibold">
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
{{ __('Save') }}
|
||||
</Button>
|
||||
</header>
|
||||
<div class="w-3/4 mx-auto py-5">
|
||||
<div class="">
|
||||
<div class="py-5">
|
||||
<div class="px-20 pb-5 space-y-5 border-b mb-5">
|
||||
<div class="text-lg text-ink-gray-9 font-semibold mb-4">
|
||||
{{ __('Details') }}
|
||||
</div>
|
||||
<div class="space-y-10 mb-4">
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-5">
|
||||
<div class="space-y-5">
|
||||
<FormControl
|
||||
v-model="batch.title"
|
||||
:label="__('Title')"
|
||||
@@ -26,31 +26,162 @@
|
||||
doctype="User"
|
||||
:label="__('Instructors')"
|
||||
:required="true"
|
||||
:onCreate="(close) => openSettings('Members', close)"
|
||||
:filters="{ ignore_user_type: 1 }"
|
||||
/>
|
||||
</div>
|
||||
<FormControl
|
||||
v-model="batch.description"
|
||||
:label="__('Short Description')"
|
||||
type="textarea"
|
||||
:rows="8"
|
||||
:placeholder="__('Short description of the batch')"
|
||||
:required="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-10">
|
||||
<div class="flex flex-col space-y-5">
|
||||
<FormControl
|
||||
v-model="batch.published"
|
||||
type="checkbox"
|
||||
:label="__('Published')"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.allow_self_enrollment"
|
||||
type="checkbox"
|
||||
:label="__('Allow self enrollment')"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.certification"
|
||||
type="checkbox"
|
||||
:label="__('Certification')"
|
||||
/>
|
||||
</div>
|
||||
<div class="px-20 pb-5 space-y-5 border-b mb-5">
|
||||
<div class="text-lg text-ink-gray-9 font-semibold mb-4">
|
||||
{{ __('Settings') }}
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-5">
|
||||
<FormControl
|
||||
v-model="batch.published"
|
||||
type="checkbox"
|
||||
:label="__('Published')"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.allow_self_enrollment"
|
||||
type="checkbox"
|
||||
:label="__('Allow self enrollment')"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.certification"
|
||||
type="checkbox"
|
||||
:label="__('Certification')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-20 pb-5 space-y-5 border-b mb-5">
|
||||
<div class="text-lg text-ink-gray-9 font-semibold mb-4">
|
||||
{{ __('Date and Time') }}
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-10">
|
||||
<div class="space-y-5">
|
||||
<FormControl
|
||||
v-model="batch.start_date"
|
||||
:label="__('Start Date')"
|
||||
type="date"
|
||||
class="mb-4"
|
||||
:required="true"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.end_date"
|
||||
:label="__('End Date')"
|
||||
type="date"
|
||||
class="mb-4"
|
||||
:required="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-5">
|
||||
<FormControl
|
||||
v-model="batch.start_time"
|
||||
:label="__('Start Time')"
|
||||
type="time"
|
||||
class="mb-4"
|
||||
:required="true"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.end_time"
|
||||
:label="__('End Time')"
|
||||
type="time"
|
||||
class="mb-4"
|
||||
:required="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-5">
|
||||
<FormControl
|
||||
v-model="batch.timezone"
|
||||
:label="__('Timezone')"
|
||||
type="text"
|
||||
:placeholder="__('Example: IST (+5:30)')"
|
||||
class="mb-4"
|
||||
:required="true"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.evaluation_end_date"
|
||||
:label="__('Evaluation End Date')"
|
||||
type="date"
|
||||
class="mb-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-20 pb-5 space-y-5 border-b mb-5">
|
||||
<div>
|
||||
<label class="block text-sm text-ink-gray-5 mb-1">
|
||||
{{ __('Batch Details') }}
|
||||
<span class="text-ink-red-3">*</span>
|
||||
</label>
|
||||
<TextEditor
|
||||
:content="batch.batch_details"
|
||||
@change="(val) => (batch.batch_details = val)"
|
||||
:editable="true"
|
||||
:fixedMenu="true"
|
||||
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem] max-h-[20rem] overflow-y-scroll mb-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-20 pb-5 space-y-5 border-b mb-5">
|
||||
<div class="text-lg text-ink-gray-9 font-semibold mb-4">
|
||||
{{ __('Configurations') }}
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-10">
|
||||
<div class="space-y-5">
|
||||
<FormControl
|
||||
v-model="batch.seat_count"
|
||||
:label="__('Seat Count')"
|
||||
type="number"
|
||||
class="mb-4"
|
||||
:placeholder="__('Number of seats available')"
|
||||
/>
|
||||
<Link
|
||||
doctype="Email Template"
|
||||
:label="__('Email Template')"
|
||||
v-model="batch.confirmation_email_template"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-5">
|
||||
<FormControl
|
||||
v-model="batch.medium"
|
||||
type="select"
|
||||
:options="[
|
||||
{
|
||||
label: 'Online',
|
||||
value: 'Online',
|
||||
},
|
||||
{
|
||||
label: 'Offline',
|
||||
value: 'Offline',
|
||||
},
|
||||
]"
|
||||
:label="__('Medium')"
|
||||
class="mb-4"
|
||||
/>
|
||||
<Link
|
||||
doctype="LMS Category"
|
||||
:label="__('Category')"
|
||||
v-model="batch.category"
|
||||
:onCreate="(value, close) => openSettings('Categories', close)"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-5">
|
||||
<div>
|
||||
<div class="text-xs text-ink-gray-5 mb-2">
|
||||
<div class="text-xs text-ink-gray-5">
|
||||
{{ __('Meta Image') }}
|
||||
</div>
|
||||
<FileUploader
|
||||
@@ -70,11 +201,9 @@
|
||||
<Button @click="openFileSelector">
|
||||
{{ __('Upload') }}
|
||||
</Button>
|
||||
<div class="mt-2 text-ink-gray-5 text-sm">
|
||||
<div class="mt-1 text-ink-gray-5 text-sm leading-5">
|
||||
{{
|
||||
__(
|
||||
'Appears when the batch URL is shared on any online platform'
|
||||
)
|
||||
__('Appears when the batch URL is shared on socials')
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
@@ -106,119 +235,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-10">
|
||||
<div class="text-lg text-ink-gray-9 font-semibold mb-4">
|
||||
{{ __('Date and Time') }}
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-10">
|
||||
<div>
|
||||
<FormControl
|
||||
v-model="batch.start_date"
|
||||
:label="__('Start Date')"
|
||||
type="date"
|
||||
class="mb-4"
|
||||
:required="true"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.end_date"
|
||||
:label="__('End Date')"
|
||||
type="date"
|
||||
class="mb-4"
|
||||
:required="true"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormControl
|
||||
v-model="batch.start_time"
|
||||
:label="__('Start Time')"
|
||||
type="time"
|
||||
class="mb-4"
|
||||
:required="true"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.end_time"
|
||||
:label="__('End Time')"
|
||||
type="time"
|
||||
class="mb-4"
|
||||
:required="true"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormControl
|
||||
v-model="batch.timezone"
|
||||
:label="__('Timezone')"
|
||||
type="text"
|
||||
:placeholder="__('Example: IST (+5:30)')"
|
||||
class="mb-4"
|
||||
:required="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-10">
|
||||
<div class="text-lg text-ink-gray-9 font-semibold mb-4">
|
||||
{{ __('Settings') }}
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-10">
|
||||
<div>
|
||||
<FormControl
|
||||
v-model="batch.seat_count"
|
||||
:label="__('Seat Count')"
|
||||
type="number"
|
||||
class="mb-4"
|
||||
:placeholder="__('Number of seats available')"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.evaluation_end_date"
|
||||
:label="__('Evaluation End Date')"
|
||||
type="date"
|
||||
class="mb-4"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormControl
|
||||
v-model="batch.medium"
|
||||
type="select"
|
||||
:options="[
|
||||
{
|
||||
label: 'Online',
|
||||
value: 'Online',
|
||||
},
|
||||
{
|
||||
label: 'Offline',
|
||||
value: 'Offline',
|
||||
},
|
||||
]"
|
||||
:label="__('Medium')"
|
||||
class="mb-4"
|
||||
/>
|
||||
<Link
|
||||
doctype="LMS Category"
|
||||
:label="__('Category')"
|
||||
v-model="batch.category"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Link
|
||||
doctype="Email Template"
|
||||
:label="__('Email Template')"
|
||||
v-model="batch.confirmation_email_template"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
<div class="text-lg text-ink-gray-9 font-semibold mb-4">
|
||||
{{ __('Payment') }}
|
||||
<div class="px-20 pb-5 space-y-5">
|
||||
<div class="text-lg text-ink-gray-9 font-semibold">
|
||||
{{ __('Pricing') }}
|
||||
</div>
|
||||
<FormControl
|
||||
v-model="batch.paid_batch"
|
||||
type="checkbox"
|
||||
:label="__('Paid Batch')"
|
||||
/>
|
||||
<div class="grid grid-cols-3 gap-10 mt-4">
|
||||
<div v-if="batch.paid_batch" class="grid grid-cols-3 gap-5">
|
||||
<FormControl
|
||||
v-model="batch.amount"
|
||||
:label="__('Amount')"
|
||||
@@ -232,33 +258,6 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-10">
|
||||
<div class="text-lg text-ink-gray-9 font-semibold mb-4">
|
||||
{{ __('Description') }}
|
||||
</div>
|
||||
<FormControl
|
||||
v-model="batch.description"
|
||||
:label="__('Short Description')"
|
||||
type="textarea"
|
||||
class="my-4"
|
||||
:placeholder="__('Short description of the batch')"
|
||||
:required="true"
|
||||
/>
|
||||
<div>
|
||||
<label class="block text-sm text-ink-gray-5 mb-1">
|
||||
{{ __('Batch Details') }}
|
||||
<span class="text-ink-red-3">*</span>
|
||||
</label>
|
||||
<TextEditor
|
||||
:content="batch.batch_details"
|
||||
@change="(val) => (batch.batch_details = val)"
|
||||
:editable="true"
|
||||
:fixedMenu="true"
|
||||
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem] mb-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -279,15 +278,16 @@ import {
|
||||
TextEditor,
|
||||
createResource,
|
||||
usePageMeta,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showToast } from '@/utils'
|
||||
import { Image } from 'lucide-vue-next'
|
||||
import { capture } from '@/telemetry'
|
||||
import { useOnboarding } from 'frappe-ui/frappe'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import MultiSelect from '@/components/Controls/MultiSelect.vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import { openSettings } from '@/utils'
|
||||
|
||||
const router = useRouter()
|
||||
const user = inject('$user')
|
||||
@@ -459,7 +459,7 @@ const createNewBatch = () => {
|
||||
})
|
||||
},
|
||||
onError(err) {
|
||||
showToast('Message', err.messages?.[0] || err, 'alert-circle')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -478,7 +478,7 @@ const editBatchDetails = () => {
|
||||
})
|
||||
},
|
||||
onError(err) {
|
||||
showToast('Message', err.messages?.[0] || err, 'alert-circle')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -20,12 +20,14 @@
|
||||
</header>
|
||||
<div class="p-5 pb-10">
|
||||
<div
|
||||
v-if="batchCount"
|
||||
class="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:items-center justify-between mb-5"
|
||||
>
|
||||
<div class="text-lg text-ink-gray-9 font-semibold">
|
||||
{{ __('All Batches') }}
|
||||
</div>
|
||||
<div
|
||||
v-if="batches.data?.length || batchCount"
|
||||
class="flex flex-col space-y-2 lg:space-y-0 lg:flex-row lg:items-center lg:space-x-4"
|
||||
>
|
||||
<TabButtons
|
||||
@@ -70,22 +72,8 @@
|
||||
<BatchCard :batch="batch" />
|
||||
</router-link>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="!batches.list.loading"
|
||||
class="flex flex-col items-center justify-center text-sm text-ink-gray-5 mt-48"
|
||||
>
|
||||
<BookOpen class="size-10 mx-auto stroke-1 text-ink-gray-4" />
|
||||
<div class="text-lg font-medium mb-1">
|
||||
{{ __('No batches found') }}
|
||||
</div>
|
||||
<div class="leading-5 w-2/5 text-center">
|
||||
{{
|
||||
__(
|
||||
'There are no batches matching the criteria. Keep an eye out, fresh learning experiences are on the way soon!'
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<EmptyState v-else-if="!batches.list.loading" type="Batches" />
|
||||
|
||||
<div
|
||||
v-if="!batches.list.loading && batches.hasNextPage"
|
||||
class="flex justify-center mt-5"
|
||||
@@ -100,6 +88,7 @@
|
||||
import {
|
||||
Breadcrumbs,
|
||||
Button,
|
||||
call,
|
||||
createListResource,
|
||||
FormControl,
|
||||
Select,
|
||||
@@ -107,9 +96,10 @@ import {
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { computed, inject, onMounted, ref, watch } from 'vue'
|
||||
import { BookOpen, Plus } from 'lucide-vue-next'
|
||||
import { Plus } from 'lucide-vue-next'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import BatchCard from '@/components/BatchCard.vue'
|
||||
import EmptyState from '@/components/EmptyState.vue'
|
||||
|
||||
const user = inject('$user')
|
||||
const dayjs = inject('$dayjs')
|
||||
@@ -125,10 +115,12 @@ const is_student = computed(() => user.data?.is_student)
|
||||
const currentTab = ref(is_student.value ? 'All' : 'Upcoming')
|
||||
const orderBy = ref('start_date')
|
||||
const readOnlyMode = window.read_only_mode
|
||||
const batchCount = ref(0)
|
||||
|
||||
onMounted(() => {
|
||||
setFiltersFromQuery()
|
||||
updateBatches()
|
||||
getBatchCount()
|
||||
categories.value = [
|
||||
{
|
||||
label: '',
|
||||
@@ -306,6 +298,14 @@ const canCreateBatch = () => {
|
||||
return false
|
||||
}
|
||||
|
||||
const getBatchCount = () => {
|
||||
call('frappe.client.get_count', {
|
||||
doctype: 'LMS Batch',
|
||||
}).then((data) => {
|
||||
batchCount.value = data
|
||||
})
|
||||
}
|
||||
|
||||
const breadcrumbs = computed(() => [
|
||||
{
|
||||
label: __('Batches'),
|
||||
|
||||
@@ -156,9 +156,9 @@ import {
|
||||
FormControl,
|
||||
Breadcrumbs,
|
||||
usePageMeta,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import { reactive, inject, onMounted, computed } from 'vue'
|
||||
import { showToast } from '@/utils/'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import NotPermitted from '@/components/NotPermitted.vue'
|
||||
@@ -259,7 +259,7 @@ const generatePaymentLink = () => {
|
||||
window.location.href = data
|
||||
},
|
||||
onError(err) {
|
||||
showToast(__('Error'), err.messages?.[0] || err, 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -333,14 +333,7 @@ const validateAddress = () => {
|
||||
}
|
||||
|
||||
const showError = (err) => {
|
||||
createToast({
|
||||
title: 'Error',
|
||||
text: err.messages?.[0] || err,
|
||||
icon: 'x',
|
||||
iconClasses: 'bg-surface-red-5 text-ink-white rounded-md p-px',
|
||||
position: 'top-center',
|
||||
timeout: 10,
|
||||
})
|
||||
toast.error(err.messages?.[0] || err)
|
||||
}
|
||||
|
||||
const changeCurrency = (country) => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
class="sticky flex items-center justify-between top-0 z-10 border-b bg-surface-white px-3 py-2.5 sm:px-5"
|
||||
>
|
||||
<Breadcrumbs :items="breadcrumbs" />
|
||||
<router-link :to="{ name: 'Batches' }">
|
||||
<router-link :to="{ name: 'Batches', query: { certification: true } }">
|
||||
<Button>
|
||||
<template #prefix>
|
||||
<GraduationCap class="h-4 w-4 stroke-1.5" />
|
||||
@@ -101,22 +101,7 @@
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="flex flex-col items-center justify-center text-sm text-ink-gray-5 mt-48"
|
||||
>
|
||||
<BookOpen class="size-10 mx-auto stroke-1 text-ink-gray-4" />
|
||||
<div class="text-lg font-medium mb-1">
|
||||
{{ __('No certified members') }}
|
||||
</div>
|
||||
<div class="leading-5 w-2/5 text-center">
|
||||
{{
|
||||
__(
|
||||
'No certified members found. Please check again later or get certified yourself.'
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<EmptyState v-else type="Certified Members" />
|
||||
</template>
|
||||
<script setup>
|
||||
import {
|
||||
@@ -130,8 +115,9 @@ import {
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { computed, inject, onMounted, ref } from 'vue'
|
||||
import { BookOpen, GraduationCap } from 'lucide-vue-next'
|
||||
import { GraduationCap } from 'lucide-vue-next'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import EmptyState from '@/components/EmptyState.vue'
|
||||
|
||||
const currentCategory = ref('')
|
||||
const filters = ref({})
|
||||
|
||||
@@ -19,62 +19,112 @@
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="mt-5 mb-10">
|
||||
<div class="container mb-5">
|
||||
<div class="mt-5 mb-5">
|
||||
<div class="px-10 pb-5 mb-5 space-y-5 border-b">
|
||||
<div class="text-lg font-semibold mb-4">
|
||||
{{ __('Details') }}
|
||||
</div>
|
||||
<FormControl
|
||||
v-model="course.title"
|
||||
:label="__('Title')"
|
||||
class="mb-4"
|
||||
:required="true"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="course.short_introduction"
|
||||
:label="__('Short Introduction')"
|
||||
:placeholder="
|
||||
__(
|
||||
'A one line introduction to the course that appears on the course card'
|
||||
)
|
||||
"
|
||||
class="mb-4"
|
||||
:required="true"
|
||||
/>
|
||||
<div class="mb-4">
|
||||
<div class="mb-1.5 text-sm text-ink-gray-5">
|
||||
{{ __('Course Description') }}
|
||||
<span class="text-ink-red-3">*</span>
|
||||
</div>
|
||||
<TextEditor
|
||||
:content="course.description"
|
||||
@change="(val) => (course.description = val)"
|
||||
:editable="true"
|
||||
:fixedMenu="true"
|
||||
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem]"
|
||||
<div class="grid grid-cols-2 gap-5">
|
||||
<FormControl
|
||||
v-model="course.title"
|
||||
:label="__('Title')"
|
||||
:required="true"
|
||||
/>
|
||||
<Link
|
||||
doctype="LMS Category"
|
||||
v-model="course.category"
|
||||
:label="__('Category')"
|
||||
:onCreate="(value, close) => openSettings('Categories', close)"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div class="text-xs text-ink-gray-5 mb-2">
|
||||
{{ __('Course Image') }}
|
||||
<span class="text-ink-red-3">*</span>
|
||||
<div class="grid grid-cols-2 gap-5">
|
||||
<MultiSelect
|
||||
v-model="instructors"
|
||||
doctype="User"
|
||||
:label="__('Instructors')"
|
||||
:filters="{ ignore_user_type: 1 }"
|
||||
:onCreate="(close) => openSettings('Members', close)"
|
||||
:required="true"
|
||||
/>
|
||||
<div>
|
||||
<div class="mb-1.5 text-xs text-ink-gray-5">
|
||||
{{ __('Tags') }}
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
v-if="course.tags"
|
||||
v-for="tag in course.tags?.split(', ')"
|
||||
class="flex items-center bg-surface-gray-2 text-ink-gray-7 p-2 rounded-md mr-2"
|
||||
>
|
||||
{{ tag }}
|
||||
<X
|
||||
class="stroke-1.5 w-3 h-3 ml-2 cursor-pointer"
|
||||
@click="removeTag(tag)"
|
||||
/>
|
||||
</div>
|
||||
<FormControl
|
||||
v-model="newTag"
|
||||
:placeholder="__('Add a keyword and then press enter')"
|
||||
class="w-full"
|
||||
@keyup.enter="updateTags()"
|
||||
id="tags"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<FileUploader
|
||||
v-if="!course.course_image"
|
||||
:fileTypes="['image/*']"
|
||||
:validateFile="validateFile"
|
||||
@success="(file) => saveImage(file)"
|
||||
>
|
||||
<template
|
||||
v-slot="{ file, progress, uploading, openFileSelector }"
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-5">
|
||||
<FormControl
|
||||
v-model="course.short_introduction"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
:label="__('Short Introduction')"
|
||||
:placeholder="
|
||||
__(
|
||||
'A one line introduction to the course that appears on the course card'
|
||||
)
|
||||
"
|
||||
:required="true"
|
||||
/>
|
||||
<div class="mb-4">
|
||||
<div class="text-xs text-ink-gray-5 mb-2">
|
||||
{{ __('Course Image') }}
|
||||
<span class="text-ink-red-3">*</span>
|
||||
</div>
|
||||
<FileUploader
|
||||
v-if="!course.course_image"
|
||||
:fileTypes="['image/*']"
|
||||
:validateFile="validateFile"
|
||||
@success="(file) => saveImage(file)"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class="border rounded-md w-fit py-5 px-20">
|
||||
<Image class="size-5 stroke-1 text-ink-gray-7" />
|
||||
<template
|
||||
v-slot="{ file, progress, uploading, openFileSelector }"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class="border rounded-md w-fit py-5 px-20">
|
||||
<Image class="size-5 stroke-1 text-ink-gray-7" />
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<Button @click="openFileSelector">
|
||||
{{ __('Upload') }}
|
||||
</Button>
|
||||
<div class="mt-1 text-ink-gray-5 text-sm leading-5">
|
||||
{{
|
||||
__('Appears on the course card in the course list')
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</FileUploader>
|
||||
<div v-else class="mb-4">
|
||||
<div class="flex items-center">
|
||||
<img
|
||||
:src="course.course_image.file_url"
|
||||
class="border rounded-md w-40"
|
||||
/>
|
||||
<div class="ml-4">
|
||||
<Button @click="openFileSelector">
|
||||
{{ __('Upload') }}
|
||||
<Button @click="removeImage()">
|
||||
{{ __('Remove') }}
|
||||
</Button>
|
||||
<div class="mt-2 text-ink-gray-5 text-sm">
|
||||
{{
|
||||
@@ -83,85 +133,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</FileUploader>
|
||||
<div v-else class="mb-4">
|
||||
<div class="flex items-center">
|
||||
<img
|
||||
:src="course.course_image.file_url"
|
||||
class="border rounded-md w-40"
|
||||
/>
|
||||
<div class="ml-4">
|
||||
<Button @click="removeImage()">
|
||||
{{ __('Remove') }}
|
||||
</Button>
|
||||
<div class="mt-2 text-ink-gray-5 text-sm">
|
||||
{{ __('Appears on the course card in the course list') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FormControl
|
||||
v-model="course.video_link"
|
||||
:label="__('Preview Video')"
|
||||
:placeholder="
|
||||
__(
|
||||
'Paste the youtube link of a short video introducing the course'
|
||||
)
|
||||
"
|
||||
class="mb-4"
|
||||
/>
|
||||
<div class="mb-4">
|
||||
<div class="mb-1.5 text-xs text-ink-gray-5">
|
||||
{{ __('Tags') }}
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
v-if="course.tags"
|
||||
v-for="tag in course.tags?.split(', ')"
|
||||
class="flex items-center bg-surface-gray-2 text-ink-gray-7 p-2 rounded-md mr-2"
|
||||
>
|
||||
{{ tag }}
|
||||
<X
|
||||
class="stroke-1.5 w-3 h-3 ml-2 cursor-pointer"
|
||||
@click="removeTag(tag)"
|
||||
/>
|
||||
</div>
|
||||
<FormControl
|
||||
v-model="newTag"
|
||||
:placeholder="__('Add a keyword and then press enter')"
|
||||
class="w-72"
|
||||
@keyup.enter="updateTags()"
|
||||
id="tags"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-1/2 mb-4">
|
||||
<Link
|
||||
doctype="LMS Category"
|
||||
v-model="course.category"
|
||||
:label="__('Category')"
|
||||
:onCreate="(value, close) => openSettings(close)"
|
||||
/>
|
||||
</div>
|
||||
<MultiSelect
|
||||
v-model="instructors"
|
||||
doctype="User"
|
||||
:label="__('Instructors')"
|
||||
:filters="{ ignore_user_type: 1 }"
|
||||
:required="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="container border-t">
|
||||
<div class="text-lg font-semibold mt-5 mb-4">
|
||||
|
||||
<div class="px-10 pb-5 mb-5 space-y-5 border-b">
|
||||
<div class="text-lg font-semibold">
|
||||
{{ __('Settings') }}
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-10 mb-4">
|
||||
<div
|
||||
v-if="user.data?.is_moderator"
|
||||
class="flex flex-col space-y-4"
|
||||
>
|
||||
<div class="grid grid-cols-2 gap-5">
|
||||
<div class="flex flex-col space-y-5">
|
||||
<FormControl
|
||||
type="checkbox"
|
||||
v-model="course.published"
|
||||
@@ -171,10 +153,9 @@
|
||||
v-model="course.published_on"
|
||||
:label="__('Published On')"
|
||||
type="date"
|
||||
class="mb-5"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-3">
|
||||
<div class="flex flex-col space-y-5">
|
||||
<FormControl
|
||||
type="checkbox"
|
||||
v-model="course.upcoming"
|
||||
@@ -193,7 +174,34 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container border-t space-y-4">
|
||||
|
||||
<div class="px-10 pb-5 mb-5 space-y-5 border-b">
|
||||
<div class="">
|
||||
<div class="mb-1.5 text-sm text-ink-gray-5">
|
||||
{{ __('Course Description') }}
|
||||
<span class="text-ink-red-3">*</span>
|
||||
</div>
|
||||
<TextEditor
|
||||
:content="course.description"
|
||||
@change="(val) => (course.description = val)"
|
||||
:editable="true"
|
||||
:fixedMenu="true"
|
||||
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormControl
|
||||
v-model="course.video_link"
|
||||
:label="__('Preview Video')"
|
||||
:placeholder="
|
||||
__(
|
||||
'Paste the youtube link of a short video introducing the course'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="px-10 pb-5 space-y-5">
|
||||
<div class="text-lg font-semibold mt-5">
|
||||
{{ __('Pricing and Certification') }}
|
||||
</div>
|
||||
@@ -214,19 +222,31 @@
|
||||
:label="__('Paid Certificate')"
|
||||
/>
|
||||
</div>
|
||||
<FormControl v-model="course.course_price" :label="__('Amount')" />
|
||||
<Link
|
||||
doctype="Currency"
|
||||
v-model="course.currency"
|
||||
:filters="{ enabled: 1 }"
|
||||
:label="__('Currency')"
|
||||
/>
|
||||
<Link
|
||||
v-if="course.paid_certificate"
|
||||
doctype="Course Evaluator"
|
||||
v-model="course.evaluator"
|
||||
:label="__('Evaluator')"
|
||||
/>
|
||||
<div class="grid grid-cols-2 gap-5">
|
||||
<div class="space-y-5">
|
||||
<FormControl
|
||||
v-if="course.paid_course || course.paid_certificate"
|
||||
v-model="course.course_price"
|
||||
:label="__('Amount')"
|
||||
/>
|
||||
<Link
|
||||
v-if="course.paid_certificate"
|
||||
doctype="Course Evaluator"
|
||||
v-model="course.evaluator"
|
||||
:label="__('Evaluator')"
|
||||
:onCreate="
|
||||
(value, close) => openSettings('Evaluators', close)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<Link
|
||||
v-if="course.paid_course || course.paid_certificate"
|
||||
doctype="Currency"
|
||||
v-model="course.currency"
|
||||
:filters="{ enabled: 1 }"
|
||||
:label="__('Currency')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -250,6 +270,7 @@ import {
|
||||
FormControl,
|
||||
FileUploader,
|
||||
usePageMeta,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import {
|
||||
inject,
|
||||
@@ -261,13 +282,12 @@ import {
|
||||
watch,
|
||||
getCurrentInstance,
|
||||
} from 'vue'
|
||||
import { showToast } from '@/utils'
|
||||
import { Image, Trash2, X } from 'lucide-vue-next'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { capture } from '@/telemetry'
|
||||
import { useOnboarding } from 'frappe-ui/frappe'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import { useSettings } from '@/stores/settings'
|
||||
import { openSettings } from '@/utils'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import CourseOutline from '@/components/CourseOutline.vue'
|
||||
import MultiSelect from '@/components/Controls/MultiSelect.vue'
|
||||
@@ -277,7 +297,6 @@ const newTag = ref('')
|
||||
const { brand } = sessionStore()
|
||||
const router = useRouter()
|
||||
const instructors = ref([])
|
||||
const settingsStore = useSettings()
|
||||
const app = getCurrentInstance()
|
||||
const { updateOnboardingStep } = useOnboarding('learning')
|
||||
const { $dialog } = app.appContext.config.globalProperties
|
||||
@@ -429,10 +448,10 @@ const submitCourse = () => {
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
showToast('Success', 'Course updated successfully', 'check')
|
||||
toast.success(__('Course updated successfully'))
|
||||
},
|
||||
onError(err) {
|
||||
showToast('Error', err.messages?.[0] || err, 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -446,14 +465,14 @@ const submitCourse = () => {
|
||||
}
|
||||
|
||||
capture('course_created')
|
||||
showToast('Success', 'Course created successfully', 'check')
|
||||
toast.success(__('Course created successfully'))
|
||||
router.push({
|
||||
name: 'CourseForm',
|
||||
params: { courseName: data.name },
|
||||
})
|
||||
},
|
||||
onError(err) {
|
||||
showToast('Error', err.messages?.[0] || err, 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -467,7 +486,7 @@ const deleteCourse = createResource({
|
||||
}
|
||||
},
|
||||
onSuccess() {
|
||||
showToast(__('Success'), __('Course deleted successfully'), 'check')
|
||||
toast.success(__('Course deleted successfully'))
|
||||
router.push({ name: 'Courses' })
|
||||
},
|
||||
})
|
||||
@@ -531,12 +550,6 @@ const removeImage = () => {
|
||||
course.course_image = null
|
||||
}
|
||||
|
||||
const openSettings = (close) => {
|
||||
close()
|
||||
settingsStore.activeTab = 'Categories'
|
||||
settingsStore.isSettingsOpen = true
|
||||
}
|
||||
|
||||
const check_permission = () => {
|
||||
let user_is_instructor = false
|
||||
if (user.data?.is_moderator) return
|
||||
|
||||
@@ -20,12 +20,14 @@
|
||||
</header>
|
||||
<div class="p-5 pb-10">
|
||||
<div
|
||||
v-if="courseCount"
|
||||
class="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:items-center justify-between mb-5"
|
||||
>
|
||||
<div class="text-lg text-ink-gray-9 font-semibold">
|
||||
{{ __('All Courses') }}
|
||||
</div>
|
||||
<div
|
||||
v-if="courses.data?.length || courseCount"
|
||||
class="flex flex-col space-y-2 lg:space-y-0 lg:flex-row lg:items-center lg:space-x-4"
|
||||
>
|
||||
<TabButtons :buttons="courseTabs" v-model="currentTab" />
|
||||
@@ -66,22 +68,7 @@
|
||||
<CourseCard :course="course" />
|
||||
</router-link>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="!courses.list.loading"
|
||||
class="flex flex-col items-center justify-center text-sm text-ink-gray-5 italic mt-48"
|
||||
>
|
||||
<BookOpen class="size-10 mx-auto stroke-1 text-ink-gray-4" />
|
||||
<div class="text-lg font-medium mb-1">
|
||||
{{ __('No courses found') }}
|
||||
</div>
|
||||
<div class="leading-5 w-2/5 text-center">
|
||||
{{
|
||||
__(
|
||||
'There are no courses matching the criteria. Keep an eye out, fresh learning experiences are on the way soon!'
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<EmptyState v-else-if="!courses.list.loading" type="Courses" />
|
||||
<div
|
||||
v-if="!courses.list.loading && courses.hasNextPage"
|
||||
class="flex justify-center mt-5"
|
||||
@@ -104,10 +91,11 @@ import {
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { computed, inject, onMounted, ref, watch } from 'vue'
|
||||
import { BookOpen, Plus } from 'lucide-vue-next'
|
||||
import { Plus } from 'lucide-vue-next'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { canCreateCourse } from '@/utils'
|
||||
import CourseCard from '@/components/CourseCard.vue'
|
||||
import EmptyState from '@/components/EmptyState.vue'
|
||||
import router from '../router'
|
||||
|
||||
const user = inject('$user')
|
||||
@@ -121,12 +109,13 @@ const certification = ref(false)
|
||||
const filters = ref({})
|
||||
const currentTab = ref('Live')
|
||||
const { brand } = sessionStore()
|
||||
const readOnlyMode = window.read_only_mode
|
||||
const courseCount = ref(0)
|
||||
|
||||
onMounted(() => {
|
||||
identifyUserPersona()
|
||||
setFiltersFromQuery()
|
||||
updateCourses()
|
||||
getCourseCount()
|
||||
categories.value = [
|
||||
{
|
||||
label: '',
|
||||
@@ -175,19 +164,22 @@ const identifyUserPersona = async () => {
|
||||
if (user.data?.is_system_manager && !user.data?.developer_mode) {
|
||||
let personaCaptured = await isPersonaCaptured()
|
||||
if (personaCaptured) return
|
||||
|
||||
call('frappe.client.get_count', {
|
||||
doctype: 'LMS Course',
|
||||
}).then((data) => {
|
||||
if (!data) {
|
||||
router.push({
|
||||
name: 'PersonaForm',
|
||||
})
|
||||
}
|
||||
})
|
||||
if (!courseCount.value) {
|
||||
router.push({
|
||||
name: 'PersonaForm',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getCourseCount = () => {
|
||||
call('frappe.client.get_count', {
|
||||
doctype: 'LMS Course',
|
||||
}).then((data) => {
|
||||
courseCount.value = data
|
||||
})
|
||||
}
|
||||
|
||||
const updateCourses = () => {
|
||||
updateFilters()
|
||||
courses.update({
|
||||
|
||||
@@ -94,6 +94,12 @@
|
||||
{{ dayjs(job.data.creation).fromNow() }}
|
||||
</Badge>
|
||||
<Badge size="lg">
|
||||
<template #prefix>
|
||||
<ClipboardType class="size-3 stroke-2 text-ink-gray-7" />
|
||||
</template>
|
||||
{{ job.data.type }}
|
||||
</Badge>
|
||||
<Badge v-if="applicationCount.data" size="lg">
|
||||
<template #prefix>
|
||||
<SquareUserRound class="size-3 stroke-2 text-ink-gray-7" />
|
||||
</template>
|
||||
@@ -102,12 +108,6 @@
|
||||
applicationCount.data == 1 ? __('applicant') : __('applicants')
|
||||
}}
|
||||
</Badge>
|
||||
<Badge size="lg">
|
||||
<template #prefix>
|
||||
<ClipboardType class="size-3 stroke-2 text-ink-gray-7" />
|
||||
</template>
|
||||
{{ job.data.type }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</Button>
|
||||
</header>
|
||||
<div class="py-5">
|
||||
<div class="container border-b mb-4 pb-4">
|
||||
<div class="container border-b mb-4 pb-5">
|
||||
<div class="text-lg font-semibold mb-4">
|
||||
{{ __('Job Details') }}
|
||||
</div>
|
||||
@@ -20,6 +20,15 @@
|
||||
:label="__('Title')"
|
||||
:required="true"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="job.type"
|
||||
:label="__('Type')"
|
||||
type="select"
|
||||
:options="jobTypes"
|
||||
:required="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<FormControl
|
||||
v-model="job.location"
|
||||
:label="__('City')"
|
||||
@@ -31,17 +40,8 @@
|
||||
:label="__('Country')"
|
||||
:required="true"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormControl
|
||||
v-model="job.type"
|
||||
:label="__('Type')"
|
||||
type="select"
|
||||
:options="jobTypes"
|
||||
class="mb-4"
|
||||
:required="true"
|
||||
/>
|
||||
<FormControl
|
||||
v-if="jobName != 'new'"
|
||||
v-model="job.status"
|
||||
:label="__('Status')"
|
||||
type="select"
|
||||
@@ -51,7 +51,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container border-b mb-4 pb-4">
|
||||
<div class="container border-b mb-4 pb-5">
|
||||
<div class="text-lg font-semibold mb-4">
|
||||
{{ __('Company Details') }}
|
||||
</div>
|
||||
@@ -145,12 +145,13 @@ import {
|
||||
TextEditor,
|
||||
FileUploader,
|
||||
usePageMeta,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import { computed, onMounted, reactive, inject } from 'vue'
|
||||
import { FileText, X } from 'lucide-vue-next'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getFileSize, showToast } from '../utils'
|
||||
import { getFileSize } from '@/utils'
|
||||
|
||||
const user = inject('$user')
|
||||
const router = useRouter()
|
||||
@@ -259,7 +260,7 @@ const createNewJob = () => {
|
||||
})
|
||||
},
|
||||
onError(err) {
|
||||
showToast('Error', err.messages?.[0] || err, 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -278,7 +279,7 @@ const editJobDetails = () => {
|
||||
})
|
||||
},
|
||||
onError(err) {
|
||||
showToast('Error', err.messages?.[0] || err, 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -26,15 +26,17 @@
|
||||
</header>
|
||||
<div>
|
||||
<div
|
||||
v-if="jobCount"
|
||||
class="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:items-center justify-between w-full md:w-4/5 mx-auto p-5"
|
||||
>
|
||||
<div
|
||||
v-if="jobCount"
|
||||
class="text-xl font-semibold text-ink-gray-7 mb-4 md:mb-0"
|
||||
>
|
||||
<div class="text-xl font-semibold text-ink-gray-7 mb-4 md:mb-0">
|
||||
{{ __('{0} Open Jobs').format(jobCount) }}
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-2">
|
||||
|
||||
<div
|
||||
v-if="jobs.data?.length || jobCount > 0"
|
||||
class="grid grid-cols-1 md:grid-cols-3 gap-2"
|
||||
>
|
||||
<FormControl
|
||||
type="text"
|
||||
:placeholder="__('Search')"
|
||||
@@ -79,21 +81,7 @@
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="flex flex-col items-center justify-center text-sm text-ink-gray-5 mt-56"
|
||||
>
|
||||
<Laptop class="size-10 mx-auto stroke-1 text-ink-gray-4" />
|
||||
<div class="text-lg font-medium mb-1">
|
||||
{{ __('No jobs found') }}
|
||||
</div>
|
||||
<div class="leading-5 w-2/5 text-center">
|
||||
{{ __('There are no jobs available at the moment.') }}
|
||||
</div>
|
||||
<div class="leading-5 w-1/5 text-center">
|
||||
{{ __('Post a new job or check again later.') }}
|
||||
</div>
|
||||
</div>
|
||||
<EmptyState v-else type="Job Openings" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -106,11 +94,12 @@ import {
|
||||
FormControl,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { Laptop, Plus, Search } from 'lucide-vue-next'
|
||||
import { Plus, Search } from 'lucide-vue-next'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import { inject, computed, ref, onMounted, watch } from 'vue'
|
||||
import JobCard from '@/components/JobCard.vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import EmptyState from '@/components/EmptyState.vue'
|
||||
|
||||
const user = inject('$user')
|
||||
const jobType = ref(null)
|
||||
|
||||
@@ -84,6 +84,7 @@ import {
|
||||
createResource,
|
||||
FormControl,
|
||||
usePageMeta,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import {
|
||||
computed,
|
||||
@@ -97,7 +98,7 @@ import { sessionStore } from '../stores/session'
|
||||
import EditorJS from '@editorjs/editorjs'
|
||||
import LessonHelp from '@/components/LessonHelp.vue'
|
||||
import { ChevronRight } from 'lucide-vue-next'
|
||||
import { createToast, getEditorTools, enablePlyr } from '@/utils'
|
||||
import { getEditorTools, enablePlyr } from '@/utils'
|
||||
import { capture } from '@/telemetry'
|
||||
import { useOnboarding } from 'frappe-ui/frappe'
|
||||
|
||||
@@ -410,14 +411,14 @@ const createNewLesson = () => {
|
||||
updateOnboardingStep('create_first_lesson')
|
||||
|
||||
capture('lesson_created')
|
||||
showToast('Success', 'Lesson created successfully', 'check')
|
||||
toast.success(__('Lesson created successfully'))
|
||||
lessonDetails.reload()
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
onError(err) {
|
||||
showToast('Error', err.message, 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -434,11 +435,11 @@ const editCurrentLesson = () => {
|
||||
},
|
||||
onSuccess() {
|
||||
showSuccessMessage
|
||||
? showToast('Success', 'Lesson updated successfully', 'check')
|
||||
? toast.success(__('Lesson updated successfully'))
|
||||
: ''
|
||||
},
|
||||
onError(err) {
|
||||
showToast('Error', err.message, 'x')
|
||||
toast.error(err.message)
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -453,20 +454,6 @@ const validateLesson = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const showToast = (title, text, icon) => {
|
||||
createToast({
|
||||
title: title,
|
||||
text: text,
|
||||
icon: icon,
|
||||
iconClasses:
|
||||
icon == 'check'
|
||||
? 'bg-surface-green-3 text-ink-white rounded-md p-px'
|
||||
: 'bg-surface-red-5 text-ink-white rounded-md p-px',
|
||||
position: icon == 'check' ? 'bottom-right' : 'top-center',
|
||||
timeout: icon == 'check' ? 5 : 10,
|
||||
})
|
||||
}
|
||||
|
||||
const breadcrumbs = computed(() => {
|
||||
let crumbs = [
|
||||
{
|
||||
|
||||
@@ -141,9 +141,9 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { createResource, FormControl, Button, Badge } from 'frappe-ui'
|
||||
import { createResource, FormControl, Button, Badge, toast } from 'frappe-ui'
|
||||
import { computed, reactive, ref, onMounted, inject } from 'vue'
|
||||
import { showToast, convertToTitleCase } from '@/utils'
|
||||
import { convertToTitleCase } from '@/utils'
|
||||
import { Plus, X, Check, CircleAlert } from 'lucide-vue-next'
|
||||
|
||||
const user = inject('$user')
|
||||
@@ -198,7 +198,7 @@ const createSlot = createResource({
|
||||
}
|
||||
},
|
||||
onSuccess() {
|
||||
showToast('Success', 'Slot added successfully', 'check')
|
||||
toast.success(__('Slot added successfully'))
|
||||
evaluator.reload()
|
||||
showSlotsTemplate.value = 0
|
||||
newSlot.day = ''
|
||||
@@ -206,7 +206,7 @@ const createSlot = createResource({
|
||||
newSlot.end_time = ''
|
||||
},
|
||||
onError(err) {
|
||||
showToast('Error', err.messages?.[0] || err, 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -221,10 +221,10 @@ const updateSlot = createResource({
|
||||
}
|
||||
},
|
||||
onSuccess() {
|
||||
showToast('Success', 'Availability updated successfully', 'check')
|
||||
toast.success(__('Availability updated successfully'))
|
||||
},
|
||||
onError(err) {
|
||||
showToast('Error', err.messages?.[0] || err, 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -237,11 +237,11 @@ const deleteSlot = createResource({
|
||||
}
|
||||
},
|
||||
onSuccess() {
|
||||
showToast('Success', 'Slot deleted successfully', 'check')
|
||||
toast.success(__('Slot deleted successfully'))
|
||||
evaluator.reload()
|
||||
},
|
||||
onError(err) {
|
||||
showToast('Error', err.messages?.[0] || err, 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -256,10 +256,10 @@ const updateUnavailability = createResource({
|
||||
}
|
||||
},
|
||||
onSuccess() {
|
||||
showToast('Success', 'Unavailability updated successfully', 'check')
|
||||
toast.success(__('Unavailability updated successfully'))
|
||||
},
|
||||
onError(err) {
|
||||
showToast('Error', err.messages?.[0] || err, 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -44,9 +44,9 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { FormControl, createResource } from 'frappe-ui'
|
||||
import { FormControl, createResource, toast } from 'frappe-ui'
|
||||
import { ref } from 'vue'
|
||||
import { showToast, convertToTitleCase } from '@/utils'
|
||||
import { convertToTitleCase } from '@/utils'
|
||||
import { CircleAlert } from 'lucide-vue-next'
|
||||
|
||||
const moderator = ref(false)
|
||||
@@ -102,7 +102,7 @@ const changeRole = (role) => {
|
||||
},
|
||||
{
|
||||
onSuccess(data) {
|
||||
showToast('Success', 'Role updated successfully', 'check')
|
||||
toast.success(__('Role updated successfully'))
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -168,6 +168,7 @@
|
||||
ignore_user_type: 1,
|
||||
}"
|
||||
:label="__('Program Member')"
|
||||
:onCreate="(value, close) => openSettings('Members', close)"
|
||||
/>
|
||||
</template>
|
||||
</Dialog>
|
||||
@@ -187,12 +188,13 @@ import {
|
||||
ListHeaderItem,
|
||||
ListSelectBanner,
|
||||
usePageMeta,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import { Plus, Trash2 } from 'lucide-vue-next'
|
||||
import { showToast } from '@/utils/'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { openSettings } from '@/utils'
|
||||
import Draggable from 'vuedraggable'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
|
||||
@@ -229,11 +231,11 @@ const addProgramCourse = () => {
|
||||
onSuccess(data) {
|
||||
showDialog.value = false
|
||||
course.value = null
|
||||
showToast(__('Success'), __('Course added to program'), 'check')
|
||||
toast.success(__('Course added to program'))
|
||||
program.reload()
|
||||
},
|
||||
onError(err) {
|
||||
showToast('Error', err.messages?.[0] || err, 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -251,11 +253,11 @@ const addProgramMember = () => {
|
||||
onSuccess(data) {
|
||||
showDialog.value = false
|
||||
member.value = null
|
||||
showToast(__('Success'), __('Member added to program'), 'check')
|
||||
toast.success(__('Member added to program'))
|
||||
program.reload()
|
||||
},
|
||||
onError(err) {
|
||||
showToast('Error', err.messages?.[0] || err, 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -272,11 +274,11 @@ const remove = (selections, unselectAll, doctype) => {
|
||||
{
|
||||
onSuccess(data) {
|
||||
unselectAll()
|
||||
showToast(__('Success'), __('Items removed successfully'), 'check')
|
||||
toast.success(__('Items removed successfully'))
|
||||
program.reload()
|
||||
},
|
||||
onError(err) {
|
||||
showToast('Error', err.messages?.[0] || err, 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -298,11 +300,11 @@ const updateOrder = (e) => {
|
||||
},
|
||||
{
|
||||
onSuccess(data) {
|
||||
showToast(__('Success'), __('Course moved successfully'), 'check')
|
||||
toast.success(__('Course moved successfully'))
|
||||
program.reload()
|
||||
},
|
||||
onError(err) {
|
||||
showToast('Error', err.messages?.[0] || err, 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -82,22 +82,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="text-center p-5 text-ink-gray-5 mt-52 w-3/4 md:w-1/2 mx-auto space-y-2"
|
||||
>
|
||||
<BookOpen class="size-10 mx-auto stroke-1 text-ink-gray-4" />
|
||||
<div class="text-xl font-medium">
|
||||
{{ __('No programs found') }}
|
||||
</div>
|
||||
<div class="leading-5">
|
||||
{{
|
||||
__(
|
||||
'There are no programs available at the moment. Keep an eye out, fresh learning experiences are on the way soon!'
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<EmptyState v-else type="Programs" />
|
||||
|
||||
<Dialog
|
||||
v-model="showDialog"
|
||||
@@ -127,13 +112,14 @@ import {
|
||||
Dialog,
|
||||
FormControl,
|
||||
usePageMeta,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import { computed, inject, onMounted, ref } from 'vue'
|
||||
import { BookOpen, Edit, Plus, LockKeyhole } from 'lucide-vue-next'
|
||||
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 { showToast } from '@/utils'
|
||||
import { useSettings } from '@/stores/settings'
|
||||
|
||||
const { brand } = sessionStore()
|
||||
@@ -198,7 +184,7 @@ const enrollMember = (program, course) => {
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
showToast('Error', err.messages?.[0] || err, 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -198,6 +198,7 @@ import {
|
||||
ListSelectBanner,
|
||||
Button,
|
||||
usePageMeta,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import {
|
||||
computed,
|
||||
@@ -210,7 +211,7 @@ import {
|
||||
} from 'vue'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import { Plus, Trash2 } from 'lucide-vue-next'
|
||||
import { showToast, updateDocumentTitle } from '@/utils'
|
||||
import { updateDocumentTitle } from '@/utils'
|
||||
import { useRouter } from 'vue-router'
|
||||
import Question from '@/components/Modals/Question.vue'
|
||||
|
||||
@@ -340,14 +341,14 @@ const createQuiz = () => {
|
||||
{},
|
||||
{
|
||||
onSuccess(data) {
|
||||
showToast(__('Success'), __('Quiz created successfully'), 'check')
|
||||
toast.success(__('Quiz created successfully'))
|
||||
router.push({
|
||||
name: 'QuizForm',
|
||||
params: { quizID: data.name },
|
||||
})
|
||||
},
|
||||
onError(err) {
|
||||
showToast(__('Error'), __(err.messages?.[0] || err), 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -359,10 +360,10 @@ const updateQuiz = () => {
|
||||
{
|
||||
onSuccess(data) {
|
||||
quiz.total_marks = data.total_marks
|
||||
showToast(__('Success'), __('Quiz updated successfully'), 'check')
|
||||
toast.success(__('Quiz updated successfully'))
|
||||
},
|
||||
onError(err) {
|
||||
showToast(__('Error'), __(err.messages?.[0] || err), 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -428,7 +429,7 @@ const deleteQuestions = (selections, unselectAll) => {
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
showToast(__('Success'), __('Questions deleted successfully'), 'check')
|
||||
toast.success(__('Questions deleted successfully'))
|
||||
quizDetails.reload()
|
||||
unselectAll()
|
||||
},
|
||||
|
||||
@@ -80,10 +80,10 @@ import {
|
||||
Button,
|
||||
Badge,
|
||||
usePageMeta,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import { computed, onBeforeUnmount, onMounted, inject } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showToast } from '@/utils'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
|
||||
const { brand } = sessionStore()
|
||||
@@ -147,7 +147,7 @@ const saveSubmission = () => {
|
||||
{},
|
||||
{
|
||||
onError(err) {
|
||||
showToast(__('Error'), __(err.messages?.[0] || err), 'x')
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -40,18 +40,7 @@
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="text-center p-5 text-ink-gray-5 mt-52 w-3/4 md:w-1/2 mx-auto space-y-2"
|
||||
>
|
||||
<BookOpen class="size-10 mx-auto stroke-1 text-ink-gray-4" />
|
||||
<div class="text-xl font-medium">
|
||||
{{ __('No submissions') }}
|
||||
</div>
|
||||
<div class="leading-5">
|
||||
{{ __('No quiz submissions found. Please check again later.') }}
|
||||
</div>
|
||||
</div>
|
||||
<EmptyState v-else />
|
||||
</template>
|
||||
<script setup>
|
||||
import {
|
||||
@@ -65,10 +54,10 @@ import {
|
||||
ListHeaderItem,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { BookOpen } from 'lucide-vue-next'
|
||||
import { computed, onMounted, inject } from 'vue'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import { useRouter } from 'vue-router'
|
||||
import EmptyState from '@/components/EmptyState.vue'
|
||||
|
||||
const { brand } = sessionStore()
|
||||
const router = useRouter()
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
</router-link>
|
||||
</header>
|
||||
<div v-if="quizzes.data?.length" class="md:w-3/4 md:mx-auto py-5 mx-5">
|
||||
<div v-if="quizCount" class="text-xl font-semibold text-ink-gray-7 mb-4">
|
||||
{{ __('{0} Quizzes').format(quizCount) }}
|
||||
</div>
|
||||
<ListView
|
||||
:columns="quizColumns"
|
||||
:rows="quizzes.data"
|
||||
@@ -53,27 +56,13 @@
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="text-center p-5 text-ink-gray-5 mt-52 w-3/4 md:w-1/2 mx-auto space-y-2"
|
||||
>
|
||||
<BookOpen class="size-10 mx-auto stroke-1 text-ink-gray-4" />
|
||||
<div class="text-xl font-medium">
|
||||
{{ __('No quizzes found') }}
|
||||
</div>
|
||||
<div class="leading-5">
|
||||
{{
|
||||
__(
|
||||
'You have not created any quizzes yet. To create a new quiz, click on the "New Quiz" button above.'
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<EmptyState v-else type="Quizzes" />
|
||||
</template>
|
||||
<script setup>
|
||||
import {
|
||||
Breadcrumbs,
|
||||
Button,
|
||||
call,
|
||||
createListResource,
|
||||
ListView,
|
||||
ListRows,
|
||||
@@ -83,19 +72,22 @@ import {
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { computed, inject, onMounted } from 'vue'
|
||||
import { BookOpen, Plus } from 'lucide-vue-next'
|
||||
import { computed, inject, onMounted, ref } from 'vue'
|
||||
import { Plus } from 'lucide-vue-next'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import EmptyState from '@/components/EmptyState.vue'
|
||||
|
||||
const { brand } = sessionStore()
|
||||
const user = inject('$user')
|
||||
const router = useRouter()
|
||||
const quizCount = ref(0)
|
||||
const readOnlyMode = window.read_only_mode
|
||||
|
||||
onMounted(() => {
|
||||
if (!user.data?.is_moderator && !user.data?.is_instructor) {
|
||||
router.push({ name: 'Courses' })
|
||||
}
|
||||
getQuizCount()
|
||||
})
|
||||
|
||||
const quizFilter = computed(() => {
|
||||
@@ -114,6 +106,14 @@ const quizzes = createListResource({
|
||||
orderBy: 'modified desc',
|
||||
})
|
||||
|
||||
const getQuizCount = () => {
|
||||
call('frappe.client.get_count', {
|
||||
doctype: 'LMS Quiz',
|
||||
}).then((data) => {
|
||||
quizCount.value = data
|
||||
})
|
||||
}
|
||||
|
||||
const quizColumns = computed(() => {
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { toast } from 'frappe-ui'
|
||||
import { useTimeAgo } from '@vueuse/core'
|
||||
import { Quiz } from '@/utils/quiz'
|
||||
import { Assignment } from '@/utils/assignment'
|
||||
import { Upload } from '@/utils/upload'
|
||||
import { Markdown } from '@/utils/markdownParser'
|
||||
import { useSettings } from '@/stores/settings'
|
||||
import { usersStore } from '@/stores/user'
|
||||
import Header from '@editorjs/header'
|
||||
import Paragraph from '@editorjs/paragraph'
|
||||
import { CodeBox } from '@/utils/code'
|
||||
@@ -14,19 +15,11 @@ import dayjs from '@/utils/dayjs'
|
||||
import Embed from '@editorjs/embed'
|
||||
import SimpleImage from '@editorjs/simple-image'
|
||||
import Table from '@editorjs/table'
|
||||
import { usersStore } from '../stores/user'
|
||||
import Plyr from 'plyr'
|
||||
import 'plyr/dist/plyr.css'
|
||||
|
||||
const readOnlyMode = window.read_only_mode
|
||||
|
||||
export function createToast(options) {
|
||||
toast({
|
||||
position: 'bottom-right',
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function timeAgo(date) {
|
||||
return useTimeAgo(date).value
|
||||
}
|
||||
@@ -97,26 +90,6 @@ export function getFileSize(file_size) {
|
||||
return value
|
||||
}
|
||||
|
||||
export function showToast(title, text, icon, iconClasses = null) {
|
||||
if (!iconClasses) {
|
||||
if (icon == 'check') {
|
||||
iconClasses = 'bg-surface-green-3 text-ink-white rounded-md p-px'
|
||||
} else if (icon == 'alert-circle') {
|
||||
iconClasses = 'bg-yellow-600 text-ink-white rounded-md p-px'
|
||||
} else {
|
||||
iconClasses = 'bg-surface-red-5 text-ink-white rounded-md p-px'
|
||||
}
|
||||
}
|
||||
createToast({
|
||||
title: title,
|
||||
text: htmlToText(text),
|
||||
icon: icon,
|
||||
iconClasses: iconClasses,
|
||||
position: icon == 'check' ? 'bottom-right' : 'top-center',
|
||||
timeout: icon != 'check' ? 10 : 5,
|
||||
})
|
||||
}
|
||||
|
||||
export function getImgDimensions(imgSrc) {
|
||||
return new Promise((resolve) => {
|
||||
let img = new Image()
|
||||
@@ -579,3 +552,10 @@ export const enablePlyr = () => {
|
||||
})
|
||||
}, 500)
|
||||
}
|
||||
|
||||
export const openSettings = (category, close) => {
|
||||
const settingsStore = useSettings()
|
||||
close()
|
||||
settingsStore.activeTab = category
|
||||
settingsStore.isSettingsOpen = true
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
module.exports = {
|
||||
presets: [require('frappe-ui/src/tailwind/preset')],
|
||||
import frappeUIPreset from 'frappe-ui/src/tailwind/preset'
|
||||
|
||||
export default {
|
||||
presets: [frappeUIPreset],
|
||||
content: [
|
||||
'./index.html',
|
||||
'./src/**/*.{vue,js,ts,jsx,tsx}',
|
||||
|
||||
@@ -25,7 +25,7 @@ export default defineConfig({
|
||||
}),
|
||||
],
|
||||
server: {
|
||||
allowedHosts: ['fs', 'persona'],
|
||||
allowedHosts: ['fs', 'per2'],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
2812
frontend/yarn.lock
2812
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,7 @@ app_license = "AGPL"
|
||||
# include js, css files in header of web template
|
||||
web_include_css = "lms.bundle.css"
|
||||
# web_include_css = "/assets/lms/css/lms.css"
|
||||
web_include_js = ["website.bundle.js"]
|
||||
web_include_js = []
|
||||
|
||||
# include custom scss in every website theme (without file extension ".scss")
|
||||
# website_theme_scss = "lms/public/scss/website"
|
||||
|
||||
@@ -21,9 +21,9 @@ from lms.lms.utils import (
|
||||
|
||||
class LMSBatch(Document):
|
||||
def validate(self):
|
||||
if self.seat_count:
|
||||
self.validate_seats_left()
|
||||
self.validate_seats_left()
|
||||
self.validate_batch_end_date()
|
||||
self.validate_batch_time()
|
||||
self.validate_duplicate_courses()
|
||||
self.validate_payments_app()
|
||||
self.validate_amount_and_currency()
|
||||
@@ -40,6 +40,11 @@ class LMSBatch(Document):
|
||||
if self.end_date < self.start_date:
|
||||
frappe.throw(_("Batch end date cannot be before the batch start date"))
|
||||
|
||||
def validate_batch_time(self):
|
||||
if self.start_time and self.end_time:
|
||||
if get_time(self.start_time) >= get_time(self.end_time):
|
||||
frappe.throw(_("Batch start time cannot be greater than or equal to end time."))
|
||||
|
||||
def validate_duplicate_courses(self):
|
||||
courses = [row.course for row in self.courses]
|
||||
duplicates = {course for course in courses if courses.count(course) > 1}
|
||||
@@ -94,6 +99,9 @@ class LMSBatch(Document):
|
||||
enrollment.save()
|
||||
|
||||
def validate_seats_left(self):
|
||||
if cint(self.seat_count) < 0:
|
||||
frappe.throw(_("Seat count cannot be negative."))
|
||||
|
||||
students = frappe.db.count("LMS Batch Enrollment", {"batch": self.name})
|
||||
if cint(self.seat_count) < students:
|
||||
frappe.throw(_("There are no seats available in this batch."))
|
||||
@@ -208,86 +216,6 @@ def authenticate():
|
||||
return response.json()["access_token"]
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_batch(
|
||||
title,
|
||||
start_date,
|
||||
end_date,
|
||||
description=None,
|
||||
batch_details=None,
|
||||
batch_details_raw=None,
|
||||
meta_image=None,
|
||||
seat_count=0,
|
||||
start_time=None,
|
||||
end_time=None,
|
||||
medium="Online",
|
||||
category=None,
|
||||
paid_batch=0,
|
||||
amount=0,
|
||||
currency=None,
|
||||
amount_usd=0,
|
||||
name=None,
|
||||
published=0,
|
||||
evaluation_end_date=None,
|
||||
):
|
||||
frappe.only_for("Moderator")
|
||||
if name:
|
||||
doc = frappe.get_doc("LMS Batch", name)
|
||||
else:
|
||||
doc = frappe.get_doc({"doctype": "LMS Batch"})
|
||||
|
||||
doc.update(
|
||||
{
|
||||
"title": title,
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
"description": description,
|
||||
"batch_details": batch_details,
|
||||
"batch_details_raw": batch_details_raw,
|
||||
"meta_image": meta_image,
|
||||
"seat_count": seat_count,
|
||||
"start_time": start_time,
|
||||
"end_time": end_time,
|
||||
"medium": medium,
|
||||
"category": category,
|
||||
"paid_batch": paid_batch,
|
||||
"amount": amount,
|
||||
"currency": currency,
|
||||
"amount_usd": amount_usd,
|
||||
"published": published,
|
||||
"evaluation_end_date": evaluation_end_date,
|
||||
}
|
||||
)
|
||||
doc.save()
|
||||
return doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_course(course, parent, name=None, evaluator=None):
|
||||
frappe.only_for("Moderator")
|
||||
|
||||
if frappe.db.exists("Batch Course", {"course": course, "parent": parent}):
|
||||
frappe.throw(_("Course already added to the batch."))
|
||||
|
||||
if name:
|
||||
doc = frappe.get_doc("Batch Course", name)
|
||||
else:
|
||||
doc = frappe.new_doc("Batch Course")
|
||||
|
||||
doc.update(
|
||||
{
|
||||
"course": course,
|
||||
"evaluator": evaluator,
|
||||
"parent": parent,
|
||||
"parentfield": "courses",
|
||||
"parenttype": "LMS Batch",
|
||||
}
|
||||
)
|
||||
doc.save()
|
||||
|
||||
return doc.name
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_batch_timetable(batch):
|
||||
timetable = frappe.get_all(
|
||||
|
||||
@@ -403,7 +403,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-04-22 16:05:27.914422",
|
||||
"modified": "2025-05-14 12:43:22.749850",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Settings",
|
||||
@@ -425,6 +425,16 @@
|
||||
"read": 1,
|
||||
"role": "LMS Student",
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Moderator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
|
||||
@@ -1,450 +0,0 @@
|
||||
frappe.ready(() => {
|
||||
setup_file_size();
|
||||
pin_header();
|
||||
|
||||
$(".enroll-in-course").click((e) => {
|
||||
enroll_in_course(e);
|
||||
});
|
||||
|
||||
$(".notify-me").click((e) => {
|
||||
notify_user(e);
|
||||
});
|
||||
|
||||
$(".nav-link").click((e) => {
|
||||
change_hash(e);
|
||||
});
|
||||
|
||||
if (window.location.hash) {
|
||||
open_tab();
|
||||
}
|
||||
|
||||
if (window.location.pathname == "/statistics") {
|
||||
generate_graph("New Signups", "#new-signups");
|
||||
generate_graph("Course Enrollments", "#course-enrollments");
|
||||
generate_graph("Lesson Completion", "#lesson-completion");
|
||||
generate_course_completion_graph();
|
||||
}
|
||||
|
||||
expand_the_active_chapter();
|
||||
|
||||
$(".chapter-title")
|
||||
.unbind()
|
||||
.click((e) => {
|
||||
rotate_chapter_icon(e);
|
||||
});
|
||||
|
||||
$(".no-preview").click((e) => {
|
||||
show_no_preview_dialog(e);
|
||||
});
|
||||
|
||||
$("#create-batch").click((e) => {
|
||||
open_batch_dialog(e);
|
||||
});
|
||||
|
||||
$("#course-filter").change((e) => {
|
||||
filter_courses(e);
|
||||
});
|
||||
});
|
||||
|
||||
const pin_header = () => {
|
||||
const el = document.querySelector(".sticky");
|
||||
if (el) {
|
||||
const observer = new IntersectionObserver(
|
||||
([e]) =>
|
||||
e.target.classList.toggle("is-pinned", e.intersectionRatio < 1),
|
||||
{ threshold: [1] }
|
||||
);
|
||||
observer.observe(el);
|
||||
}
|
||||
};
|
||||
|
||||
const setup_file_size = () => {
|
||||
frappe.provide("frappe.form.formatters");
|
||||
frappe.form.formatters.FileSize = file_size;
|
||||
};
|
||||
|
||||
const file_size = (value) => {
|
||||
if (value > 1048576) {
|
||||
value = flt(flt(value) / 1048576, 1) + "M";
|
||||
} else if (value > 1024) {
|
||||
value = flt(flt(value) / 1024, 1) + "K";
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const enroll_in_course = (e) => {
|
||||
e.preventDefault();
|
||||
let course = $(e.currentTarget).attr("data-course");
|
||||
if (frappe.session.user == "Guest") {
|
||||
window.location.href = `/login?redirect-to=/courses/${course}`;
|
||||
return;
|
||||
}
|
||||
|
||||
let batch = $(e.currentTarget).attr("data-batch");
|
||||
batch = batch ? decodeURIComponent(batch) : "";
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_enrollment.lms_enrollment.create_membership",
|
||||
args: {
|
||||
batch: batch ? batch : "",
|
||||
course: course,
|
||||
},
|
||||
callback: (data) => {
|
||||
if (data.message == "OK") {
|
||||
$(".no-preview-modal").modal("hide");
|
||||
frappe.show_alert(
|
||||
{
|
||||
message: __("Enrolled successfully"),
|
||||
indicator: "green",
|
||||
},
|
||||
3
|
||||
);
|
||||
setTimeout(function () {
|
||||
window.location.href = `/courses/${course}/learn/1.1`;
|
||||
}, 1000);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const notify_user = (e) => {
|
||||
e.preventDefault();
|
||||
var course = decodeURIComponent($("#outline-heading").attr("data-course"));
|
||||
if (frappe.session.user == "Guest") {
|
||||
window.location.href = `/login?redirect-to=/courses/${course}`;
|
||||
return;
|
||||
}
|
||||
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_course_interest.lms_course_interest.capture_interest",
|
||||
args: {
|
||||
course: course,
|
||||
},
|
||||
callback: (data) => {
|
||||
$(".no-preview-modal").modal("hide");
|
||||
frappe.show_alert(
|
||||
{
|
||||
message: __(
|
||||
"You have opted to be notified for this course. You will receive an email when the course becomes available."
|
||||
),
|
||||
indicator: "green",
|
||||
},
|
||||
3
|
||||
);
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 3000);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const generate_graph = (chart_name, element, type = "line") => {
|
||||
let date = frappe.datetime;
|
||||
|
||||
frappe.call({
|
||||
method: "lms.lms.utils.get_chart_data",
|
||||
args: {
|
||||
chart_name: chart_name,
|
||||
timespan: "Select Date Range",
|
||||
timegrain: "Daily",
|
||||
from_date: date.add_days(date.get_today(), -30),
|
||||
to_date: date.add_days(date.get_today(), +1),
|
||||
},
|
||||
callback: (data) => {
|
||||
render_chart(data.message, chart_name, element, type);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const render_chart = (data, chart_name, element, type) => {
|
||||
const chart = new frappe.Chart(element, {
|
||||
title: chart_name,
|
||||
data: data,
|
||||
type: type,
|
||||
height: 250,
|
||||
colors: ["#4563f1"],
|
||||
axisOptions: {
|
||||
xIsSeries: 1,
|
||||
},
|
||||
lineOptions: {
|
||||
regionFill: 1,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const generate_course_completion_graph = () => {
|
||||
frappe.call({
|
||||
method: "lms.lms.utils.get_course_completion_data",
|
||||
callback: (data) => {
|
||||
render_chart(
|
||||
data.message,
|
||||
"Course Completion",
|
||||
"#course-completion",
|
||||
"pie"
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const change_hash = (e) => {
|
||||
window.location.hash = $(e.currentTarget).attr("href");
|
||||
};
|
||||
|
||||
const open_tab = () => {
|
||||
$(`a[href="${window.location.hash}"]`).click();
|
||||
};
|
||||
|
||||
const expand_the_first_chapter = () => {
|
||||
let elements = $(".course-home-outline .collapse");
|
||||
elements.each((i, element) => {
|
||||
if (i < 1) {
|
||||
show_section(element);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const expand_the_active_chapter = () => {
|
||||
let selector = $(".course-home-headings.title");
|
||||
|
||||
if (selector.length && $(".course-details-page").length) {
|
||||
expand_for_course_details(selector);
|
||||
} else if ($(".active-lesson").length) {
|
||||
/* For course home page */
|
||||
selector = $(".active-lesson");
|
||||
show_section(selector.parent().parent());
|
||||
} else {
|
||||
/* If no active chapter then exapand the first chapter */
|
||||
expand_the_first_chapter();
|
||||
}
|
||||
};
|
||||
|
||||
const expand_for_course_details = (selector) => {
|
||||
$(".lesson-info").removeClass("active-lesson");
|
||||
$(".lesson-info").each((i, elem) => {
|
||||
if ($(elem).data("lesson") == selector.data("lesson")) {
|
||||
$(elem).addClass("active-lesson");
|
||||
show_section($(elem).parent().parent());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const show_section = (element) => {
|
||||
$(element).addClass("show");
|
||||
$(element)
|
||||
.siblings(".chapter-title")
|
||||
.children(".chapter-icon")
|
||||
.css("transform", "rotate(90deg)");
|
||||
$(element).siblings(".chapter-title").attr("aria-expanded", true);
|
||||
};
|
||||
|
||||
const rotate_chapter_icon = (e) => {
|
||||
let icon = $(e.currentTarget).children(".chapter-icon");
|
||||
if (icon.css("transform") == "none") {
|
||||
icon.css("transform", "rotate(90deg)");
|
||||
} else {
|
||||
icon.css("transform", "none");
|
||||
}
|
||||
};
|
||||
|
||||
const show_no_preview_dialog = (e) => {
|
||||
$("#no-preview-modal").modal("show");
|
||||
};
|
||||
|
||||
const open_batch_dialog = () => {
|
||||
this.batch_dialog = new frappe.ui.Dialog({
|
||||
title: __("New Batch"),
|
||||
fields: [
|
||||
{
|
||||
fieldtype: "Data",
|
||||
label: __("Title"),
|
||||
fieldname: "title",
|
||||
reqd: 1,
|
||||
default: batch_info && batch_info.title,
|
||||
},
|
||||
{
|
||||
fieldtype: "Check",
|
||||
label: __("Published"),
|
||||
fieldname: "published",
|
||||
default: batch_info && batch_info.published,
|
||||
},
|
||||
{
|
||||
fieldtype: "Section Break",
|
||||
},
|
||||
{
|
||||
fieldtype: "Date",
|
||||
label: __("Start Date"),
|
||||
fieldname: "start_date",
|
||||
reqd: 1,
|
||||
default: batch_info && batch_info.start_date,
|
||||
},
|
||||
{
|
||||
fieldtype: "Date",
|
||||
label: __("End Date"),
|
||||
fieldname: "end_date",
|
||||
reqd: 1,
|
||||
default: batch_info && batch_info.end_date,
|
||||
},
|
||||
{
|
||||
fieldtype: "Column Break",
|
||||
},
|
||||
{
|
||||
fieldtype: "Time",
|
||||
label: __("Start Time"),
|
||||
fieldname: "start_time",
|
||||
default: batch_info && batch_info.start_time,
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: "Time",
|
||||
label: __("End Time"),
|
||||
fieldname: "end_time",
|
||||
default: batch_info && batch_info.end_time,
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: "Section Break",
|
||||
},
|
||||
{
|
||||
fieldtype: "Select",
|
||||
label: __("Medium"),
|
||||
fieldname: "medium",
|
||||
options: ["Online", "Offline"],
|
||||
default: (batch_info && batch_info.medium) || "Online",
|
||||
},
|
||||
{
|
||||
fieldtype: "Link",
|
||||
label: __("Category"),
|
||||
fieldname: "category",
|
||||
options: "LMS Category",
|
||||
only_select: 1,
|
||||
default: batch_info && batch_info.category,
|
||||
},
|
||||
{
|
||||
fieldtype: "Column Break",
|
||||
},
|
||||
{
|
||||
fieldtype: "Int",
|
||||
label: __("Seat Count"),
|
||||
fieldname: "seat_count",
|
||||
default: batch_info && batch_info.seat_count,
|
||||
},
|
||||
{
|
||||
fieldtype: "Date",
|
||||
label: __("Evaluation End Date"),
|
||||
fieldname: "evaluation_end_date",
|
||||
default: batch_info && batch_info.evaluation_end_date,
|
||||
},
|
||||
{
|
||||
fieldtype: "Section Break",
|
||||
},
|
||||
{
|
||||
fieldtype: "Small Text",
|
||||
label: __("Description"),
|
||||
fieldname: "description",
|
||||
default: batch_info && batch_info.description,
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: "Text Editor",
|
||||
label: __("Batch Details"),
|
||||
fieldname: "batch_details",
|
||||
default: batch_info && batch_info.batch_details,
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: "HTML Editor",
|
||||
label: __("Batch Details Raw"),
|
||||
fieldname: "batch_details_raw",
|
||||
default: batch_info && batch_info.batch_details_raw,
|
||||
},
|
||||
{
|
||||
fieldtype: "Attach Image",
|
||||
label: __("Meta Image"),
|
||||
fieldname: "meta_image",
|
||||
default: batch_info && batch_info.meta_image,
|
||||
},
|
||||
{
|
||||
fieldtype: "Section Break",
|
||||
label: __("Pricing"),
|
||||
fieldname: "pricing",
|
||||
},
|
||||
{
|
||||
fieldtype: "Check",
|
||||
label: __("Paid Batch"),
|
||||
fieldname: "paid_batch",
|
||||
default: batch_info && batch_info.paid_batch,
|
||||
},
|
||||
{
|
||||
fieldtype: "Currency",
|
||||
label: __("Amount"),
|
||||
fieldname: "amount",
|
||||
default: batch_info && batch_info.amount,
|
||||
mandatory_depends_on: "paid_batch",
|
||||
depends_on: "paid_batch",
|
||||
},
|
||||
{
|
||||
fieldtype: "Link",
|
||||
label: __("Currency"),
|
||||
fieldname: "currency",
|
||||
options: "Currency",
|
||||
default: batch_info && batch_info.currency,
|
||||
mandatory_depends_on: "paid_batch",
|
||||
depends_on: "paid_batch",
|
||||
only_select: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: "Currency",
|
||||
label: __("Amount (USD)"),
|
||||
fieldname: "amount_usd",
|
||||
depends_on: "paid_batch",
|
||||
description: __(
|
||||
"If you set an amount here, then the USD equivalent setting will not get applied."
|
||||
),
|
||||
},
|
||||
],
|
||||
primary_action_label: __("Save"),
|
||||
primary_action: (values) => {
|
||||
save_batch(values);
|
||||
},
|
||||
});
|
||||
this.batch_dialog.show();
|
||||
};
|
||||
|
||||
const save_batch = (values) => {
|
||||
let args = {};
|
||||
if (batch_info) {
|
||||
args = Object.assign(batch_info, values);
|
||||
} else {
|
||||
args = values;
|
||||
}
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_batch.lms_batch.create_batch",
|
||||
args: args,
|
||||
callback: (r) => {
|
||||
if (r.message) {
|
||||
frappe.show_alert({
|
||||
message: batch_info
|
||||
? __("Batch Updated")
|
||||
: __("Batch Created"),
|
||||
indicator: "green",
|
||||
});
|
||||
this.batch_dialog.hide();
|
||||
window.location.href = `/batches/details/${r.message.name}`;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const filter_courses = (e) => {
|
||||
const course_lists = $(".course-cards-parent");
|
||||
const filter = $(e.currentTarget).val();
|
||||
course_lists.each((i, list) => {
|
||||
const course_cards = $(list).children(".course-card");
|
||||
course_cards.sort((a, b) => {
|
||||
var value1 = $(a).data(filter);
|
||||
var value2 = $(b).data(filter);
|
||||
return value1 > value2 ? -1 : value1 < value2 ? 1 : 0;
|
||||
});
|
||||
$(list).append(course_cards);
|
||||
});
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
frappe.ready(() => {
|
||||
hide_profile_and_dashboard_for_guest_users();
|
||||
});
|
||||
|
||||
const hide_profile_and_dashboard_for_guest_users = () => {
|
||||
if (frappe.session.user == "Guest") {
|
||||
let links = $(".nav-link").filter(
|
||||
(i, elem) =>
|
||||
$(elem).text().trim() === "My Profile" ||
|
||||
$(elem).text().trim() === "Dashboard"
|
||||
);
|
||||
links.length && links.each((i, elem) => $(elem).addClass("hide"));
|
||||
}
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
import "./profile.js";
|
||||
import "./common_functions.js";
|
||||
import "../../../../frappe/frappe/public/js/frappe/ui/chart.js";
|
||||
import "../../../../frappe/frappe/public/js/frappe/ui/keyboard.js";
|
||||
import "../../../../frappe/frappe/public/js/frappe/event_emitter.js";
|
||||
@@ -11,9 +11,11 @@
|
||||
<b>{{ _("Batch Start Date:") }}</b> {{ frappe.utils.format_date(start_date, "medium") }}
|
||||
</p>
|
||||
|
||||
{% if medium %}
|
||||
<p>
|
||||
<b>{{ _("Medium:") }}</b> {{ medium }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<p>
|
||||
<b>{{ _("Timings:") }}</b> {{ frappe.utils.format_time(start_time, "hh:mm a") }}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
{% extends "templates/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
{% include "public/icons/symbol-defs.svg" %}
|
||||
{% include "lms/templates/onboarding_header.html" %}
|
||||
{% block page_content %}
|
||||
Hello, world!
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
{%- block script -%}
|
||||
{{ super() }}
|
||||
{% if frappe.get_system_settings("enable_telemetry") %}
|
||||
{% set telemetry_boot_info = get_telemetry_boot_info() %}
|
||||
<script>
|
||||
const telemetry_boot_info = {{ get_telemetry_boot_info() }}
|
||||
if (telemetry_boot_info && Object.keys(telemetry_boot_info).length)
|
||||
Object.assign(frappe.boot, telemetry_boot_info)
|
||||
</script>
|
||||
{% endif %}
|
||||
<script>
|
||||
frappe.router = {
|
||||
slug(name) {
|
||||
return name.toLowerCase().replace(/ /g, "-");
|
||||
},
|
||||
};
|
||||
frappe.utils.make_event_emitter(frappe.router)
|
||||
</script>
|
||||
{{ include_script("telemetry.bundle.js") }}
|
||||
|
||||
{%- endblock -%}
|
||||
@@ -2,10 +2,11 @@
|
||||
"name": "frappe_lms",
|
||||
"version": "1.0.0",
|
||||
"description": "Easy to use, open-source, Learning Management System",
|
||||
"workspaces1": [
|
||||
"workspaces": [
|
||||
"frappe-ui",
|
||||
"frontend"
|
||||
],
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test-local": "cypress open --e2e --browser chrome",
|
||||
"postinstall": "cd frontend && yarn install --check-files",
|
||||
|
||||
Reference in New Issue
Block a user