Merge pull request #1246 from pateljannat/batch-refactor

refactor: improved performance and ui batch list
This commit is contained in:
Jannat Patel
2025-01-15 10:57:11 +05:30
committed by GitHub
2 changed files with 286 additions and 274 deletions

View File

@@ -1,21 +1,8 @@
<template> <template>
<div class="">
<header <header
class="sticky top-0 z-10 flex items-center justify-between border-b bg-white px-3 py-2.5 sm:px-5" class="sticky flex items-center justify-between top-0 z-10 border-b bg-white px-3 py-2.5 sm:px-5"
> >
<Breadcrumbs <Breadcrumbs :items="breadcrumbs" />
class="h-7"
:items="[{ label: __('Batches'), route: { name: 'Batches' } }]"
/>
<div class="flex space-x-2">
<div class="w-44">
<Select
v-if="categories.data?.length"
v-model="currentCategory"
:options="categories.data"
:placeholder="__('Category')"
/>
</div>
<router-link <router-link
v-if="user.data?.is_moderator" v-if="user.data?.is_moderator"
:to="{ :to="{
@@ -30,227 +17,251 @@
{{ __('New') }} {{ __('New') }}
</Button> </Button>
</router-link> </router-link>
</div>
</header> </header>
<div v-if="batches.data" class="pb-5"> <div class="p-5 pb-10">
<div <div class="flex items-center justify-between mb-5">
v-if="batches.data.length == 0 && batches.list.loading" <div class="text-lg font-semibold">
class="p-5 text-base text-gray-700" {{ __('All Batches') }}
>
{{ __('Loading Batches...') }}
</div> </div>
<Tabs <div class="flex items-center space-x-2">
v-if="hasBatches" <TabButtons
v-model="tabIndex" v-if="user.data && user.data?.is_student"
:tabs="makeTabs" :buttons="batchTabs"
tablistClass="overflow-x-visible flex-wrap !gap-3 md:flex-nowrap" v-model="currentTab"
> />
<template #tab="{ tab, selected }"> <FormControl
<div> v-model="title"
<button :placeholder="__('Search by Title')"
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" type="text"
:class="{ 'text-gray-900': selected }" @input="updateBatches()"
> />
<component v-if="tab.icon" :is="tab.icon" class="h-5" /> <div v-if="user.data && !user.data?.is_student" class="w-44">
{{ __(tab.label) }} <Select
<Badge v-model="currentDuration"
:class=" :options="batchType"
selected :placeholder="__('Type')"
? 'text-gray-800 border border-gray-800' @change="updateBatches()"
: 'border border-gray-500' />
"
variant="subtle"
theme="gray"
size="sm"
>
{{ tab.count }}
</Badge>
</button>
</div> </div>
</template> <div class="w-44">
<template #default="{ tab }"> <Select
<div v-if="categories.length"
v-if="tab.batches && tab.batches.value.length" v-model="currentCategory"
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-5 m-5" :options="categories"
> :placeholder="__('Category')"
@change="updateBatches()"
/>
</div>
</div>
</div>
<div v-if="batches.data?.length" class="grid grid-cols-4 gap-5">
<router-link <router-link
v-for="batch in tab.batches.value" v-for="batch in batches.data"
:to="{ name: 'BatchDetail', params: { batchName: batch.name } }" :to="{ name: 'BatchDetail', params: { batchName: batch.name } }"
> >
<BatchCard :batch="batch" /> <BatchCard :batch="batch" />
</router-link> </router-link>
</div> </div>
<div v-else class="p-5 italic text-gray-500">
{{ __('No {0} batches').format(tab.label.toLowerCase()) }}
</div>
</template>
</Tabs>
<div <div
v-else-if=" v-else
!batches.loading && class="flex flex-col items-center justify-center text-sm text-gray-600 italic mt-48"
!hasBatches &&
(user.data?.is_instructor || user.data?.is_moderator)
"
class="grid grid-cols-3 p-5"
> >
<router-link <BookOpen class="size-10 mx-auto stroke-1.5 text-gray-500" />
:to="{ <div class="text-xl font-medium mb-2">
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') }} {{ __('No batches found') }}
</div> </div>
<div> <div class="leading-5 w-2/5 text-center">
{{ {{
__( __(
'There are no batches available at the moment. Keep an eye out, fresh learning experiences are on the way soon!' 'There are no batches matching the criteria. Keep an eye out, fresh learning experiences are on the way soon!'
) )
}} }}
</div> </div>
</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> </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

@@ -935,7 +935,7 @@ def check_multicurrency(amount, currency, country=None, amount_usd=None):
# Conversion logic starts here. Exchange rate is fetched and amount is converted. # Conversion logic starts here. Exchange rate is fetched and amount is converted.
exchange_rate = get_current_exchange_rate(currency, "USD") exchange_rate = get_current_exchange_rate(currency, "USD")
amount = amount * exchange_rate amount = flt(amount * exchange_rate, 2)
currency = "USD" currency = "USD"
# Check if the amount should be rounded and then apply rounding # Check if the amount should be rounded and then apply rounding
@@ -1210,60 +1210,6 @@ def get_neighbour_lesson(course, chapter, lesson):
} }
@frappe.whitelist(allow_guest=True)
def get_batches():
batches = []
filters = {}
if frappe.session.user == "Guest":
filters.update({"start_date": [">=", getdate()], "published": 1})
batch_list = frappe.get_all("LMS Batch", filters)
for batch in batch_list:
batches.append(get_batch_card_details(batch.name))
batches = categorize_batches(batches)
return batches
def get_batch_card_details(batchname):
batch = frappe.db.get_value(
"LMS Batch",
batchname,
[
"name",
"title",
"description",
"seat_count",
"paid_batch",
"amount",
"amount_usd",
"currency",
"start_date",
"end_date",
"start_time",
"end_time",
"timezone",
"published",
"category",
],
as_dict=True,
)
batch.instructors = get_instructors(batchname)
students_count = frappe.db.count("Batch Student", {"parent": batchname})
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 batch
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
def get_batch_details(batch): def get_batch_details(batch):
batch_details = frappe.db.get_value( batch_details = frappe.db.get_value(
@@ -1902,3 +1848,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