feat: students and assessment tab in dashboard
This commit is contained in:
@@ -3,25 +3,25 @@
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "vite --host",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"dayjs": "^1.11.6",
|
||||
"feather-icons": "^4.28.0",
|
||||
"frappe-ui": "^0.1.16",
|
||||
"frappe-ui": "^0.1.22",
|
||||
"lucide-vue-next": "^0.259.0",
|
||||
"markdown-it": "^14.0.0",
|
||||
"pinia": "^2.0.33",
|
||||
"tailwindcss": "^3.2.7",
|
||||
"vue": "^3.2.25",
|
||||
"dayjs": "^1.11.6",
|
||||
"vue-router": "^4.0.12",
|
||||
"markdown-it": "^14.0.0"
|
||||
"vue-router": "^4.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^2.0.0",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"postcss": "^8.4.5",
|
||||
"vite": "^3.0.0"
|
||||
"vite": "^5.0.11"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
<div class="text-lg font-semibold mb-4">
|
||||
{{ __('Assessments') }}
|
||||
</div>
|
||||
<div v-if="assessments?.length">
|
||||
<div v-if="assessments.data?.length">
|
||||
<ListView
|
||||
:columns="getAssessmentColumns()"
|
||||
:rows="attempts?.data"
|
||||
:rows="assessments.data"
|
||||
row-key="name"
|
||||
:options="{ selectable: false, showTooltip: false }"
|
||||
>
|
||||
@@ -18,30 +18,59 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ListView } from 'frappe-ui'
|
||||
import { ListView, createResource } from 'frappe-ui'
|
||||
import { inject } from 'vue'
|
||||
|
||||
const user = inject('$user')
|
||||
|
||||
const props = defineProps({
|
||||
assessments: {
|
||||
batch: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
rows: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
columns: {
|
||||
type: Array,
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
selectable: true,
|
||||
totalCount: 0,
|
||||
rowCount: 0,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
const getSubmissionColumns = () => {
|
||||
return [
|
||||
const assessments = createResource({
|
||||
url: 'lms.lms.utils.get_assessments',
|
||||
params: {
|
||||
batch: props.batch,
|
||||
},
|
||||
auto: true,
|
||||
})
|
||||
|
||||
const getAssessmentColumns = () => {
|
||||
let columns = [
|
||||
{
|
||||
label: 'Assessment',
|
||||
key: 'title',
|
||||
},
|
||||
{
|
||||
label: 'Type',
|
||||
key: 'type',
|
||||
key: 'assessment_type',
|
||||
},
|
||||
{
|
||||
]
|
||||
|
||||
if (!user.data?.is_moderator) {
|
||||
columns.push({
|
||||
label: 'Status/Score',
|
||||
key: 'status',
|
||||
align: 'center',
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
return columns
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
<template>
|
||||
<div>
|
||||
<UpcomingEvaluations :upcoming_evals="batch.data.upcoming_evals" />
|
||||
<Assessments :assessments="batch.data.assessments" />
|
||||
<UpcomingEvaluations
|
||||
:batch="batch.data.name"
|
||||
:endDate="batch.data.evaluation_end_date"
|
||||
:courses="batch.data.courses"
|
||||
:isStudent="isStudent"
|
||||
/>
|
||||
<Assessments :batch="batch.data.name" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import UpcomingEvaluations from './UpcomingEvaluations.vue'
|
||||
import Assessments from './Assessments.vue'
|
||||
import UpcomingEvaluations from '@/components/UpcomingEvaluations.vue'
|
||||
import Assessments from '@/components/Assessments.vue'
|
||||
|
||||
const props = defineProps({
|
||||
batch: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
isStudent: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -35,11 +35,21 @@
|
||||
{{ formatTime(batch.data.end_time) }}
|
||||
</span>
|
||||
</div>
|
||||
<Button v-if="user?.data?.is_moderator" class="w-full mt-4">
|
||||
<span>
|
||||
{{ __('Manage Batch') }}
|
||||
</span>
|
||||
</Button>
|
||||
<router-link
|
||||
v-if="user?.data?.is_moderator"
|
||||
:to="{
|
||||
name: 'Batch',
|
||||
params: {
|
||||
batchName: batch.data.name,
|
||||
},
|
||||
}"
|
||||
>
|
||||
<Button variant="solid" class="w-full mt-4">
|
||||
<span>
|
||||
{{ __('Manage Batch') }}
|
||||
</span>
|
||||
</Button>
|
||||
</router-link>
|
||||
<Button
|
||||
v-else-if="batch.data.paid_batch"
|
||||
class="w-full mt-4"
|
||||
|
||||
153
frontend/src/components/BatchStudents.vue
Normal file
153
frontend/src/components/BatchStudents.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<Button class="float-right mb-3" variant="solid" @click="openStudentModal()">
|
||||
<template #prefix>
|
||||
<Plus class="h-4 w-4" />
|
||||
</template>
|
||||
{{ __('Add Student') }}
|
||||
</Button>
|
||||
<div class="text-lg font-semibold mb-4">
|
||||
{{ __('Students') }}
|
||||
</div>
|
||||
<div v-if="students.data?.length">
|
||||
<ListView
|
||||
:columns="getStudentColumns()"
|
||||
:rows="students.data"
|
||||
row-key="name"
|
||||
:options="{ showTooltip: false }"
|
||||
>
|
||||
<ListHeader
|
||||
class="mb-2 grid items-center space-x-4 rounded bg-gray-100 p-2"
|
||||
>
|
||||
<ListHeaderItem :item="item" v-for="item in getStudentColumns()">
|
||||
<template #prefix="{ item }">
|
||||
<component
|
||||
v-if="item.icon"
|
||||
:is="item.icon"
|
||||
class="h-4 w-4 stroke-1.5 ml-4"
|
||||
/>
|
||||
</template>
|
||||
</ListHeaderItem>
|
||||
</ListHeader>
|
||||
<ListRows>
|
||||
<ListRow :row="row" v-for="row in students.data">
|
||||
<template #default="{ column, item }">
|
||||
<ListRowItem :item="row[column.key]" :align="column.align">
|
||||
<template #prefix>
|
||||
<div v-if="column.key == 'full_name'">
|
||||
<Avatar
|
||||
class="flex items-center"
|
||||
:image="row['user_image']"
|
||||
:label="item"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
{{ row[column.key] }}
|
||||
</div>
|
||||
</ListRowItem>
|
||||
</template>
|
||||
</ListRow>
|
||||
</ListRows>
|
||||
<ListSelectBanner>
|
||||
<template #actions="{ unselectAll, selections }">
|
||||
<div class="flex gap-2">
|
||||
<Button variant="ghost" @click="removeStudents(selections)">
|
||||
<Trash2 class="h-4 w-4 stroke-1.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
label="Unselect all"
|
||||
@click="unselectAll.toString()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ListSelectBanner>
|
||||
</ListView>
|
||||
</div>
|
||||
<StudentModal
|
||||
:batch="props.batch"
|
||||
v-model="showStudentModal"
|
||||
v-model:reloadStudents="students"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import {
|
||||
createResource,
|
||||
ListHeader,
|
||||
ListHeaderItem,
|
||||
ListSelectBanner,
|
||||
ListRow,
|
||||
ListRows,
|
||||
ListView,
|
||||
ListRowItem,
|
||||
Avatar,
|
||||
Button,
|
||||
} from 'frappe-ui'
|
||||
import { Settings, Trash2, Plus } from 'lucide-vue-next'
|
||||
import { ref } from 'vue'
|
||||
import StudentModal from '@/components/StudentModal.vue'
|
||||
|
||||
const showStudentModal = ref(false)
|
||||
|
||||
const props = defineProps({
|
||||
batch: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const students = createResource({
|
||||
url: 'lms.lms.utils.get_batch_students',
|
||||
cache: ['students', props.batch],
|
||||
params: {
|
||||
batch: props.batch,
|
||||
},
|
||||
auto: true,
|
||||
})
|
||||
|
||||
const getStudentColumns = () => {
|
||||
return [
|
||||
{
|
||||
label: 'Full Name',
|
||||
key: 'full_name',
|
||||
},
|
||||
{
|
||||
label: 'Courses Done',
|
||||
key: 'courses_completed',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
label: 'Assessments Done',
|
||||
key: 'assessments_completed',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
label: 'Last Active',
|
||||
key: 'last_active',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const openStudentModal = () => {
|
||||
showStudentModal.value = true
|
||||
}
|
||||
|
||||
const removeStudent = createResource({
|
||||
url: 'frappe.client.delete',
|
||||
makeParams(values) {
|
||||
return {
|
||||
doctype: 'Batch Student',
|
||||
name: values.student,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const removeStudents = (selections) => {
|
||||
selections.forEach(async (student) => {
|
||||
console.log(student)
|
||||
removeStudent.submit({ student })
|
||||
await setTimeout(1000)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
277
frontend/src/components/Controls/Autocomplete.vue
Normal file
277
frontend/src/components/Controls/Autocomplete.vue
Normal file
@@ -0,0 +1,277 @@
|
||||
<template>
|
||||
<Combobox v-model="selectedValue" nullable v-slot="{ open: isComboboxOpen }">
|
||||
<Popover class="w-full" v-model:show="showOptions">
|
||||
<template #target="{ open: openPopover, togglePopover }">
|
||||
<slot name="target" v-bind="{ open: openPopover, togglePopover }">
|
||||
<div class="w-full">
|
||||
<button
|
||||
class="flex w-full items-center justify-between focus:outline-none"
|
||||
:class="inputClasses"
|
||||
@click="() => togglePopover()"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<slot name="prefix" />
|
||||
<span
|
||||
class="overflow-hidden text-ellipsis whitespace-nowrap text-base leading-5"
|
||||
v-if="selectedValue"
|
||||
>
|
||||
{{ displayValue(selectedValue) }}
|
||||
</span>
|
||||
<span class="text-base leading-5 text-gray-500" v-else>
|
||||
{{ placeholder || '' }}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronDown class="h-4 w-4 stroke-1.5" />
|
||||
</button>
|
||||
</div>
|
||||
</slot>
|
||||
</template>
|
||||
<template #body="{ isOpen }">
|
||||
<div v-show="isOpen">
|
||||
<div class="mt-1 rounded-lg bg-white py-1 text-base shadow-2xl">
|
||||
<div class="relative px-1.5 pt-0.5">
|
||||
<ComboboxInput
|
||||
ref="search"
|
||||
class="form-input w-full"
|
||||
type="text"
|
||||
@change="
|
||||
(e) => {
|
||||
query = e.target.value
|
||||
}
|
||||
"
|
||||
:value="query"
|
||||
autocomplete="off"
|
||||
placeholder="Search"
|
||||
/>
|
||||
<button
|
||||
class="absolute right-1.5 inline-flex h-7 w-7 items-center justify-center"
|
||||
@click="selectedValue = null"
|
||||
>
|
||||
<X class="h-4 w-4 stroke-1.5" />
|
||||
</button>
|
||||
</div>
|
||||
<ComboboxOptions
|
||||
class="my-1 max-h-[12rem] overflow-y-auto px-1.5"
|
||||
static
|
||||
>
|
||||
<div
|
||||
class="mt-1.5"
|
||||
v-for="group in groups"
|
||||
:key="group.key"
|
||||
v-show="group.items.length > 0"
|
||||
>
|
||||
<div
|
||||
v-if="group.group && !group.hideLabel"
|
||||
class="px-2.5 py-1.5 text-sm font-medium text-gray-500"
|
||||
>
|
||||
{{ group.group }}
|
||||
</div>
|
||||
<ComboboxOption
|
||||
as="template"
|
||||
v-for="option in group.items"
|
||||
:key="option.value"
|
||||
:value="option"
|
||||
v-slot="{ active, selected }"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
'flex items-center rounded px-2.5 py-1.5 text-base',
|
||||
{ 'bg-gray-100': active },
|
||||
]"
|
||||
>
|
||||
<slot
|
||||
name="item-prefix"
|
||||
v-bind="{ active, selected, option }"
|
||||
/>
|
||||
<slot
|
||||
name="item-label"
|
||||
v-bind="{ active, selected, option }"
|
||||
>
|
||||
{{ option.label }}
|
||||
</slot>
|
||||
</li>
|
||||
</ComboboxOption>
|
||||
</div>
|
||||
<li
|
||||
v-if="groups.length == 0"
|
||||
class="mt-1.5 rounded-md px-2.5 py-1.5 text-base text-gray-600"
|
||||
>
|
||||
No results found
|
||||
</li>
|
||||
</ComboboxOptions>
|
||||
<div v-if="slots.footer" class="border-t p-1.5 pb-0.5">
|
||||
<slot
|
||||
name="footer"
|
||||
v-bind="{ value: search?.el._value, close }"
|
||||
></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
</Combobox>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxInput,
|
||||
ComboboxOptions,
|
||||
ComboboxOption,
|
||||
} from '@headlessui/vue'
|
||||
import { Popover, Button } from 'frappe-ui'
|
||||
import { ChevronDown, X } from 'lucide-vue-next'
|
||||
import { ref, computed, useAttrs, useSlots, watch, nextTick } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'subtle',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
filterable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'update:query', 'change'])
|
||||
|
||||
const query = ref('')
|
||||
const showOptions = ref(false)
|
||||
const search = ref(null)
|
||||
|
||||
const attrs = useAttrs()
|
||||
const slots = useSlots()
|
||||
|
||||
const valuePropPassed = computed(() => 'value' in attrs)
|
||||
|
||||
const selectedValue = computed({
|
||||
get() {
|
||||
return valuePropPassed.value ? attrs.value : props.modelValue
|
||||
},
|
||||
set(val) {
|
||||
query.value = ''
|
||||
if (val) {
|
||||
showOptions.value = false
|
||||
}
|
||||
emit(valuePropPassed.value ? 'change' : 'update:modelValue', val)
|
||||
},
|
||||
})
|
||||
|
||||
function close() {
|
||||
showOptions.value = false
|
||||
}
|
||||
|
||||
const groups = computed(() => {
|
||||
if (!props.options || props.options.length == 0) return []
|
||||
|
||||
let groups = props.options[0]?.group
|
||||
? props.options
|
||||
: [{ group: '', items: props.options }]
|
||||
|
||||
return groups
|
||||
.map((group, i) => {
|
||||
return {
|
||||
key: i,
|
||||
group: group.group,
|
||||
hideLabel: group.hideLabel || false,
|
||||
items: props.filterable ? filterOptions(group.items) : group.items,
|
||||
}
|
||||
})
|
||||
.filter((group) => group.items.length > 0)
|
||||
})
|
||||
|
||||
function filterOptions(options) {
|
||||
if (!query.value) {
|
||||
return options
|
||||
}
|
||||
return options.filter((option) => {
|
||||
let searchTexts = [option.label, option.value]
|
||||
return searchTexts.some((text) =>
|
||||
(text || '').toString().toLowerCase().includes(query.value.toLowerCase())
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function displayValue(option) {
|
||||
if (typeof option === 'string') {
|
||||
let allOptions = groups.value.flatMap((group) => group.items)
|
||||
let selectedOption = allOptions.find((o) => o.value === option)
|
||||
return selectedOption?.label || option
|
||||
}
|
||||
return option?.label
|
||||
}
|
||||
|
||||
watch(query, (q) => {
|
||||
emit('update:query', q)
|
||||
})
|
||||
|
||||
watch(showOptions, (val) => {
|
||||
if (val) {
|
||||
nextTick(() => {
|
||||
search.value.el.focus()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const textColor = computed(() => {
|
||||
return props.disabled ? 'text-gray-600' : 'text-gray-800'
|
||||
})
|
||||
|
||||
const inputClasses = computed(() => {
|
||||
let sizeClasses = {
|
||||
sm: 'text-base rounded h-7',
|
||||
md: 'text-base rounded h-8',
|
||||
lg: 'text-lg rounded-md h-10',
|
||||
xl: 'text-xl rounded-md h-10',
|
||||
}[props.size]
|
||||
|
||||
let paddingClasses = {
|
||||
sm: 'py-1.5 px-2',
|
||||
md: 'py-1.5 px-2.5',
|
||||
lg: 'py-1.5 px-3',
|
||||
xl: 'py-1.5 px-3',
|
||||
}[props.size]
|
||||
|
||||
let variant = props.disabled ? 'disabled' : props.variant
|
||||
let variantClasses = {
|
||||
subtle:
|
||||
'border border-gray-100 bg-gray-100 placeholder-gray-500 hover:border-gray-200 hover:bg-gray-200 focus:bg-white focus:border-gray-500 focus:shadow-sm focus:ring-0 focus-visible:ring-2 focus-visible:ring-gray-400',
|
||||
outline:
|
||||
'border border-gray-300 bg-white placeholder-gray-500 hover:border-gray-400 hover:shadow-sm focus:bg-white focus:border-gray-500 focus:shadow-sm focus:ring-0 focus-visible:ring-2 focus-visible:ring-gray-400',
|
||||
disabled: [
|
||||
'border bg-gray-50 placeholder-gray-400',
|
||||
props.variant === 'outline' ? 'border-gray-300' : 'border-transparent',
|
||||
],
|
||||
}[variant]
|
||||
|
||||
return [
|
||||
sizeClasses,
|
||||
paddingClasses,
|
||||
variantClasses,
|
||||
textColor.value,
|
||||
'transition-colors w-full',
|
||||
]
|
||||
})
|
||||
|
||||
defineExpose({ query })
|
||||
</script>
|
||||
146
frontend/src/components/Controls/Link.vue
Normal file
146
frontend/src/components/Controls/Link.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div class="space-y-1.5">
|
||||
<label class="block" :class="labelClasses" v-if="attrs.label">
|
||||
{{ attrs.label }}
|
||||
</label>
|
||||
<Autocomplete
|
||||
ref="autocomplete"
|
||||
:options="options.data"
|
||||
v-model="value"
|
||||
:size="attrs.size || 'sm'"
|
||||
:variant="attrs.variant"
|
||||
:placeholder="attrs.placeholder"
|
||||
:filterable="false"
|
||||
>
|
||||
<template #target="{ open, togglePopover }">
|
||||
<slot name="target" v-bind="{ open, togglePopover }" />
|
||||
</template>
|
||||
|
||||
<template #prefix>
|
||||
<slot name="prefix" />
|
||||
</template>
|
||||
|
||||
<template #item-prefix="{ active, selected, option }">
|
||||
<slot name="item-prefix" v-bind="{ active, selected, option }" />
|
||||
</template>
|
||||
|
||||
<template #item-label="{ active, selected, option }">
|
||||
<slot name="item-label" v-bind="{ active, selected, option }" />
|
||||
</template>
|
||||
|
||||
<template v-if="attrs.onCreate" #footer="{ value, close }">
|
||||
<div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="w-full !justify-start"
|
||||
label="Create New"
|
||||
@click="attrs.onCreate(value, close)"
|
||||
>
|
||||
<template #prefix>
|
||||
<Plus class="h-4 w-4 stroke-1.5" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</Autocomplete>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Autocomplete from '@/components/Controls/Autocomplete.vue'
|
||||
import { watchDebounced } from '@vueuse/core'
|
||||
import { createResource, Button } from 'frappe-ui'
|
||||
import { Plus } from 'lucide-vue-next'
|
||||
import { useAttrs, computed, ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
doctype: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
filters: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
|
||||
const attrs = useAttrs()
|
||||
|
||||
const valuePropPassed = computed(() => 'value' in attrs)
|
||||
|
||||
const value = computed({
|
||||
get: () => (valuePropPassed.value ? attrs.value : props.modelValue),
|
||||
set: (val) => {
|
||||
return (
|
||||
val?.value &&
|
||||
emit(valuePropPassed.value ? 'change' : 'update:modelValue', val?.value)
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const autocomplete = ref(null)
|
||||
const text = ref('')
|
||||
|
||||
watchDebounced(
|
||||
() => autocomplete.value?.query,
|
||||
(val) => {
|
||||
val = val || ''
|
||||
if (text.value === val) return
|
||||
text.value = val
|
||||
reload(val)
|
||||
},
|
||||
{ debounce: 300, immediate: true }
|
||||
)
|
||||
|
||||
watchDebounced(
|
||||
() => props.doctype,
|
||||
() => reload(''),
|
||||
{ debounce: 300, immediate: true }
|
||||
)
|
||||
|
||||
const options = createResource({
|
||||
url: 'frappe.desk.search.search_link',
|
||||
cache: [props.doctype, text.value],
|
||||
method: 'POST',
|
||||
params: {
|
||||
txt: text.value,
|
||||
doctype: props.doctype,
|
||||
filters: props.filters,
|
||||
},
|
||||
transform: (data) => {
|
||||
return data.map((option) => {
|
||||
return {
|
||||
label: option.value,
|
||||
value: option.value,
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
function reload(val) {
|
||||
options.update({
|
||||
params: {
|
||||
txt: val,
|
||||
doctype: props.doctype,
|
||||
filters: props.filters,
|
||||
},
|
||||
})
|
||||
options.reload()
|
||||
}
|
||||
|
||||
const labelClasses = computed(() => {
|
||||
return [
|
||||
{
|
||||
sm: 'text-xs',
|
||||
md: 'text-base',
|
||||
}[attrs.size || 'sm'],
|
||||
'text-gray-600',
|
||||
]
|
||||
})
|
||||
</script>
|
||||
38
frontend/src/components/Controls/Rating.vue
Normal file
38
frontend/src/components/Controls/Rating.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="flex text-center">
|
||||
<div v-for="index in 5">
|
||||
<Star
|
||||
:class="{ 'fill-orange-500': index <= rating }"
|
||||
class="h-5 w-5 fill-gray-400 text-gray-200 mr-1 cursor-pointer"
|
||||
@click="markRating(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Star } from 'lucide-vue-next'
|
||||
import { ref } from 'vue'
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
modelValue: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
let rating = ref(props.modelValue)
|
||||
|
||||
let emitChange = (value) => {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
function markRating(index) {
|
||||
emitChange(index)
|
||||
rating.value = index
|
||||
}
|
||||
</script>
|
||||
@@ -1,125 +1,146 @@
|
||||
<template>
|
||||
<div v-if="reviews.data" class="my-10">
|
||||
<div class="text-2xl font-semibold mb-5">
|
||||
{{ __("Reviews") }}
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<div class="flex flex-col items-center">
|
||||
<div v-if="avg_rating" class="text-3xl font-semibold mb-2">
|
||||
{{ avg_rating }}
|
||||
</div>
|
||||
<div class="flex mb-2">
|
||||
<Star v-for="index in 5" class="h-5 w-5 text-gray-100 bg-gray-200 rounded-sm mr-1" :class="(index <= Math.ceil(avg_rating)) ? 'fill-orange-500' : 'fill-gray-600'"/>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
{{ reviews.data.length }} {{ __("reviews") }}
|
||||
</div>
|
||||
<Button v-if="membership" @click="openReviewModal()">
|
||||
<span>
|
||||
{{ __("Write a review") }}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="border border-gray-300 mx-4"></div>
|
||||
<div class="flex flex-col">
|
||||
<div v-for="index in reversedRange(5)">
|
||||
<div class="flex items-center mb-4">
|
||||
<span class="mr-2">
|
||||
{{ index }} {{ __("stars") }}
|
||||
</span>
|
||||
<div class="bg-gray-200 rounded-full w-52 mr-2">
|
||||
<div class="bg-gray-900 h-1 rounded-full" :style="{ width: rating_percent[index] + '%' }"></div>
|
||||
</div>
|
||||
<span>
|
||||
{{ Math.floor(rating_percent[index]) }}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-12">
|
||||
<div v-for="(review, index) in reviews.data">
|
||||
<div class="my-4">
|
||||
<div class="flex items-center">
|
||||
<UserAvatar :user="review.owner_details" :size="'2xl'"/>
|
||||
<div class="mx-4">
|
||||
<span class="text-lg font-medium mr-4">
|
||||
{{ review.owner_details.full_name }}
|
||||
</span>
|
||||
<span>
|
||||
{{ review.creation }}
|
||||
</span>
|
||||
<div class="flex mt-2">
|
||||
<Star v-for="index in 5" class="h-5 w-5 text-gray-100 bg-gray-200 rounded-sm mr-2" :class="(index <= Math.ceil(review.rating)) ? 'fill-orange-500' : 'fill-gray-600'"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 leading-5">
|
||||
{{ review.review }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx-3 h-px border-t border-gray-200" v-if="index < reviews.data.length - 1"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ReviewModal v-model="showReviewModal" v-model:reloadReviews="reviews" :courseName="courseName"/>
|
||||
<div v-if="reviews.data" class="my-10">
|
||||
<div class="text-2xl font-semibold mb-5">
|
||||
{{ __('Reviews') }}
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<div class="flex flex-col items-center">
|
||||
<div v-if="avg_rating" class="text-3xl font-semibold mb-2">
|
||||
{{ avg_rating }}
|
||||
</div>
|
||||
<div class="flex mb-2">
|
||||
<Star
|
||||
v-for="index in 5"
|
||||
class="h-5 w-5 text-gray-100 bg-gray-200 rounded-sm mr-1"
|
||||
:class="
|
||||
index <= Math.ceil(avg_rating)
|
||||
? 'fill-orange-500'
|
||||
: 'fill-gray-600'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-2">{{ reviews.data.length }} {{ __('reviews') }}</div>
|
||||
<Button v-if="membership" @click="openReviewModal()">
|
||||
<span>
|
||||
{{ __('Write a review') }}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="border border-gray-300 mx-4"></div>
|
||||
<div class="flex flex-col">
|
||||
<div v-for="index in reversedRange(5)">
|
||||
<div class="flex items-center mb-4">
|
||||
<span class="mr-2"> {{ index }} {{ __('stars') }} </span>
|
||||
<div class="bg-gray-200 rounded-full w-52 mr-2">
|
||||
<div
|
||||
class="bg-gray-900 h-1 rounded-full"
|
||||
:style="{ width: rating_percent[index] + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
<span> {{ Math.floor(rating_percent[index]) }}% </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-12">
|
||||
<div v-for="(review, index) in reviews.data">
|
||||
<div class="my-4">
|
||||
<div class="flex items-center">
|
||||
<UserAvatar :user="review.owner_details" :size="'2xl'" />
|
||||
<div class="mx-4">
|
||||
<span class="text-lg font-medium mr-4">
|
||||
{{ review.owner_details.full_name }}
|
||||
</span>
|
||||
<span>
|
||||
{{ review.creation }}
|
||||
</span>
|
||||
<div class="flex mt-2">
|
||||
<Star
|
||||
v-for="index in 5"
|
||||
class="h-5 w-5 text-gray-100 bg-gray-200 rounded-sm mr-2"
|
||||
:class="
|
||||
index <= Math.ceil(review.rating)
|
||||
? 'fill-orange-500'
|
||||
: 'fill-gray-600'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 leading-5">
|
||||
{{ review.review }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx-3 h-px border-t border-gray-200"
|
||||
v-if="index < reviews.data.length - 1"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ReviewModal
|
||||
v-model="showReviewModal"
|
||||
v-model:reloadReviews="reviews"
|
||||
:courseName="courseName"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Star } from 'lucide-vue-next'
|
||||
import { createResource, Button } from "frappe-ui";
|
||||
import { computed, ref } from "vue";
|
||||
import UserAvatar from '@/components/UserAvatar.vue';
|
||||
import ReviewModal from '@/components/ReviewModal.vue';
|
||||
import { createResource, Button } from 'frappe-ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import ReviewModal from '@/components/ReviewModal.vue'
|
||||
|
||||
const props = defineProps({
|
||||
courseName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
avg_rating: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
membership: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
courseName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
avg_rating: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
membership: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
|
||||
const reversedRange = (count) => Array.from({ length: count }, (_, index) => count - index);
|
||||
const reversedRange = (count) =>
|
||||
Array.from({ length: count }, (_, index) => count - index)
|
||||
|
||||
const reviews = createResource({
|
||||
url: "lms.lms.utils.get_reviews",
|
||||
cache: ["course_reviews", props.courseName],
|
||||
params: {
|
||||
course: props.courseName
|
||||
},
|
||||
auto: true,
|
||||
});
|
||||
url: 'lms.lms.utils.get_reviews',
|
||||
cache: ['course_reviews', props.courseName],
|
||||
params: {
|
||||
course: props.courseName,
|
||||
},
|
||||
auto: true,
|
||||
})
|
||||
|
||||
const rating_percent = computed(() => {
|
||||
let rating_count = {};
|
||||
let rating_percent = {};
|
||||
let rating_count = {}
|
||||
let rating_percent = {}
|
||||
|
||||
for (const key of [1, 2, 3, 4, 5]) {
|
||||
rating_count[key] = 0;
|
||||
}
|
||||
for (const key of [1, 2, 3, 4, 5]) {
|
||||
rating_count[key] = 0
|
||||
}
|
||||
|
||||
for (const review of reviews?.data) {
|
||||
rating_count[review.rating] += 1;
|
||||
}
|
||||
for (const review of reviews?.data) {
|
||||
rating_count[review.rating] += 1
|
||||
}
|
||||
|
||||
[1,2,3,4,5].forEach((key) => {
|
||||
rating_percent[key] = (rating_count[key] / reviews.data.length * 100).toFixed(2);
|
||||
});
|
||||
return rating_percent;
|
||||
});
|
||||
;[1, 2, 3, 4, 5].forEach((key) => {
|
||||
rating_percent[key] = (
|
||||
(rating_count[key] / reviews.data.length) *
|
||||
100
|
||||
).toFixed(2)
|
||||
})
|
||||
return rating_percent
|
||||
})
|
||||
const showReviewModal = ref(false)
|
||||
|
||||
function openReviewModal() {
|
||||
console.log("called")
|
||||
showReviewModal.value = true;
|
||||
showReviewModal.value = true
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
185
frontend/src/components/EvaluationModal.vue
Normal file
185
frontend/src/components/EvaluationModal.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<Dialog
|
||||
v-model="show"
|
||||
:options="{
|
||||
title: __('Schedule Evaluation'),
|
||||
size: 'xl',
|
||||
actions: [
|
||||
{
|
||||
label: 'Submit',
|
||||
variant: 'solid',
|
||||
onClick: (close) => submitEvaluation(close),
|
||||
},
|
||||
],
|
||||
}"
|
||||
>
|
||||
<template #body-content>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<div class="mb-1.5 text-sm text-gray-600">
|
||||
{{ __('Course') }}
|
||||
</div>
|
||||
<Select v-model="evaluation.course" :options="getCourses()" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-1.5 text-sm text-gray-600">
|
||||
{{ __('Date') }}
|
||||
</div>
|
||||
<DatePicker v-model="evaluation.date" />
|
||||
</div>
|
||||
<div v-if="slots.data">
|
||||
<div class="mb-1.5 text-sm text-gray-600">
|
||||
{{ __('Select a slot') }}
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div v-for="slot in slots.data">
|
||||
<div
|
||||
class="text-base text-center border rounded-md bg-gray-200 p-2 cursor-pointer"
|
||||
@click="saveSlot(slot)"
|
||||
:class="{
|
||||
'border-gray-900': evaluation.start_time == slot.start_time,
|
||||
}"
|
||||
>
|
||||
{{ formatTime(slot.start_time) }} -
|
||||
{{ formatTime(slot.end_time) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Dialog, createResource, Select, DatePicker } from 'frappe-ui'
|
||||
import { defineModel, reactive, watch, inject } from 'vue'
|
||||
import { createToast, formatTime } from '@/utils/'
|
||||
|
||||
const user = inject('$user')
|
||||
const dayjs = inject('$dayjs')
|
||||
const show = defineModel()
|
||||
const evaluations = defineModel('reloadEvals')
|
||||
|
||||
const props = defineProps({
|
||||
courses: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
batch: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
endDate: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
let evaluation = reactive({
|
||||
course: '',
|
||||
date: '',
|
||||
start_time: '',
|
||||
end_time: '',
|
||||
day: '',
|
||||
batch: props.batch,
|
||||
member: user.data.name,
|
||||
})
|
||||
|
||||
const createEvaluation = createResource({
|
||||
url: 'frappe.client.insert',
|
||||
makeParams(values) {
|
||||
return {
|
||||
doc: {
|
||||
doctype: 'LMS Certificate Request',
|
||||
batch_name: values.batch,
|
||||
...values,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function submitEvaluation(close) {
|
||||
createEvaluation.submit(evaluation, {
|
||||
validate() {
|
||||
if (!evaluation.course) {
|
||||
return 'Please select a course.'
|
||||
}
|
||||
if (!evaluation.date) {
|
||||
return 'Please select a date.'
|
||||
}
|
||||
if (!evaluation.start_time) {
|
||||
return 'Please select a slot.'
|
||||
}
|
||||
if (dayjs(evaluation.date).isSameOrBefore(dayjs(), 'day')) {
|
||||
return 'Please select a future date.'
|
||||
}
|
||||
if (dayjs(evaluation.date).isAfter(dayjs(props.endDate), 'day')) {
|
||||
return `Please select a date before the end date ${dayjs(
|
||||
props.endDate
|
||||
).format('DD MMMM YYYY')}.`
|
||||
}
|
||||
},
|
||||
onSuccess() {
|
||||
evaluations.value.reload()
|
||||
close()
|
||||
},
|
||||
onError(err) {
|
||||
createToast({
|
||||
title: 'Error',
|
||||
text: err.messages?.[0] || err,
|
||||
icon: 'x',
|
||||
iconClasses: 'bg-red-600 text-white rounded-md p-px',
|
||||
position: 'top-center',
|
||||
timeout: 10,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const getCourses = () => {
|
||||
let courses = []
|
||||
for (const course of props.courses) {
|
||||
courses.push({
|
||||
label: course.title,
|
||||
value: course.course,
|
||||
})
|
||||
}
|
||||
return courses
|
||||
}
|
||||
|
||||
const slots = createResource({
|
||||
url: 'lms.lms.doctype.course_evaluator.course_evaluator.get_schedule',
|
||||
makeParams(values) {
|
||||
return {
|
||||
course: values.course,
|
||||
date: values.date,
|
||||
batch: props.batch,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
watch(
|
||||
() => evaluation.date,
|
||||
(date) => {
|
||||
evaluation.start_time = ''
|
||||
if (date) {
|
||||
slots.submit(evaluation)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => evaluation.course,
|
||||
(course) => {
|
||||
evaluation.date = ''
|
||||
evaluation.start_time = ''
|
||||
slots.reset()
|
||||
}
|
||||
)
|
||||
|
||||
const saveSlot = (slot) => {
|
||||
evaluation.start_time = slot.start_time
|
||||
evaluation.end_time = slot.end_time
|
||||
evaluation.day = slot.day
|
||||
}
|
||||
</script>
|
||||
18
frontend/src/components/Icons/IndicatorIcon.vue
Normal file
18
frontend/src/components/Icons/IndicatorIcon.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle
|
||||
cx="8"
|
||||
cy="8"
|
||||
r="4.5"
|
||||
fill="transparent"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
110
frontend/src/components/LiveClass.vue
Normal file
110
frontend/src/components/LiveClass.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<Button
|
||||
v-if="user.data.is_moderator"
|
||||
variant="solid"
|
||||
class="float-right mb-3"
|
||||
@click="openLiveClassModal"
|
||||
>
|
||||
<template #prefix>
|
||||
<Plus class="h-4 w-4" />
|
||||
</template>
|
||||
<span>
|
||||
{{ __('Create') }}
|
||||
</span>
|
||||
</Button>
|
||||
<div class="text-lg font-semibold mb-4">
|
||||
{{ __('Live Class') }}
|
||||
</div>
|
||||
<div v-if="liveClasses.data?.length" class="grid grid-cols-2 gap-5">
|
||||
<div v-for="cls in liveClasses.data">
|
||||
<div class="border rounded-md p-3">
|
||||
<div class="font-semibold text-lg mb-4">
|
||||
{{ cls.title }}
|
||||
</div>
|
||||
<div class="flex items-center mb-2">
|
||||
<Calendar class="w-4 h-4 stroke-1.5" />
|
||||
<span class="ml-2">
|
||||
{{ dayjs(cls.date).format('DD MMMM YYYY') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center mb-5">
|
||||
<Clock class="w-4 h-4 stroke-1.5" />
|
||||
<span class="ml-2">
|
||||
{{ formatTime(cls.time) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mb-5">
|
||||
{{ cls.description }}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<a
|
||||
:href="cls.start_url"
|
||||
target="_blank"
|
||||
class="w-1/2 cursor-pointer inline-flex items-center justify-center gap-2 transition-colors focus:outline-none text-gray-800 bg-gray-100 hover:bg-gray-200 active:bg-gray-300 focus-visible:ring focus-visible:ring-gray-400 h-7 text-base px-2 rounded"
|
||||
>
|
||||
<Monitor class="h-4 w-4 stroke-1.5" />
|
||||
{{ __('Start') }}
|
||||
</a>
|
||||
<a
|
||||
:href="cls.join_url"
|
||||
target="_blank"
|
||||
class="w-1/2 cursor-pointer inline-flex items-center justify-center gap-2 transition-colors focus:outline-none text-gray-800 bg-gray-100 hover:bg-gray-200 active:bg-gray-300 focus-visible:ring focus-visible:ring-gray-400 h-7 text-base px-2 rounded"
|
||||
>
|
||||
<Video class="h-4 w-4 stroke-1.5" />
|
||||
{{ __('Join') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-sm italic text-gray-600">
|
||||
{{ __('No live classes scheduled') }}
|
||||
</div>
|
||||
<LiveClassModal
|
||||
:batch="props.batch"
|
||||
v-model="showLiveClassModal"
|
||||
v-model:reloadLiveClasses="liveClasses"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import { createListResource, Button } from 'frappe-ui'
|
||||
import { Plus, Clock, Calendar, Video, Monitor } from 'lucide-vue-next'
|
||||
import { inject } from 'vue'
|
||||
import LiveClassModal from '@/components/LiveClassModal.vue'
|
||||
import { ref } from 'vue'
|
||||
import { formatTime } from '@/utils/'
|
||||
|
||||
const user = inject('$user')
|
||||
const showLiveClassModal = ref(false)
|
||||
const dayjs = inject('$dayjs')
|
||||
|
||||
const props = defineProps({
|
||||
batch: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const liveClasses = createListResource({
|
||||
doctype: 'LMS Live Class',
|
||||
filters: {
|
||||
batch: props.batchName,
|
||||
date: ['>=', new Date()],
|
||||
},
|
||||
fields: [
|
||||
'title',
|
||||
'description',
|
||||
'time',
|
||||
'date',
|
||||
'start_url',
|
||||
'join_url',
|
||||
'owner',
|
||||
],
|
||||
orderBy: 'date',
|
||||
auto: true,
|
||||
})
|
||||
|
||||
const openLiveClassModal = () => {
|
||||
showLiveClassModal.value = true
|
||||
}
|
||||
</script>
|
||||
227
frontend/src/components/LiveClassModal.vue
Normal file
227
frontend/src/components/LiveClassModal.vue
Normal file
@@ -0,0 +1,227 @@
|
||||
<template>
|
||||
<Dialog
|
||||
v-model="show"
|
||||
:options="{
|
||||
title: __('Create a Live Class'),
|
||||
size: 'xl',
|
||||
actions: [
|
||||
{
|
||||
label: 'Submit',
|
||||
variant: 'solid',
|
||||
onClick: (close) => submitLiveClass(close),
|
||||
},
|
||||
],
|
||||
}"
|
||||
>
|
||||
<template #body-content>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<div class="mb-1.5 text-sm text-gray-600">
|
||||
{{ __('Title') }}
|
||||
</div>
|
||||
<Input type="text" v-model="liveClass.title" />
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div class="mb-1.5 text-sm text-gray-600">
|
||||
<Tooltip
|
||||
class="flex items-center"
|
||||
:text="
|
||||
__(
|
||||
'Time must be in 24 hour format (HH:mm). Example 11:30 or 22:00'
|
||||
)
|
||||
"
|
||||
>
|
||||
<span>
|
||||
{{ __('Time') }}
|
||||
</span>
|
||||
<Info class="stroke-2 w-3 h-3 ml-1" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Input v-model="liveClass.time" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-1.5 text-sm text-gray-600">
|
||||
{{ __('Timezone') }}
|
||||
</div>
|
||||
<Select
|
||||
v-model="liveClass.timezone"
|
||||
:options="getTimezoneOptions()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<div class="mb-1.5 text-sm text-gray-600">
|
||||
{{ __('Date') }}
|
||||
</div>
|
||||
<DatePicker v-model="liveClass.date" inputClass="w-full" />
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div class="mb-1.5 text-sm text-gray-600">
|
||||
<Tooltip
|
||||
class="flex items-center"
|
||||
:text="__('Duration of the live class in minutes')"
|
||||
>
|
||||
<span>
|
||||
{{ __('Duration') }}
|
||||
</span>
|
||||
<Info class="stroke-2 w-3 h-3 ml-1" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Input type="number" v-model="liveClass.duration" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-1.5 text-sm text-gray-600">
|
||||
{{ __('Auto Recording') }}
|
||||
</div>
|
||||
<Select
|
||||
v-model="liveClass.auto_recording"
|
||||
:options="getRecordingOptions()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-1.5 text-sm text-gray-600">
|
||||
{{ __('Description') }}
|
||||
</div>
|
||||
<Textarea v-model="liveClass.description" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import {
|
||||
Input,
|
||||
DatePicker,
|
||||
Select,
|
||||
Textarea,
|
||||
Dialog,
|
||||
createResource,
|
||||
Tooltip,
|
||||
} from 'frappe-ui'
|
||||
import { reactive, inject } from 'vue'
|
||||
import { getTimezones, createToast } from '@/utils/'
|
||||
import { Info } from 'lucide-vue-next'
|
||||
|
||||
const liveClasses = defineModel('reloadLiveClasses')
|
||||
const show = defineModel()
|
||||
const user = inject('$user')
|
||||
const dayjs = inject('$dayjs')
|
||||
|
||||
const props = defineProps({
|
||||
batch: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
let liveClass = reactive({
|
||||
title: '',
|
||||
description: '',
|
||||
date: '',
|
||||
time: '',
|
||||
duration: '',
|
||||
timezone: '',
|
||||
auto_recording: 'No Recording',
|
||||
batch: props.batch,
|
||||
host: user.data.name,
|
||||
})
|
||||
console.log(liveClass)
|
||||
|
||||
const getTimezoneOptions = () => {
|
||||
return getTimezones().map((timezone) => {
|
||||
return {
|
||||
label: timezone,
|
||||
value: timezone,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getRecordingOptions = () => {
|
||||
return [
|
||||
{
|
||||
label: 'No Recording',
|
||||
value: 'No Recording',
|
||||
},
|
||||
{
|
||||
label: 'Local',
|
||||
value: 'Local',
|
||||
},
|
||||
{
|
||||
label: 'Cloud',
|
||||
value: 'Cloud',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const createLiveClass = createResource({
|
||||
url: 'lms.lms.doctype.lms_batch.lms_batch.create_live_class',
|
||||
makeParams(values) {
|
||||
return {
|
||||
doctype: 'LMS Live Class',
|
||||
batch_name: values.batch,
|
||||
...values,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const submitLiveClass = (close) => {
|
||||
createLiveClass.submit(liveClass, {
|
||||
validate() {
|
||||
if (!liveClass.title) {
|
||||
return 'Please enter a title.'
|
||||
}
|
||||
if (!liveClass.date) {
|
||||
return 'Please select a date.'
|
||||
}
|
||||
if (dayjs(liveClass.date).isSameOrBefore(dayjs(), 'day')) {
|
||||
return 'Please select a future date.'
|
||||
}
|
||||
if (!liveClass.time) {
|
||||
return 'Please select a time.'
|
||||
}
|
||||
if (!valideTime()) {
|
||||
return 'Please enter a valid time in the format HH:mm.'
|
||||
}
|
||||
if (!liveClass.duration) {
|
||||
return 'Please select a duration.'
|
||||
}
|
||||
if (!liveClass.timezone) {
|
||||
return 'Please select a timezone.'
|
||||
}
|
||||
},
|
||||
onSuccess() {
|
||||
liveClasses.value.reload()
|
||||
close()
|
||||
},
|
||||
onError(err) {
|
||||
createToast({
|
||||
title: 'Error',
|
||||
text: err.messages?.[0] || err,
|
||||
icon: 'x',
|
||||
iconClasses: 'bg-red-600 text-white rounded-md p-px',
|
||||
position: 'top-center',
|
||||
timeout: 10,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const valideTime = () => {
|
||||
let time = liveClass.time.split(':')
|
||||
if (time.length != 2) {
|
||||
return false
|
||||
}
|
||||
if (time[0] < 0 || time[0] > 23) {
|
||||
return false
|
||||
}
|
||||
if (time[1] < 0 || time[1] > 59) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
</script>
|
||||
@@ -1,35 +0,0 @@
|
||||
<template>
|
||||
<div class="flex text-center">
|
||||
<div v-for="index in 5">
|
||||
<Star :class="{'fill-orange-500': index <= rating}"
|
||||
class="h-5 w-5 fill-gray-400 text-gray-200 mr-1 cursor-pointer" @click="markRating(index)"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Star } from 'lucide-vue-next'
|
||||
import { ref } from 'vue'
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
modelValue: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
let rating = ref(props.modelValue)
|
||||
|
||||
let emitChange = (value) => {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
function markRating(index) {
|
||||
emitChange(index);
|
||||
rating.value = index;
|
||||
}
|
||||
</script>
|
||||
@@ -1,83 +1,87 @@
|
||||
<template>
|
||||
<Dialog v-model="show" :options='{
|
||||
title: __("Write a Review"),
|
||||
size: "xl",
|
||||
actions: [
|
||||
{
|
||||
label: "Submit",
|
||||
variant: "solid",
|
||||
onClick: (close) => submitReview(close)
|
||||
}
|
||||
]
|
||||
}'>
|
||||
<template #body-content>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<div class="mb-1.5 text-sm text-gray-600">
|
||||
{{ __("Rating") }}
|
||||
</div>
|
||||
<Rating v-model="review.rating"/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-1.5 text-sm text-gray-600">
|
||||
{{ __("Review") }}
|
||||
</div>
|
||||
<Textarea type="text" size="md" rows=5 v-model="review.review"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
<Dialog
|
||||
v-model="show"
|
||||
:options="{
|
||||
title: __('Write a Review'),
|
||||
size: 'xl',
|
||||
actions: [
|
||||
{
|
||||
label: 'Submit',
|
||||
variant: 'solid',
|
||||
onClick: (close) => submitReview(close),
|
||||
},
|
||||
],
|
||||
}"
|
||||
>
|
||||
<template #body-content>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<div class="mb-1.5 text-sm text-gray-600">
|
||||
{{ __('Rating') }}
|
||||
</div>
|
||||
<Rating v-model="review.rating" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-1.5 text-sm text-gray-600">
|
||||
{{ __('Review') }}
|
||||
</div>
|
||||
<Textarea type="text" size="md" rows="5" v-model="review.review" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Dialog, Textarea, createResource } from 'frappe-ui'
|
||||
import { defineModel, reactive } from "vue"
|
||||
import Rating from '@/components/Rating.vue';
|
||||
import { createToast } from "@/utils/"
|
||||
import { defineModel, reactive } from 'vue'
|
||||
import Rating from '@/components/Controls/Rating.vue'
|
||||
import { createToast } from '@/utils/'
|
||||
|
||||
const show = defineModel()
|
||||
const reviews = defineModel("reloadReviews")
|
||||
const reviews = defineModel('reloadReviews')
|
||||
let review = reactive({
|
||||
review: "",
|
||||
rating: 0,
|
||||
review: '',
|
||||
rating: 0,
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
courseName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
courseName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const createReview = createResource({
|
||||
url: "frappe.client.insert",
|
||||
makeParams(values) {
|
||||
return {
|
||||
doc: {
|
||||
doctype: "LMS Course Review",
|
||||
course: props.courseName,
|
||||
...values,
|
||||
}
|
||||
}
|
||||
}
|
||||
url: 'frappe.client.insert',
|
||||
makeParams(values) {
|
||||
return {
|
||||
doc: {
|
||||
doctype: 'LMS Course Review',
|
||||
course: props.courseName,
|
||||
...values,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
function submitReview(close) {
|
||||
review.rating = (review.rating)/5;
|
||||
createReview.submit(review, {
|
||||
validate() {
|
||||
if (!review.rating) {
|
||||
return "Please enter a rating."
|
||||
}
|
||||
}, onSuccess() {
|
||||
reviews.value.reload()
|
||||
},
|
||||
onError(err) {
|
||||
createToast({
|
||||
text: err.messages?.[0] || err,
|
||||
icon: 'x',
|
||||
iconClasses: 'text-red-600 bg-red-300',
|
||||
})
|
||||
}
|
||||
})
|
||||
close();
|
||||
review.rating = review.rating / 5
|
||||
createReview.submit(review, {
|
||||
validate() {
|
||||
if (!review.rating) {
|
||||
return 'Please enter a rating.'
|
||||
}
|
||||
},
|
||||
onSuccess() {
|
||||
reviews.value.reload()
|
||||
},
|
||||
onError(err) {
|
||||
createToast({
|
||||
text: err.messages?.[0] || err,
|
||||
icon: 'x',
|
||||
iconClasses: 'text-red-600 bg-red-300',
|
||||
})
|
||||
},
|
||||
})
|
||||
close()
|
||||
}
|
||||
</script>
|
||||
|
||||
70
frontend/src/components/StudentModal.vue
Normal file
70
frontend/src/components/StudentModal.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<Dialog
|
||||
v-model="show"
|
||||
:options="{
|
||||
title: __('Add a Student'),
|
||||
size: 'xl',
|
||||
actions: [
|
||||
{
|
||||
label: 'Submit',
|
||||
variant: 'solid',
|
||||
onClick: (close) => addStudent(close),
|
||||
},
|
||||
],
|
||||
}"
|
||||
>
|
||||
<template #body-content>
|
||||
<div class="flex flex-col gap-4">
|
||||
<Link
|
||||
doctype="User"
|
||||
v-model="student"
|
||||
:filters="{ ignore_user_type: 1 }"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Dialog, createResource } from 'frappe-ui'
|
||||
import { ref } from 'vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
|
||||
const students = defineModel('reloadStudents')
|
||||
const student = ref()
|
||||
const show = defineModel()
|
||||
|
||||
const props = defineProps({
|
||||
batch: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const studentResource = createResource({
|
||||
url: 'frappe.client.insert',
|
||||
makeParams(values) {
|
||||
return {
|
||||
doc: {
|
||||
doctype: 'Batch Student',
|
||||
parent: props.batch,
|
||||
parenttype: 'LMS Batch',
|
||||
parentfield: 'students',
|
||||
student: student.value,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const addStudent = (close) => {
|
||||
studentResource.submit(
|
||||
{},
|
||||
{
|
||||
onSuccess() {
|
||||
students.value.reload()
|
||||
close()
|
||||
student.value = null
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
</script>
|
||||
@@ -1,11 +1,14 @@
|
||||
<template>
|
||||
<div class="mb-10">
|
||||
<Button v-if="isStudent" @click="openEvalModal" class="float-right">
|
||||
{{ __('Schedule Evaluation') }}
|
||||
</Button>
|
||||
<div class="text-lg font-semibold mb-4">
|
||||
{{ __('Upcoming Evaluations') }}
|
||||
</div>
|
||||
<div v-if="upcoming_evals.length">
|
||||
<div class="grid grid-cols-2">
|
||||
<div v-for="evl in upcoming_evals">
|
||||
<div v-if="upcoming_evals.data">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div v-for="evl in upcoming_evals.data">
|
||||
<div class="border rounded-md p-3">
|
||||
<div class="font-medium mb-3">
|
||||
{{ evl.course_title }}
|
||||
@@ -24,7 +27,7 @@
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<UserCog2 class="w-4 h-4 stroke-1.5" />
|
||||
<span class="ml-2">
|
||||
<span class="ml-2 font-medium">
|
||||
{{ evl.evaluator_name }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -36,18 +39,55 @@
|
||||
{{ __('No upcoming evaluations.') }}
|
||||
</div>
|
||||
</div>
|
||||
<EvaluationModal
|
||||
:batch="batch"
|
||||
:endDate="endDate"
|
||||
:courses="courses"
|
||||
v-model="showEvalModal"
|
||||
v-model:reloadEvals="upcoming_evals"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Calendar, Clock, UserCog2 } from 'lucide-vue-next'
|
||||
import { inject } from 'vue'
|
||||
import { inject, ref } from 'vue'
|
||||
import { formatTime } from '../utils'
|
||||
import { Button, createResource } from 'frappe-ui'
|
||||
import EvaluationModal from '@/components/EvaluationModal.vue'
|
||||
|
||||
const dayjs = inject('$dayjs')
|
||||
const user = inject('$user')
|
||||
const showEvalModal = ref(false)
|
||||
|
||||
const props = defineProps({
|
||||
upcoming_evals: {
|
||||
batch: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
courses: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
isStudent: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
endDate: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const upcoming_evals = createResource({
|
||||
url: 'lms.lms.utils.get_upcoming_evals',
|
||||
cache: ['upcoming_evals', user.data.name],
|
||||
params: {
|
||||
student: user.data.name,
|
||||
courses: props.courses.map((course) => course.course),
|
||||
},
|
||||
auto: true,
|
||||
})
|
||||
|
||||
function openEvalModal() {
|
||||
showEvalModal.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-if="user.data?.is_moderator || is_student" class="h-screen text-base">
|
||||
<div v-if="user.data?.is_moderator || isStudent" class="h-screen text-base">
|
||||
<header
|
||||
class="sticky top-0 z-10 flex items-center justify-between border-b bg-white px-3 py-2.5 sm:px-5"
|
||||
>
|
||||
@@ -36,7 +36,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ tab }">
|
||||
<div class="p-10">
|
||||
<div class="pt-5 px-10 pb-10">
|
||||
<div v-if="tab.label == 'Courses'">
|
||||
<div class="text-xl font-semibold">
|
||||
{{ __('Courses') }}
|
||||
@@ -59,7 +59,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="tab.label == 'Dashboard'">
|
||||
<BatchDashboard :batch="batch" />
|
||||
<BatchDashboard :batch="batch" :isStudent="isStudent" />
|
||||
</div>
|
||||
<div v-else-if="tab.label == 'Live Class'">
|
||||
<LiveClass :batch="batch.data.name" />
|
||||
</div>
|
||||
<div v-else-if="tab.label == 'Students'">
|
||||
<BatchStudents :batch="batch.data.name" />
|
||||
</div>
|
||||
<div v-else-if="tab.label == 'Assessments'">
|
||||
<Assessments :batch="batch.data.name" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -72,8 +81,8 @@
|
||||
<div class="flex items-center mb-3">
|
||||
<Calendar class="h-4 w-4 stroke-1.5 mr-2 text-gray-700" />
|
||||
<span>
|
||||
{{ dayjs(batch.data.start_date).format('DD MMM YYYY') }} -
|
||||
{{ dayjs(batch.data.end_date).format('DD MMM YYYY') }}
|
||||
{{ dayjs(batch.data.start_date).format('DD MMMM YYYY') }} -
|
||||
{{ dayjs(batch.data.end_date).format('DD MMMM YYYY') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center mb-6">
|
||||
@@ -97,14 +106,18 @@
|
||||
{{ __('Not Permitted') }}
|
||||
</div>
|
||||
<div class="px-5 py-3">
|
||||
<div class="mb-4 leading-6">
|
||||
<div v-if="user.data" class="mb-4 leading-6">
|
||||
{{
|
||||
__(
|
||||
'You are not a member of this batch. Please checkout our upcoming batches.'
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<div v-else class="mb-4 leading-6">
|
||||
{{ __('Please login to access this page.') }}
|
||||
</div>
|
||||
<router-link
|
||||
v-if="user.data"
|
||||
:to="{
|
||||
name: 'Batches',
|
||||
params: {
|
||||
@@ -116,6 +129,14 @@
|
||||
{{ __('Upcoming Batches') }}
|
||||
</Button>
|
||||
</router-link>
|
||||
<Button
|
||||
v-else
|
||||
variant="solid"
|
||||
class="w-full"
|
||||
@click="redirectToLogin()"
|
||||
>
|
||||
{{ __('Login') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -123,10 +144,21 @@
|
||||
<script setup>
|
||||
import { Breadcrumbs, Button, createResource, Tabs, Badge } from 'frappe-ui'
|
||||
import { computed, inject, ref } from 'vue'
|
||||
import { Calendar, Clock, LayoutDashboard, BookOpen } from 'lucide-vue-next'
|
||||
import {
|
||||
Calendar,
|
||||
Clock,
|
||||
LayoutDashboard,
|
||||
BookOpen,
|
||||
Laptop,
|
||||
BookOpenCheck,
|
||||
Contact2,
|
||||
} from 'lucide-vue-next'
|
||||
import { formatTime } from '@/utils'
|
||||
import CourseCard from '@/components/CourseCard.vue'
|
||||
import BatchDashboard from '@/components/BatchDashboard.vue'
|
||||
import LiveClass from '@/components/LiveClass.vue'
|
||||
import BatchStudents from '@/components/BatchStudents.vue'
|
||||
import Assessments from '@/components/Assessments.vue'
|
||||
|
||||
const dayjs = inject('$dayjs')
|
||||
const user = inject('$user')
|
||||
@@ -161,7 +193,7 @@ const breadcrumbs = computed(() => {
|
||||
]
|
||||
})
|
||||
|
||||
const is_student = computed(() => {
|
||||
const isStudent = computed(() => {
|
||||
return (
|
||||
user?.data &&
|
||||
batch.data?.students.length &&
|
||||
@@ -172,16 +204,31 @@ const is_student = computed(() => {
|
||||
const tabIndex = ref(0)
|
||||
const tabs = []
|
||||
|
||||
if (is_student) {
|
||||
if (isStudent.value) {
|
||||
tabs.push({
|
||||
label: 'Dashboard',
|
||||
icon: LayoutDashboard,
|
||||
})
|
||||
}
|
||||
|
||||
if (user.data?.is_moderator) {
|
||||
tabs.push({
|
||||
label: 'Students',
|
||||
icon: Contact2,
|
||||
})
|
||||
tabs.push({
|
||||
label: 'Assessments',
|
||||
icon: BookOpenCheck,
|
||||
})
|
||||
}
|
||||
|
||||
tabs.push({
|
||||
label: 'Live Class',
|
||||
icon: Laptop,
|
||||
})
|
||||
|
||||
tabs.push({
|
||||
label: 'Courses',
|
||||
count: computed(() => courses?.data?.length),
|
||||
icon: BookOpen,
|
||||
})
|
||||
|
||||
@@ -193,4 +240,8 @@ const courses = createResource({
|
||||
cache: ['batchCourses', props.batchName],
|
||||
auto: true,
|
||||
})
|
||||
|
||||
const redirectToLogin = () => {
|
||||
window.location.href = `/login?redirect-to=/batches`
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -377,7 +377,7 @@ iframe {
|
||||
background: #011627;
|
||||
color: #d6deeb;
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.lesson-content a {
|
||||
|
||||
@@ -3,10 +3,14 @@ import relativeTime from 'dayjs/esm/plugin/relativeTime'
|
||||
import localizedFormat from 'dayjs/esm/plugin/localizedFormat'
|
||||
import updateLocale from 'dayjs/esm/plugin/updateLocale'
|
||||
import isToday from 'dayjs/esm/plugin/isToday'
|
||||
import isSameOrBefore from 'dayjs/esm/plugin/isSameOrBefore'
|
||||
import isSameOrAfter from 'dayjs/esm/plugin/isSameOrAfter'
|
||||
|
||||
dayjs.extend(updateLocale)
|
||||
dayjs.extend(relativeTime)
|
||||
dayjs.extend(localizedFormat)
|
||||
dayjs.extend(isToday)
|
||||
dayjs.extend(isSameOrBefore)
|
||||
dayjs.extend(isSameOrAfter)
|
||||
|
||||
export default dayjs
|
||||
|
||||
@@ -39,3 +39,143 @@ export function formatNumberIntoCurrency(number, currency) {
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export function getTimezones() {
|
||||
return [
|
||||
'Pacific/Midway',
|
||||
'Pacific/Pago_Pago',
|
||||
'Pacific/Honolulu',
|
||||
'America/Anchorage',
|
||||
'America/Vancouver',
|
||||
'America/Los_Angeles',
|
||||
'America/Tijuana',
|
||||
'America/Edmonton',
|
||||
'America/Denver',
|
||||
'America/Phoenix',
|
||||
'America/Mazatlan',
|
||||
'America/Winnipeg',
|
||||
'America/Regina',
|
||||
'America/Chicago',
|
||||
'America/Mexico_City',
|
||||
'America/Guatemala',
|
||||
'America/El_Salvador',
|
||||
'America/Managua',
|
||||
'America/Costa_Rica',
|
||||
'America/Montreal',
|
||||
'America/New_York',
|
||||
'America/Indianapolis',
|
||||
'America/Panama',
|
||||
'America/Bogota',
|
||||
'America/Lima',
|
||||
'America/Halifax',
|
||||
'America/Puerto_Rico',
|
||||
'America/Caracas',
|
||||
'America/Santiago',
|
||||
'America/St_Johns',
|
||||
'America/Montevideo',
|
||||
'America/Araguaina',
|
||||
'America/Argentina/Buenos_Aires',
|
||||
'America/Godthab',
|
||||
'America/Sao_Paulo',
|
||||
'Atlantic/Azores',
|
||||
'Canada/Atlantic',
|
||||
'Atlantic/Cape_Verde',
|
||||
'UTC',
|
||||
'Etc/Greenwich',
|
||||
'Europe/Belgrade',
|
||||
'CET',
|
||||
'Atlantic/Reykjavik',
|
||||
'Europe/Dublin',
|
||||
'Europe/London',
|
||||
'Europe/Lisbon',
|
||||
'Africa/Casablanca',
|
||||
'Africa/Nouakchott',
|
||||
'Europe/Oslo',
|
||||
'Europe/Copenhagen',
|
||||
'Europe/Brussels',
|
||||
'Europe/Berlin',
|
||||
'Europe/Helsinki',
|
||||
'Europe/Amsterdam',
|
||||
'Europe/Rome',
|
||||
'Europe/Stockholm',
|
||||
'Europe/Vienna',
|
||||
'Europe/Luxembourg',
|
||||
'Europe/Paris',
|
||||
'Europe/Zurich',
|
||||
'Europe/Madrid',
|
||||
'Africa/Bangui',
|
||||
'Africa/Algiers',
|
||||
'Africa/Tunis',
|
||||
'Africa/Harare',
|
||||
'Africa/Nairobi',
|
||||
'Europe/Warsaw',
|
||||
'Europe/Prague',
|
||||
'Europe/Budapest',
|
||||
'Europe/Sofia',
|
||||
'Europe/Istanbul',
|
||||
'Europe/Athens',
|
||||
'Europe/Bucharest',
|
||||
'Asia/Nicosia',
|
||||
'Asia/Beirut',
|
||||
'Asia/Damascus',
|
||||
'Asia/Jerusalem',
|
||||
'Asia/Amman',
|
||||
'Africa/Tripoli',
|
||||
'Africa/Cairo',
|
||||
'Africa/Johannesburg',
|
||||
'Europe/Moscow',
|
||||
'Asia/Baghdad',
|
||||
'Asia/Kuwait',
|
||||
'Asia/Riyadh',
|
||||
'Asia/Bahrain',
|
||||
'Asia/Qatar',
|
||||
'Asia/Aden',
|
||||
'Asia/Tehran',
|
||||
'Africa/Khartoum',
|
||||
'Africa/Djibouti',
|
||||
'Africa/Mogadishu',
|
||||
'Asia/Dubai',
|
||||
'Asia/Muscat',
|
||||
'Asia/Baku',
|
||||
'Asia/Kabul',
|
||||
'Asia/Yekaterinburg',
|
||||
'Asia/Tashkent',
|
||||
'Asia/Calcutta',
|
||||
'Asia/Kathmandu',
|
||||
'Asia/Novosibirsk',
|
||||
'Asia/Almaty',
|
||||
'Asia/Dacca',
|
||||
'Asia/Krasnoyarsk',
|
||||
'Asia/Dhaka',
|
||||
'Asia/Bangkok',
|
||||
'Asia/Saigon',
|
||||
'Asia/Jakarta',
|
||||
'Asia/Irkutsk',
|
||||
'Asia/Shanghai',
|
||||
'Asia/Hong_Kong',
|
||||
'Asia/Taipei',
|
||||
'Asia/Kuala_Lumpur',
|
||||
'Asia/Singapore',
|
||||
'Australia/Perth',
|
||||
'Asia/Yakutsk',
|
||||
'Asia/Seoul',
|
||||
'Asia/Tokyo',
|
||||
'Australia/Darwin',
|
||||
'Australia/Adelaide',
|
||||
'Asia/Vladivostok',
|
||||
'Pacific/Port_Moresby',
|
||||
'Australia/Brisbane',
|
||||
'Australia/Sydney',
|
||||
'Australia/Hobart',
|
||||
'Asia/Magadan',
|
||||
'SST',
|
||||
'Pacific/Noumea',
|
||||
'Asia/Kamchatka',
|
||||
'Pacific/Fiji',
|
||||
'Pacific/Auckland',
|
||||
'Asia/Kolkata',
|
||||
'Europe/Kiev',
|
||||
'America/Tegucigalpa',
|
||||
'Pacific/Apia',
|
||||
]
|
||||
}
|
||||
|
||||
@@ -20,8 +20,12 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: `../${path.basename(path.resolve('..'))}/public/frontend`,
|
||||
outDir: `../lms/public/frontend`,
|
||||
emptyOutDir: true,
|
||||
commonjsOptions: {
|
||||
include: [/tailwind.config.js/, /node_modules/],
|
||||
},
|
||||
sourcemap: true,
|
||||
target: 'es2015',
|
||||
rollupOptions: {
|
||||
output: {
|
||||
|
||||
1376
frontend/yarn.lock
1376
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from lms.lms.utils import get_evaluator
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class CourseEvaluator(Document):
|
||||
@@ -39,10 +40,14 @@ class CourseEvaluator(Document):
|
||||
@frappe.whitelist()
|
||||
def get_schedule(course, date, batch=None):
|
||||
evaluator = get_evaluator(course, batch)
|
||||
day = datetime.strptime(date, "%Y-%m-%d").strftime("%A")
|
||||
|
||||
all_slots = frappe.get_all(
|
||||
"Evaluator Schedule",
|
||||
filters={"parent": evaluator},
|
||||
filters={
|
||||
"parent": evaluator,
|
||||
"day": day,
|
||||
},
|
||||
fields=["day", "start_time", "end_time"],
|
||||
order_by="start_time",
|
||||
)
|
||||
|
||||
@@ -14,8 +14,13 @@ from frappe.utils import (
|
||||
format_datetime,
|
||||
get_time,
|
||||
)
|
||||
from lms.lms.utils import get_lessons, get_lesson_index, get_lesson_url
|
||||
from lms.www.utils import get_quiz_details, get_assignment_details
|
||||
from lms.lms.utils import (
|
||||
get_lessons,
|
||||
get_lesson_index,
|
||||
get_lesson_url,
|
||||
get_quiz_details,
|
||||
get_assignment_details,
|
||||
)
|
||||
from frappe.email.doctype.email_template.email_template import get_email_template
|
||||
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-29 15:00:30.617298",
|
||||
"modified": "2024-01-09 10:05:13.918890",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Certificate Request",
|
||||
@@ -151,6 +151,16 @@
|
||||
"role": "Moderator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS Student",
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-20 11:29:20.899897",
|
||||
"modified": "2024-01-09 11:22:33.272341",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Live Class",
|
||||
@@ -155,6 +155,15 @@
|
||||
"role": "Moderator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS Student",
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"show_title_field_in_link": 1,
|
||||
|
||||
@@ -27,6 +27,7 @@ from frappe.utils import (
|
||||
pretty_date,
|
||||
get_time_str,
|
||||
nowtime,
|
||||
format_datetime,
|
||||
)
|
||||
from frappe.utils.dateutils import get_period
|
||||
from lms.lms.md import find_macros, markdown_to_html
|
||||
@@ -886,6 +887,7 @@ def get_evaluator(course, batch=None):
|
||||
return evaluator
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_upcoming_evals(student, courses):
|
||||
upcoming_evals = frappe.get_all(
|
||||
"LMS Certificate Request",
|
||||
@@ -1378,27 +1380,19 @@ def get_batch_details(batch):
|
||||
"amount",
|
||||
"currency",
|
||||
"paid_batch",
|
||||
"evaluation_end_date",
|
||||
],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
batch_details.courses = frappe.get_all(
|
||||
"Batch Course", {"parent": batch}, pluck="course"
|
||||
"Batch Course", filters={"parent": batch}, fields=["course", "title"]
|
||||
)
|
||||
batch_details.students = frappe.get_all(
|
||||
"Batch Student", {"parent": batch}, pluck="student"
|
||||
)
|
||||
batch_details.price = fmt_money(batch_details.amount, 0, batch_details.currency)
|
||||
|
||||
is_student = frappe.session.user in batch_details.students
|
||||
if frappe.session.user != "Guest":
|
||||
if is_student:
|
||||
batch_details.upcoming_evals = get_upcoming_evals(
|
||||
frappe.session.user, batch_details.courses
|
||||
)
|
||||
if is_student or has_course_moderator_role():
|
||||
batch_details.assessments = get_assessments(batch, frappe.session.user)
|
||||
|
||||
if batch_details.seat_count:
|
||||
students_enrolled = frappe.db.count(
|
||||
"Batch Student",
|
||||
@@ -1479,6 +1473,7 @@ def get_batch_courses(batch):
|
||||
return courses
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_assessments(batch, member=None):
|
||||
if not member:
|
||||
member = frappe.session.user
|
||||
@@ -1520,6 +1515,10 @@ def get_assignment_details(assessment, member):
|
||||
as_dict=True,
|
||||
)
|
||||
assessment.completed = True
|
||||
assessment.status = assessment.submission.status
|
||||
else:
|
||||
assessment.status = "Not Attempted"
|
||||
assessment.color = "red"
|
||||
|
||||
assessment.edit_url = f"/assignments/{assessment.assessment_name}"
|
||||
submission_name = existing_submission if existing_submission else "new-submission"
|
||||
@@ -1548,10 +1547,12 @@ def get_quiz_details(assessment, member):
|
||||
|
||||
if len(existing_submission):
|
||||
assessment.submission = existing_submission[0]
|
||||
|
||||
assessment.completed = False
|
||||
if assessment.submission:
|
||||
assessment.completed = True
|
||||
assessment.status = assessment.submission.score
|
||||
else:
|
||||
assessment.status = "Not Attempted"
|
||||
assessment.color = "red"
|
||||
assessment.completed = False
|
||||
|
||||
assessment.edit_url = f"/quizzes/{assessment.assessment_name}"
|
||||
submission_name = (
|
||||
@@ -1560,3 +1561,58 @@ def get_quiz_details(assessment, member):
|
||||
assessment.url = f"/quiz-submission/{assessment.assessment_name}/{submission_name}"
|
||||
|
||||
return assessment
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_batch_students(batch):
|
||||
students = []
|
||||
|
||||
students_list = frappe.get_all(
|
||||
"Batch Student", filters={"parent": batch}, fields=["student", "name"]
|
||||
)
|
||||
|
||||
batch_courses = frappe.get_all("Batch Course", {"parent": batch}, pluck="course")
|
||||
|
||||
assessments = frappe.get_all(
|
||||
"LMS Assessment",
|
||||
filters={"parent": batch},
|
||||
fields=["name", "assessment_type", "assessment_name"],
|
||||
)
|
||||
|
||||
for student in students_list:
|
||||
courses_completed = 0
|
||||
assessments_completed = 0
|
||||
detail = frappe.db.get_value(
|
||||
"User",
|
||||
student.student,
|
||||
["full_name", "email", "username", "last_active", "user_image"],
|
||||
as_dict=True,
|
||||
)
|
||||
detail.last_active = format_datetime(detail.last_active, "dd MMM YY")
|
||||
detail.name = student.name
|
||||
students.append(detail)
|
||||
|
||||
for course in batch_courses:
|
||||
progress = frappe.db.get_value(
|
||||
"LMS Enrollment", {"course": course, "member": student.student}, "progress"
|
||||
)
|
||||
|
||||
if progress == 100:
|
||||
courses_completed += 1
|
||||
|
||||
detail.courses_completed = courses_completed
|
||||
|
||||
for assessment in assessments:
|
||||
if has_submitted_assessment(
|
||||
assessment.assessment_name, assessment.assessment_type, student.student
|
||||
):
|
||||
assessments_completed += 1
|
||||
|
||||
detail.assessments_completed = assessments_completed
|
||||
|
||||
return students
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_users():
|
||||
return frappe.get_all("User", {"enabled": 1}, pluck="name")
|
||||
|
||||
@@ -23,13 +23,13 @@ import {
|
||||
a0 as H,
|
||||
a1 as O,
|
||||
$ as S,
|
||||
} from "./frappe-ui.f2211ca2.js";
|
||||
import { f as $ } from "./index.05189aed.js";
|
||||
import { _ as q } from "./CourseCard.6a41330a.js";
|
||||
import { C as L, a as V } from "./clock.4d13ba48.js";
|
||||
import { c as U, B as J } from "./index.43e529db.js";
|
||||
import "./UserAvatar.b64a03ac.js";
|
||||
import "./star.d3e8ecca.js";
|
||||
} from "./frappe-ui.a747cf9c.js";
|
||||
import { f as $ } from "./index.6f049c1a.js";
|
||||
import { _ as q } from "./CourseCard.bf057db6.js";
|
||||
import { C as L, a as V } from "./clock.b36d19aa.js";
|
||||
import { c as U, B as J } from "./index.51e5b051.js";
|
||||
import "./UserAvatar.3cd4adb4.js";
|
||||
import "./star.d358f014.js";
|
||||
const K = U("LayoutDashboardIcon", [
|
||||
[
|
||||
"rect",
|
||||
@@ -18,13 +18,13 @@ import {
|
||||
K as j,
|
||||
L,
|
||||
Z as T,
|
||||
} from "./frappe-ui.f2211ca2.js";
|
||||
import { a as H, f as p } from "./index.05189aed.js";
|
||||
import { B as $ } from "./index.43e529db.js";
|
||||
import { C as B, a as C } from "./clock.4d13ba48.js";
|
||||
import { _ as O } from "./CourseCard.6a41330a.js";
|
||||
import "./UserAvatar.b64a03ac.js";
|
||||
import "./star.d3e8ecca.js";
|
||||
} from "./frappe-ui.a747cf9c.js";
|
||||
import { a as H, f as p } from "./index.6f049c1a.js";
|
||||
import { B as $ } from "./index.51e5b051.js";
|
||||
import { C as B, a as C } from "./clock.b36d19aa.js";
|
||||
import { _ as O } from "./CourseCard.bf057db6.js";
|
||||
import "./UserAvatar.3cd4adb4.js";
|
||||
import "./star.d358f014.js";
|
||||
const S = { key: 0, class: "shadow rounded-md p-5", style: { width: "300px" } },
|
||||
V = { key: 2, class: "text-lg font-semibold mb-3" },
|
||||
E = { class: "flex items-center mb-3" },
|
||||
@@ -22,11 +22,11 @@ import {
|
||||
Z as z,
|
||||
$ as L,
|
||||
a1 as P,
|
||||
} from "./frappe-ui.f2211ca2.js";
|
||||
import { f as B } from "./index.05189aed.js";
|
||||
import { B as A } from "./index.43e529db.js";
|
||||
import { C as E, a as O } from "./clock.4d13ba48.js";
|
||||
import { P as S } from "./plus.8f4bce9f.js";
|
||||
} from "./frappe-ui.a747cf9c.js";
|
||||
import { f as B } from "./index.6f049c1a.js";
|
||||
import { B as A } from "./index.51e5b051.js";
|
||||
import { C as E, a as O } from "./clock.b36d19aa.js";
|
||||
import { P as S } from "./plus.d245902e.js";
|
||||
const F = {
|
||||
class: "flex flex-col border border-gray-200 rounded-md p-4 h-full",
|
||||
style: { "min-height": "150px" },
|
||||
@@ -1,5 +1,5 @@
|
||||
import { _ as f } from "./UserAvatar.b64a03ac.js";
|
||||
import { s as g, B as v, U as y } from "./index.43e529db.js";
|
||||
import { _ as f } from "./UserAvatar.3cd4adb4.js";
|
||||
import { s as g, B as v, U as y } from "./index.51e5b051.js";
|
||||
import {
|
||||
s,
|
||||
u as r,
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
X as b,
|
||||
a0 as k,
|
||||
y as w,
|
||||
} from "./frappe-ui.f2211ca2.js";
|
||||
import { S as _ } from "./star.d3e8ecca.js";
|
||||
} from "./frappe-ui.a747cf9c.js";
|
||||
import { S as _ } from "./star.d358f014.js";
|
||||
const C = {
|
||||
key: 0,
|
||||
class: "flex flex-col border border-gray-200 h-full rounded-md shadow-sm text-base overflow-auto",
|
||||
@@ -44,12 +44,12 @@ import {
|
||||
a2 as K,
|
||||
c as Z,
|
||||
Z as G,
|
||||
} from "./frappe-ui.f2211ca2.js";
|
||||
import { c as N } from "./index.05189aed.js";
|
||||
import { U as B, B as I } from "./index.43e529db.js";
|
||||
import { S as x } from "./star.d3e8ecca.js";
|
||||
import { _ as J } from "./CourseOutline.df6c648a.js";
|
||||
import { _ as E } from "./UserAvatar.b64a03ac.js";
|
||||
} from "./frappe-ui.a747cf9c.js";
|
||||
import { c as N } from "./index.6f049c1a.js";
|
||||
import { U as B, B as I } from "./index.51e5b051.js";
|
||||
import { S as x } from "./star.d358f014.js";
|
||||
import { _ as J } from "./CourseOutline.2110618a.js";
|
||||
import { _ as E } from "./UserAvatar.3cd4adb4.js";
|
||||
const Q = { class: "shadow rounded-md", style: { width: "300px" } },
|
||||
X = ["src"],
|
||||
Y = { class: "p-5" },
|
||||
@@ -18,8 +18,8 @@ import {
|
||||
F as z,
|
||||
X as B,
|
||||
ab as I,
|
||||
} from "./frappe-ui.f2211ca2.js";
|
||||
import { c as i } from "./index.43e529db.js";
|
||||
} from "./frappe-ui.a747cf9c.js";
|
||||
import { c as i } from "./index.51e5b051.js";
|
||||
const L = i("ChevronRightIcon", [
|
||||
["path", { d: "m9 18 6-6-6-6", key: "mthhwq" }],
|
||||
]),
|
||||
@@ -22,12 +22,12 @@ import {
|
||||
$ as D,
|
||||
a0 as E,
|
||||
a1 as P,
|
||||
} from "./frappe-ui.f2211ca2.js";
|
||||
import { _ as U } from "./CourseCard.6a41330a.js";
|
||||
import { P as A } from "./plus.8f4bce9f.js";
|
||||
import "./UserAvatar.b64a03ac.js";
|
||||
import "./index.43e529db.js";
|
||||
import "./star.d3e8ecca.js";
|
||||
} from "./frappe-ui.a747cf9c.js";
|
||||
import { _ as U } from "./CourseCard.bf057db6.js";
|
||||
import { P as A } from "./plus.d245902e.js";
|
||||
import "./UserAvatar.3cd4adb4.js";
|
||||
import "./index.51e5b051.js";
|
||||
import "./star.d358f014.js";
|
||||
const F = { class: "h-screen" },
|
||||
R = { key: 0 },
|
||||
S = {
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
L as m,
|
||||
a2 as h,
|
||||
B as b,
|
||||
} from "./frappe-ui.f2211ca2.js";
|
||||
} from "./frappe-ui.a747cf9c.js";
|
||||
const v = {
|
||||
name: "FontColor",
|
||||
props: ["editor"],
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
X as l,
|
||||
A as u,
|
||||
E as p,
|
||||
} from "./frappe-ui.f2211ca2.js";
|
||||
} from "./frappe-ui.a747cf9c.js";
|
||||
const D = {
|
||||
name: "Home",
|
||||
data() {
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
F as v,
|
||||
X as u,
|
||||
K as w,
|
||||
} from "./frappe-ui.f2211ca2.js";
|
||||
} from "./frappe-ui.a747cf9c.js";
|
||||
const b = {
|
||||
name: "InsertImage",
|
||||
props: ["editor"],
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
am as _,
|
||||
X as v,
|
||||
K as w,
|
||||
} from "./frappe-ui.f2211ca2.js";
|
||||
} from "./frappe-ui.a747cf9c.js";
|
||||
const x = {
|
||||
name: "InsertLink",
|
||||
props: ["editor"],
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
y as U,
|
||||
F as p,
|
||||
K as F,
|
||||
} from "./frappe-ui.f2211ca2.js";
|
||||
} from "./frappe-ui.a747cf9c.js";
|
||||
const I = {
|
||||
name: "InsertImage",
|
||||
props: ["editor"],
|
||||
@@ -27,11 +27,11 @@ import {
|
||||
B as V0,
|
||||
a2 as G0,
|
||||
Z as Q0,
|
||||
} from "./frappe-ui.f2211ca2.js";
|
||||
import { C as W0, _ as J0 } from "./CourseOutline.df6c648a.js";
|
||||
import { _ as Y0 } from "./UserAvatar.b64a03ac.js";
|
||||
import { t as X0, c as K0 } from "./index.05189aed.js";
|
||||
import { c as Cu } from "./index.43e529db.js";
|
||||
} from "./frappe-ui.a747cf9c.js";
|
||||
import { C as W0, _ as J0 } from "./CourseOutline.2110618a.js";
|
||||
import { _ as Y0 } from "./UserAvatar.3cd4adb4.js";
|
||||
import { t as X0, c as K0 } from "./index.6f049c1a.js";
|
||||
import { c as Cu } from "./index.51e5b051.js";
|
||||
const ue = Cu("CheckCircleIcon", [
|
||||
["path", { d: "M22 11.08V12a10 10 0 1 1-5.93-9.14", key: "g774vq" }],
|
||||
["polyline", { points: "22 4 12 14.01 9 11.01", key: "6xbx8j" }],
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
D as l,
|
||||
a4 as u,
|
||||
F as n,
|
||||
} from "./frappe-ui.f2211ca2.js";
|
||||
} from "./frappe-ui.a747cf9c.js";
|
||||
const i = {
|
||||
__name: "UserAvatar",
|
||||
props: { user: { type: Object, default: null }, size: { type: String } },
|
||||
@@ -1,4 +1,4 @@
|
||||
import { c as e } from "./index.43e529db.js";
|
||||
import { c as e } from "./index.51e5b051.js";
|
||||
const c = e("CalendarIcon", [
|
||||
[
|
||||
"rect",
|
||||
File diff suppressed because it is too large
Load Diff
@@ -97,7 +97,7 @@ import {
|
||||
U as It,
|
||||
V as At,
|
||||
W as Tt,
|
||||
} from "./frappe-ui.f2211ca2.js";
|
||||
} from "./frappe-ui.a747cf9c.js";
|
||||
(function () {
|
||||
const n = document.createElement("link").relList;
|
||||
if (n && n.supports && n.supports("modulepreload")) return;
|
||||
@@ -438,10 +438,10 @@ const we = We("lms-users", () => ({
|
||||
name: "Home",
|
||||
component: () =>
|
||||
F(
|
||||
() => import("./Home.6f16d409.js"),
|
||||
() => import("./Home.28a136f6.js"),
|
||||
[
|
||||
"assets/Home.6f16d409.js",
|
||||
"assets/frappe-ui.f2211ca2.js",
|
||||
"assets/Home.28a136f6.js",
|
||||
"assets/frappe-ui.a747cf9c.js",
|
||||
"assets/frappe-ui.7692ed2d.css",
|
||||
]
|
||||
),
|
||||
@@ -451,16 +451,16 @@ const we = We("lms-users", () => ({
|
||||
name: "Courses",
|
||||
component: () =>
|
||||
F(
|
||||
() => import("./Courses.3f5a0719.js"),
|
||||
() => import("./Courses.52ce2794.js"),
|
||||
[
|
||||
"assets/Courses.3f5a0719.js",
|
||||
"assets/frappe-ui.f2211ca2.js",
|
||||
"assets/Courses.52ce2794.js",
|
||||
"assets/frappe-ui.a747cf9c.js",
|
||||
"assets/frappe-ui.7692ed2d.css",
|
||||
"assets/CourseCard.6a41330a.js",
|
||||
"assets/UserAvatar.b64a03ac.js",
|
||||
"assets/star.d3e8ecca.js",
|
||||
"assets/CourseCard.bf057db6.js",
|
||||
"assets/UserAvatar.3cd4adb4.js",
|
||||
"assets/star.d358f014.js",
|
||||
"assets/CourseCard.04c5bb55.css",
|
||||
"assets/plus.8f4bce9f.js",
|
||||
"assets/plus.d245902e.js",
|
||||
]
|
||||
),
|
||||
},
|
||||
@@ -469,16 +469,16 @@ const we = We("lms-users", () => ({
|
||||
name: "CourseDetail",
|
||||
component: () =>
|
||||
F(
|
||||
() => import("./CourseDetail.f6fd1d68.js"),
|
||||
() => import("./CourseDetail.e391d1e0.js"),
|
||||
[
|
||||
"assets/CourseDetail.f6fd1d68.js",
|
||||
"assets/frappe-ui.f2211ca2.js",
|
||||
"assets/CourseDetail.e391d1e0.js",
|
||||
"assets/frappe-ui.a747cf9c.js",
|
||||
"assets/frappe-ui.7692ed2d.css",
|
||||
"assets/index.05189aed.js",
|
||||
"assets/star.d3e8ecca.js",
|
||||
"assets/CourseOutline.df6c648a.js",
|
||||
"assets/index.6f049c1a.js",
|
||||
"assets/star.d358f014.js",
|
||||
"assets/CourseOutline.2110618a.js",
|
||||
"assets/CourseOutline.6dd858fb.css",
|
||||
"assets/UserAvatar.b64a03ac.js",
|
||||
"assets/UserAvatar.3cd4adb4.js",
|
||||
"assets/CourseDetail.6888eccf.css",
|
||||
]
|
||||
),
|
||||
@@ -489,15 +489,15 @@ const we = We("lms-users", () => ({
|
||||
name: "Lesson",
|
||||
component: () =>
|
||||
F(
|
||||
() => import("./Lesson.c80fc3b7.js"),
|
||||
() => import("./Lesson.19d410ae.js"),
|
||||
[
|
||||
"assets/Lesson.c80fc3b7.js",
|
||||
"assets/frappe-ui.f2211ca2.js",
|
||||
"assets/Lesson.19d410ae.js",
|
||||
"assets/frappe-ui.a747cf9c.js",
|
||||
"assets/frappe-ui.7692ed2d.css",
|
||||
"assets/CourseOutline.df6c648a.js",
|
||||
"assets/CourseOutline.2110618a.js",
|
||||
"assets/CourseOutline.6dd858fb.css",
|
||||
"assets/UserAvatar.b64a03ac.js",
|
||||
"assets/index.05189aed.js",
|
||||
"assets/UserAvatar.3cd4adb4.js",
|
||||
"assets/index.6f049c1a.js",
|
||||
"assets/Lesson.3532a62c.css",
|
||||
]
|
||||
),
|
||||
@@ -508,14 +508,14 @@ const we = We("lms-users", () => ({
|
||||
name: "Batches",
|
||||
component: () =>
|
||||
F(
|
||||
() => import("./Batches.f9864378.js"),
|
||||
() => import("./Batches.6064501b.js"),
|
||||
[
|
||||
"assets/Batches.f9864378.js",
|
||||
"assets/frappe-ui.f2211ca2.js",
|
||||
"assets/Batches.6064501b.js",
|
||||
"assets/frappe-ui.a747cf9c.js",
|
||||
"assets/frappe-ui.7692ed2d.css",
|
||||
"assets/index.05189aed.js",
|
||||
"assets/clock.4d13ba48.js",
|
||||
"assets/plus.8f4bce9f.js",
|
||||
"assets/index.6f049c1a.js",
|
||||
"assets/clock.b36d19aa.js",
|
||||
"assets/plus.d245902e.js",
|
||||
"assets/Batches.70c9cf07.css",
|
||||
]
|
||||
),
|
||||
@@ -525,16 +525,16 @@ const we = We("lms-users", () => ({
|
||||
name: "BatchDetail",
|
||||
component: () =>
|
||||
F(
|
||||
() => import("./BatchDetail.c5dd0840.js"),
|
||||
() => import("./BatchDetail.9bef2d15.js"),
|
||||
[
|
||||
"assets/BatchDetail.c5dd0840.js",
|
||||
"assets/frappe-ui.f2211ca2.js",
|
||||
"assets/BatchDetail.9bef2d15.js",
|
||||
"assets/frappe-ui.a747cf9c.js",
|
||||
"assets/frappe-ui.7692ed2d.css",
|
||||
"assets/index.05189aed.js",
|
||||
"assets/clock.4d13ba48.js",
|
||||
"assets/CourseCard.6a41330a.js",
|
||||
"assets/UserAvatar.b64a03ac.js",
|
||||
"assets/star.d3e8ecca.js",
|
||||
"assets/index.6f049c1a.js",
|
||||
"assets/clock.b36d19aa.js",
|
||||
"assets/CourseCard.bf057db6.js",
|
||||
"assets/UserAvatar.3cd4adb4.js",
|
||||
"assets/star.d358f014.js",
|
||||
"assets/CourseCard.04c5bb55.css",
|
||||
"assets/BatchDetail.f109aa14.css",
|
||||
]
|
||||
@@ -546,17 +546,17 @@ const we = We("lms-users", () => ({
|
||||
name: "Batch",
|
||||
component: () =>
|
||||
F(
|
||||
() => import("./Batch.6cc6d79c.js"),
|
||||
() => import("./Batch.3bb9da4e.js"),
|
||||
[
|
||||
"assets/Batch.6cc6d79c.js",
|
||||
"assets/frappe-ui.f2211ca2.js",
|
||||
"assets/Batch.3bb9da4e.js",
|
||||
"assets/frappe-ui.a747cf9c.js",
|
||||
"assets/frappe-ui.7692ed2d.css",
|
||||
"assets/index.05189aed.js",
|
||||
"assets/CourseCard.6a41330a.js",
|
||||
"assets/UserAvatar.b64a03ac.js",
|
||||
"assets/star.d3e8ecca.js",
|
||||
"assets/index.6f049c1a.js",
|
||||
"assets/CourseCard.bf057db6.js",
|
||||
"assets/UserAvatar.3cd4adb4.js",
|
||||
"assets/star.d358f014.js",
|
||||
"assets/CourseCard.04c5bb55.css",
|
||||
"assets/clock.4d13ba48.js",
|
||||
"assets/clock.b36d19aa.js",
|
||||
]
|
||||
),
|
||||
props: !0,
|
||||
@@ -16,7 +16,7 @@ var n = (t, e, r) =>
|
||||
if (o) for (var r of o(e)) c.call(e, r) && n(t, r, e[r]);
|
||||
return t;
|
||||
};
|
||||
import { ac as s, ad as f } from "./frappe-ui.f2211ca2.js";
|
||||
import { ac as s, ad as f } from "./frappe-ui.a747cf9c.js";
|
||||
function y(t) {
|
||||
s(a({ position: "bottom-right" }, t));
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
import { c as e } from "./index.43e529db.js";
|
||||
import { c as e } from "./index.51e5b051.js";
|
||||
const n = e("PlusIcon", [
|
||||
["line", { x1: "12", x2: "12", y1: "5", y2: "19", key: "pwfkuu" }],
|
||||
["line", { x1: "5", x2: "19", y1: "12", y2: "12", key: "13b5wn" }],
|
||||
@@ -1,4 +1,4 @@
|
||||
import { c as o } from "./index.43e529db.js";
|
||||
import { c as o } from "./index.51e5b051.js";
|
||||
const c = o("StarIcon", [
|
||||
[
|
||||
"polygon",
|
||||
@@ -5,10 +5,10 @@
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Frappe UI App</title>
|
||||
<script type="module" crossorigin src="/assets/index.43e529db.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/frappe-ui.f2211ca2.js">
|
||||
<script type="module" crossorigin src="/assets/index.51e5b051.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/frappe-ui.a747cf9c.js">
|
||||
<link rel="stylesheet" href="/assets/frappe-ui.7692ed2d.css">
|
||||
<link rel="stylesheet" href="/assets/index.64bc1bc1.css">
|
||||
<link rel="stylesheet" href="/assets/index.7337873e.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
Reference in New Issue
Block a user