refactor: improved performance and ui batch list
This commit is contained in:
@@ -1,256 +1,267 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="">
|
<header
|
||||||
<header
|
class="sticky flex items-center justify-between top-0 z-10 border-b bg-white px-3 py-2.5 sm:px-5"
|
||||||
class="sticky top-0 z-10 flex items-center justify-between border-b bg-white px-3 py-2.5 sm:px-5"
|
>
|
||||||
|
<Breadcrumbs :items="breadcrumbs" />
|
||||||
|
<router-link
|
||||||
|
v-if="user.data?.is_moderator"
|
||||||
|
:to="{
|
||||||
|
name: 'BatchForm',
|
||||||
|
params: { batchName: 'new' },
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<Breadcrumbs
|
<Button variant="solid">
|
||||||
class="h-7"
|
<template #prefix>
|
||||||
:items="[{ label: __('Batches'), route: { name: 'Batches' } }]"
|
<Plus class="h-4 w-4 stroke-1.5" />
|
||||||
/>
|
</template>
|
||||||
<div class="flex space-x-2">
|
{{ __('New') }}
|
||||||
<div class="w-44">
|
</Button>
|
||||||
|
</router-link>
|
||||||
|
</header>
|
||||||
|
<div class="p-5 pb-10">
|
||||||
|
<div class="flex items-center justify-between mb-5">
|
||||||
|
<div class="text-lg font-semibold">
|
||||||
|
{{ __('All Batches') }}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<TabButtons
|
||||||
|
v-if="user.data && user.data?.is_student"
|
||||||
|
:buttons="batchTabs"
|
||||||
|
v-model="currentTab"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-model="title"
|
||||||
|
:placeholder="__('Search by Title')"
|
||||||
|
type="text"
|
||||||
|
@input="updateBatches()"
|
||||||
|
/>
|
||||||
|
<div v-if="user.data && !user.data?.is_student" class="w-44">
|
||||||
<Select
|
<Select
|
||||||
v-if="categories.data?.length"
|
v-model="currentDuration"
|
||||||
v-model="currentCategory"
|
:options="batchType"
|
||||||
:options="categories.data"
|
:placeholder="__('Type')"
|
||||||
:placeholder="__('Category')"
|
@change="updateBatches()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<router-link
|
<div class="w-44">
|
||||||
v-if="user.data?.is_moderator"
|
<Select
|
||||||
:to="{
|
v-if="categories.length"
|
||||||
name: 'BatchForm',
|
v-model="currentCategory"
|
||||||
params: { batchName: 'new' },
|
:options="categories"
|
||||||
}"
|
:placeholder="__('Category')"
|
||||||
>
|
@change="updateBatches()"
|
||||||
<Button variant="solid">
|
/>
|
||||||
<template #prefix>
|
|
||||||
<Plus class="h-4 w-4 stroke-1.5" />
|
|
||||||
</template>
|
|
||||||
{{ __('New') }}
|
|
||||||
</Button>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div v-if="batches.data" class="pb-5">
|
|
||||||
<div
|
|
||||||
v-if="batches.data.length == 0 && batches.list.loading"
|
|
||||||
class="p-5 text-base text-gray-700"
|
|
||||||
>
|
|
||||||
{{ __('Loading Batches...') }}
|
|
||||||
</div>
|
|
||||||
<Tabs
|
|
||||||
v-if="hasBatches"
|
|
||||||
v-model="tabIndex"
|
|
||||||
:tabs="makeTabs"
|
|
||||||
tablistClass="overflow-x-visible flex-wrap !gap-3 md:flex-nowrap"
|
|
||||||
>
|
|
||||||
<template #tab="{ tab, selected }">
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
class="group -mb-px flex items-center gap-2 border-b border-transparent py-2.5 text-base text-gray-600 duration-300 ease-in-out hover:border-gray-400 hover:text-gray-900"
|
|
||||||
:class="{ 'text-gray-900': selected }"
|
|
||||||
>
|
|
||||||
<component v-if="tab.icon" :is="tab.icon" class="h-5" />
|
|
||||||
{{ __(tab.label) }}
|
|
||||||
<Badge
|
|
||||||
:class="
|
|
||||||
selected
|
|
||||||
? 'text-gray-800 border border-gray-800'
|
|
||||||
: 'border border-gray-500'
|
|
||||||
"
|
|
||||||
variant="subtle"
|
|
||||||
theme="gray"
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
{{ tab.count }}
|
|
||||||
</Badge>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #default="{ tab }">
|
|
||||||
<div
|
|
||||||
v-if="tab.batches && tab.batches.value.length"
|
|
||||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-5 m-5"
|
|
||||||
>
|
|
||||||
<router-link
|
|
||||||
v-for="batch in tab.batches.value"
|
|
||||||
:to="{ name: 'BatchDetail', params: { batchName: batch.name } }"
|
|
||||||
>
|
|
||||||
<BatchCard :batch="batch" />
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
<div v-else class="p-5 italic text-gray-500">
|
|
||||||
{{ __('No {0} batches').format(tab.label.toLowerCase()) }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Tabs>
|
|
||||||
<div
|
|
||||||
v-else-if="
|
|
||||||
!batches.loading &&
|
|
||||||
!hasBatches &&
|
|
||||||
(user.data?.is_instructor || user.data?.is_moderator)
|
|
||||||
"
|
|
||||||
class="grid grid-cols-3 p-5"
|
|
||||||
>
|
|
||||||
<router-link
|
|
||||||
:to="{
|
|
||||||
name: 'BatchForm',
|
|
||||||
params: {
|
|
||||||
batchName: 'new',
|
|
||||||
},
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div class="bg-gray-50 py-32 px-5 rounded-md">
|
|
||||||
<div class="flex flex-col items-center text-center space-y-2">
|
|
||||||
<Plus
|
|
||||||
class="size-10 stroke-1 text-gray-800 p-1 rounded-full border bg-white"
|
|
||||||
/>
|
|
||||||
<div class="font-medium">
|
|
||||||
{{ __('Create a Batch') }}
|
|
||||||
</div>
|
|
||||||
<span class="text-gray-700 text-sm leading-4">
|
|
||||||
{{ __('You can link courses and assessments to it.') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else-if="!batches.loading && !hasBatches"
|
|
||||||
class="text-center p-5 text-gray-600 mt-52 w-3/4 md:w-1/2 mx-auto space-y-2"
|
|
||||||
>
|
|
||||||
<BookOpen class="size-10 mx-auto stroke-1 text-gray-500" />
|
|
||||||
<div class="text-xl font-medium">
|
|
||||||
{{ __('No batches found') }}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{{
|
|
||||||
__(
|
|
||||||
'There are no batches available at the moment. Keep an eye out, fresh learning experiences are on the way soon!'
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="batches.data?.length" class="grid grid-cols-4 gap-5">
|
||||||
|
<router-link
|
||||||
|
v-for="batch in batches.data"
|
||||||
|
:to="{ name: 'BatchDetail', params: { batchName: batch.name } }"
|
||||||
|
>
|
||||||
|
<BatchCard :batch="batch" />
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="flex flex-col items-center justify-center text-sm text-gray-600 italic mt-48"
|
||||||
|
>
|
||||||
|
<BookOpen class="size-10 mx-auto stroke-1.5 text-gray-500" />
|
||||||
|
<div class="text-xl font-medium mb-2">
|
||||||
|
{{ __('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>
|
||||||
|
<div
|
||||||
|
v-if="!batches.loading && batches.hasNextPage"
|
||||||
|
class="flex justify-center mt-5"
|
||||||
|
>
|
||||||
|
<Button @click="batches.next()">
|
||||||
|
{{ __('Load More') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
createResource,
|
|
||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
Button,
|
Button,
|
||||||
Tabs,
|
createListResource,
|
||||||
Badge,
|
FormControl,
|
||||||
Select,
|
Select,
|
||||||
|
TabButtons,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
|
import { computed, inject, onMounted, ref, watch } from 'vue'
|
||||||
import { BookOpen, Plus } from 'lucide-vue-next'
|
import { BookOpen, Plus } from 'lucide-vue-next'
|
||||||
import BatchCard from '@/components/BatchCard.vue'
|
import BatchCard from '@/components/BatchCard.vue'
|
||||||
import { inject, ref, computed, onMounted, watch } from 'vue'
|
|
||||||
import { updateDocumentTitle } from '@/utils'
|
|
||||||
|
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
|
const dayjs = inject('$dayjs')
|
||||||
|
const start = ref(0)
|
||||||
|
const pageLength = ref(20)
|
||||||
|
const categories = ref([])
|
||||||
const currentCategory = ref(null)
|
const currentCategory = ref(null)
|
||||||
const hasBatches = ref(false)
|
const title = ref('')
|
||||||
|
const filters = ref({})
|
||||||
|
const currentDuration = ref(null)
|
||||||
|
const currentTab = ref('All')
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
let queries = new URLSearchParams(location.search)
|
setFiltersFromQuery()
|
||||||
if (queries.has('category')) {
|
updateBatches()
|
||||||
currentCategory.value = queries.get('category')
|
categories.value = [
|
||||||
}
|
{
|
||||||
})
|
|
||||||
|
|
||||||
const batches = createResource({
|
|
||||||
doctype: 'LMS Batch',
|
|
||||||
url: 'lms.lms.utils.get_batches',
|
|
||||||
cache: ['batches', user.data?.email || ''],
|
|
||||||
auto: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const categories = createResource({
|
|
||||||
url: 'lms.lms.api.get_categories',
|
|
||||||
makeParams() {
|
|
||||||
return {
|
|
||||||
doctype: 'LMS Batch',
|
|
||||||
filters: {
|
|
||||||
published: 1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
cache: ['batchCategories'],
|
|
||||||
auto: true,
|
|
||||||
transform(data) {
|
|
||||||
data.unshift({
|
|
||||||
label: '',
|
label: '',
|
||||||
value: null,
|
value: null,
|
||||||
})
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const setFiltersFromQuery = () => {
|
||||||
|
let queries = new URLSearchParams(location.search)
|
||||||
|
title.value = queries.get('title') || ''
|
||||||
|
currentCategory.value = queries.get('category') || null
|
||||||
|
currentDuration.value = queries.get('type') || null
|
||||||
|
}
|
||||||
|
|
||||||
|
const batches = createListResource({
|
||||||
|
doctype: 'LMS Batch',
|
||||||
|
url: 'lms.lms.utils.get_batches',
|
||||||
|
cache: ['batches', user.data?.name],
|
||||||
|
pageLength: pageLength.value,
|
||||||
|
start: start.value,
|
||||||
|
onSuccess(data) {
|
||||||
|
let allCategories = data.map((batch) => batch.category)
|
||||||
|
allCategories = allCategories.filter(
|
||||||
|
(category, index) => allCategories.indexOf(category) === index && category
|
||||||
|
)
|
||||||
|
if (categories.value.length <= allCategories.length) {
|
||||||
|
updateCategories(data)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const tabIndex = ref(0)
|
const updateBatches = () => {
|
||||||
let tabs
|
updateFilters()
|
||||||
|
batches.update({
|
||||||
|
filters: filters.value,
|
||||||
|
})
|
||||||
|
batches.reload()
|
||||||
|
}
|
||||||
|
|
||||||
const makeTabs = computed(() => {
|
const updateFilters = () => {
|
||||||
tabs = []
|
if (currentCategory.value) {
|
||||||
addToTabs('Upcoming')
|
filters.value['category'] = currentCategory.value
|
||||||
|
} else {
|
||||||
|
delete filters.value['category']
|
||||||
|
}
|
||||||
|
|
||||||
|
if (title.value) {
|
||||||
|
filters.value['title'] = ['like', `%${title.value}%`]
|
||||||
|
} else {
|
||||||
|
delete filters.value['title']
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentDuration.value) {
|
||||||
|
delete filters.value['start_date']
|
||||||
|
delete filters.value['published']
|
||||||
|
|
||||||
|
if (currentDuration.value == 'Upcoming') {
|
||||||
|
filters.value['start_date'] = ['>=', dayjs().format('YYYY-MM-DD')]
|
||||||
|
} else if (currentDuration.value == 'Archived') {
|
||||||
|
filters.value['start_date'] = ['<', dayjs().format('YYYY-MM-DD')]
|
||||||
|
} else if (currentDuration.value == 'Unpublished') {
|
||||||
|
filters.value['published'] = 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delete filters.value['start_date']
|
||||||
|
delete filters.value['published']
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTab.value == 'Enrolled' && user.data?.is_student) {
|
||||||
|
filters.value['enrolled'] = 1
|
||||||
|
} else {
|
||||||
|
delete filters.value['enrolled']
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.data || user.data?.is_student) {
|
||||||
|
filters.value['start_date'] = ['>=', dayjs().format('YYYY-MM-DD')]
|
||||||
|
filters.value['published'] = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
setQueryParams()
|
||||||
|
}
|
||||||
|
|
||||||
|
const setQueryParams = () => {
|
||||||
|
let queries = new URLSearchParams(location.search)
|
||||||
|
let filterKeys = {
|
||||||
|
title: title.value,
|
||||||
|
category: currentCategory.value,
|
||||||
|
type: currentDuration.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(filterKeys).forEach((key) => {
|
||||||
|
if (filterKeys[key]) {
|
||||||
|
queries.set(key, filterKeys[key])
|
||||||
|
} else {
|
||||||
|
queries.delete(key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
history.replaceState({}, '', `${location.pathname}?${queries.toString()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateCategories = (data) => {
|
||||||
|
data.forEach((batch) => {
|
||||||
|
if (
|
||||||
|
batch.category &&
|
||||||
|
!categories.value.find((category) => category.value === batch.category)
|
||||||
|
)
|
||||||
|
categories.value.push({
|
||||||
|
label: batch.category,
|
||||||
|
value: batch.category,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(currentTab, () => {
|
||||||
|
updateBatches()
|
||||||
|
})
|
||||||
|
|
||||||
|
const batchType = computed(() => {
|
||||||
|
let types = [
|
||||||
|
{ label: __(''), value: null },
|
||||||
|
{ label: __('Upcoming'), value: 'Upcoming' },
|
||||||
|
{ label: __('Archived'), value: 'Archived' },
|
||||||
|
]
|
||||||
if (user.data?.is_moderator) {
|
if (user.data?.is_moderator) {
|
||||||
addToTabs('Archived')
|
types.push({ label: __('Unpublished'), value: 'Unpublished' })
|
||||||
addToTabs('Private')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.data) {
|
|
||||||
addToTabs('Enrolled')
|
|
||||||
}
|
}
|
||||||
|
return types
|
||||||
|
})
|
||||||
|
|
||||||
|
const batchTabs = computed(() => {
|
||||||
|
let tabs = [
|
||||||
|
{
|
||||||
|
label: __('All'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Enrolled'),
|
||||||
|
},
|
||||||
|
]
|
||||||
return tabs
|
return tabs
|
||||||
})
|
})
|
||||||
|
|
||||||
const getBatches = (type) => {
|
const breadcrumbs = computed(() => [
|
||||||
if (currentCategory.value && currentCategory.value != '') {
|
{
|
||||||
return batches.data[type].filter(
|
label: __('Batches'),
|
||||||
(batch) => batch.category == currentCategory.value
|
route: { name: 'Batches' },
|
||||||
)
|
},
|
||||||
}
|
])
|
||||||
return batches.data[type]
|
|
||||||
}
|
|
||||||
|
|
||||||
const addToTabs = (label) => {
|
|
||||||
let batches = getBatches(label.toLowerCase().split(' ').join('_'))
|
|
||||||
tabs.push({
|
|
||||||
label,
|
|
||||||
batches: computed(() => batches),
|
|
||||||
count: computed(() => batches.length),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(batches, () => {
|
|
||||||
Object.keys(batches.data).forEach((key) => {
|
|
||||||
if (batches.data[key].length) {
|
|
||||||
hasBatches.value = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => currentCategory.value,
|
|
||||||
() => {
|
|
||||||
let queries = new URLSearchParams(location.search)
|
|
||||||
if (currentCategory.value) {
|
|
||||||
queries.set('category', currentCategory.value)
|
|
||||||
} else {
|
|
||||||
queries.delete('category')
|
|
||||||
}
|
|
||||||
history.pushState(null, '', `${location.pathname}?${queries.toString()}`)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const pageMeta = computed(() => {
|
|
||||||
return {
|
|
||||||
title: 'Batches',
|
|
||||||
description: 'All batches divided by categories',
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
updateDocumentTitle(pageMeta)
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1211,7 +1211,7 @@ def get_neighbour_lesson(course, chapter, lesson):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def get_batches():
|
def get_batches1():
|
||||||
batches = []
|
batches = []
|
||||||
filters = {}
|
filters = {}
|
||||||
if frappe.session.user == "Guest":
|
if frappe.session.user == "Guest":
|
||||||
@@ -1902,3 +1902,58 @@ def enroll_in_program_course(program, course):
|
|||||||
)
|
)
|
||||||
enrollment.save()
|
enrollment.save()
|
||||||
return enrollment
|
return enrollment
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def get_batches(filters=None, start=0, page_length=20):
|
||||||
|
if not filters:
|
||||||
|
filters = {}
|
||||||
|
|
||||||
|
if filters.get("enrolled"):
|
||||||
|
enrolled_batches = frappe.get_all(
|
||||||
|
"Batch Student", {"student": frappe.session.user}, pluck="parent"
|
||||||
|
)
|
||||||
|
filters.update({"name": ["in", enrolled_batches]})
|
||||||
|
del filters["enrolled"]
|
||||||
|
del filters["published"]
|
||||||
|
del filters["start_date"]
|
||||||
|
|
||||||
|
batches = frappe.get_all(
|
||||||
|
"LMS Batch",
|
||||||
|
filters=filters,
|
||||||
|
fields=[
|
||||||
|
"name",
|
||||||
|
"title",
|
||||||
|
"description",
|
||||||
|
"seat_count",
|
||||||
|
"paid_batch",
|
||||||
|
"amount",
|
||||||
|
"amount_usd",
|
||||||
|
"currency",
|
||||||
|
"start_date",
|
||||||
|
"end_date",
|
||||||
|
"start_time",
|
||||||
|
"end_time",
|
||||||
|
"timezone",
|
||||||
|
"published",
|
||||||
|
"category",
|
||||||
|
],
|
||||||
|
order_by="start_date desc",
|
||||||
|
start=start,
|
||||||
|
page_length=page_length,
|
||||||
|
)
|
||||||
|
|
||||||
|
for batch in batches:
|
||||||
|
batch.instructors = get_instructors(batch.name)
|
||||||
|
students_count = frappe.db.count("Batch Student", {"parent": batch.name})
|
||||||
|
|
||||||
|
if batch.seat_count:
|
||||||
|
batch.seats_left = batch.seat_count - students_count
|
||||||
|
|
||||||
|
if batch.paid_batch and batch.start_date >= getdate():
|
||||||
|
batch.amount, batch.currency = check_multicurrency(
|
||||||
|
batch.amount, batch.currency, None, batch.amount_usd
|
||||||
|
)
|
||||||
|
batch.price = fmt_money(batch.amount, 0, batch.currency)
|
||||||
|
|
||||||
|
return batches
|
||||||
|
|||||||
Reference in New Issue
Block a user