refactor: improved performance and ui batch list

This commit is contained in:
Jannat Patel
2025-01-14 17:41:46 +05:30
parent 9a395cbda0
commit b42c635cdb
2 changed files with 286 additions and 220 deletions

View File

@@ -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>

View File

@@ -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