fix: redirect from batch/details to batch

This commit is contained in:
Jannat Patel
2026-02-18 15:36:55 +05:30
parent 24bfe69985
commit 8ab6776fa9
12 changed files with 17 additions and 624 deletions

View File

@@ -7,7 +7,7 @@
>
<template #body>
<div class="p-5 min-h-[300px]">
<div class="text-lg font-semibold mb-4">
<div class="text-lg text-ink-gray-9 font-semibold mb-4">
{{ __('Training Feedback') }}
</div>
<ListView

View File

@@ -1,383 +0,0 @@
<template>
<div v-if="isAdmin || isStudent" class="">
<header
class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
>
<Breadcrumbs class="h-7" :items="breadcrumbs" />
<div class="flex items-center space-x-2">
<Button
v-if="isAdmin && batch.data?.certification"
@click="openCertificateDialog = true"
>
{{ __('Generate Certificates') }}
</Button>
<Button v-if="canMakeAnnouncement()" @click="openAnnouncementModal()">
<span>
{{ __('Make an Announcement') }}
</span>
<template #suffix>
<SendIcon class="h-4 stroke-1.5" />
</template>
</Button>
</div>
</header>
<div
v-if="batch.data"
class="grid grid-cols-1 md:grid-cols-[75%,25%] h-[calc(100vh-3.2rem)]"
>
<div class="border-r">
<Tabs
v-model="tabIndex"
as="div"
:tabs="tabs"
tablistClass="overflow-y-hidden bg-surface-white"
>
<template #tab="{ tab, selected }" class="overflow-x-hidden">
<div>
<button
class="group -mb-px flex items-center gap-1 border-b border-transparent py-2.5 text-base text-ink-gray-5 duration-300 ease-in-out hover:border-outline-gray-3 hover:text-ink-gray-9"
:class="{ 'text-ink-gray-9': selected }"
>
<component
v-if="tab.icon"
:is="tab.icon"
class="h-4 stroke-1.5"
/>
{{ __(tab.label) }}
<Badge
v-if="tab.count"
:class="{
'text-ink-gray-9 border border-gray-900': selected,
}"
variant="subtle"
theme="gray"
size="sm"
>
{{ tab.count }}
</Badge>
</button>
</div>
</template>
<template #tab-panel="{ tab }">
<div class="pt-5 px-5 pb-10">
<div v-if="tab.label == 'Courses'">
<BatchCourses :batch="batch.data.name" />
</div>
<div v-else-if="tab.label == 'Dashboard' && isStudent">
<BatchDashboard :batch="batch" :isStudent="isStudent" />
</div>
<div v-else-if="tab.label == 'Dashboard'">
<AdminBatchDashboard :batch="batch" />
</div>
<div v-else-if="tab.label == 'Students'">
<BatchStudents :batch="batch" />
</div>
<div v-else-if="tab.label == 'Classes'">
<LiveClass
:batch="batch.data.name"
:zoomAccount="batch.data.zoom_account"
/>
</div>
<div v-else-if="tab.label == 'Assessments'">
<Assessments :batch="batch.data.name" />
</div>
<div v-else-if="tab.label == 'Announcements'">
<Announcements :batch="batch.data.name" />
</div>
<div v-else-if="tab.label == 'Discussions'">
<Discussions
doctype="LMS Batch"
:docname="batch.data.name"
:title="__('Discussions')"
:key="batch.data.name"
:singleThread="true"
:scrollToBottom="false"
/>
</div>
</div>
</template>
</Tabs>
</div>
<div class="p-5 border-t md:border-t-0">
<div class="mb-10">
<div class="text-ink-gray-7 font-semibold mb-2">
{{ __('About this batch') }}
</div>
<div
v-html="batch.data.description"
class="leading-5 mb-4 text-ink-gray-7"
></div>
<div class="flex items-center avatar-group overlap mb-5">
<div
class="h-6 mr-1"
:class="{
'avatar-group overlap': batch.data.instructors.length > 1,
}"
>
<UserAvatar
v-for="instructor in batch.data.instructors"
:user="instructor"
/>
</div>
<CourseInstructors :instructors="batch.data.instructors" />
</div>
<DateRange
:startDate="batch.data.start_date"
:endDate="batch.data.end_date"
class="mb-3"
/>
<div class="flex items-center mb-3 text-ink-gray-7">
<Clock class="h-4 w-4 stroke-1.5 mr-2" />
<span>
{{ formatTime(batch.data.start_time) }} -
{{ formatTime(batch.data.end_time) }}
</span>
</div>
<div
v-if="batch.data.timezone"
class="flex items-center mb-3 text-ink-gray-7"
>
<Globe class="h-4 w-4 stroke-1.5 mr-2" />
<span>
{{ batch.data.timezone }}
</span>
</div>
</div>
<div v-if="dayjs().isSameOrAfter(dayjs(batch.data.start_date))">
<div class="text-ink-gray-7 font-semibold mb-2">
{{ __('Feedback') }}
</div>
<BatchFeedback :batch="batch.data?.name" />
</div>
</div>
<AnnouncementModal
v-model="showAnnouncementModal"
:batch="batch.data.name"
:students="batch.data.students"
/>
</div>
</div>
<div v-else-if="!user.data?.name" class="">
<div class="text-base border rounded-md w-1/3 mx-auto my-32">
<div class="border-b px-5 py-3 font-medium">
<span
class="inline-flex items-center before:bg-surface-red-5 before:w-2 before:h-2 before:rounded-md before:mr-2"
></span>
{{ __('Not Permitted') }}
</div>
<div class="px-5 py-3">
<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: {
batchName: batch.data?.name,
},
}"
>
<Button variant="solid" class="w-full">
{{ __('Upcoming Batches') }}
</Button>
</router-link>
<Button
v-else
variant="solid"
class="w-full"
@click="redirectToLogin()"
>
{{ __('Login') }}
</Button>
</div>
</div>
</div>
<BulkCertificates
v-if="batch.data"
v-model="openCertificateDialog"
:batch="batch.data"
/>
</template>
<script setup>
import { computed, inject, ref, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import {
Breadcrumbs,
Button,
createResource,
Tabs,
Badge,
usePageMeta,
} from 'frappe-ui'
import {
Clock,
LayoutDashboard,
BookOpen,
Laptop,
BookOpenCheck,
Mail,
SendIcon,
MessageCircle,
Globe,
ClipboardPen,
} from 'lucide-vue-next'
import { formatTime } from '@/utils'
import { sessionStore } from '@/stores/session'
import CourseInstructors from '@/components/CourseInstructors.vue'
import UserAvatar from '@/components/UserAvatar.vue'
import dayjs from 'dayjs/esm'
import { getLmsRoute } from '@/utils/basePath'
const user = inject('$user')
const showAnnouncementModal = ref(false)
const openCertificateDialog = ref(false)
const route = useRoute()
const router = useRouter()
const { brand } = sessionStore()
const tabIndex = ref(0)
const readOnlyMode = window.read_only_mode
const tabs = computed(() => {
let batchTabs = []
batchTabs.push({
label: 'Dashboard',
icon: LayoutDashboard,
})
if (isAdmin.value) {
batchTabs.push({
label: 'Students',
icon: ClipboardPen,
})
}
batchTabs.push({
label: 'Courses',
icon: BookOpen,
})
batchTabs.push({
label: 'Classes',
icon: Laptop,
})
if (isAdmin.value) {
batchTabs.push({
label: 'Assessments',
icon: BookOpenCheck,
})
}
batchTabs.push({
label: 'Announcements',
icon: Mail,
})
batchTabs.push({
label: 'Discussions',
icon: MessageCircle,
})
return batchTabs
})
const props = defineProps({
batchName: {
type: String,
required: true,
},
})
onMounted(() => {
const hash = route.hash
if (hash) {
tabs.value.forEach((tab, index) => {
if (tab.label?.toLowerCase() === hash.replace('#', '')) {
tabIndex.value = index
}
})
}
})
const batch = createResource({
url: 'lms.lms.utils.get_batch_details',
cache: ['batch', props.batchName],
params: {
batch: props.batchName,
},
auto: true,
})
const breadcrumbs = computed(() => {
let crumbs = [{ label: __('Batches'), route: { name: 'Batches' } }]
if (!isStudent.value) {
crumbs.push({
label: __('Details'),
route: {
name: 'BatchDetail',
params: {
batchName: batch.data?.name,
},
},
})
}
crumbs.push({
label: batch?.data?.title,
route: { name: 'Batch', params: { batchName: props.batchName } },
})
return crumbs
})
const isStudent = computed(() => {
return (
user?.data &&
batch.data?.students?.length &&
batch.data?.students.includes(user.data.name)
)
})
const redirectToLogin = () => {
window.location.href = `/login?redirect-to=${getLmsRoute(
`batches/${props.batchName}`
)}`
}
const openAnnouncementModal = () => {
showAnnouncementModal.value = true
}
watch(tabIndex, () => {
const tab = tabs.value[tabIndex.value]
if (tab.label != route.hash.replace('#', '')) {
router.push({ ...route, hash: `#${tab.label.toLowerCase()}` })
}
})
const canMakeAnnouncement = () => {
if (readOnlyMode) return false
if (!batch.data?.students?.length) return false
return user.data?.is_moderator || user.data?.is_evaluator
}
const isAdmin = computed(() => {
return user.data?.is_moderator || user.data?.is_evaluator
})
usePageMeta(() => {
return {
title: batch?.data?.title,
icon: brand.favicon,
}
})
</script>

View File

@@ -137,7 +137,7 @@ const enroll = createResource({
const enrollInBatch = () => {
if (!user.data) {
window.location.href = `/login?redirect-to=/batches/details/${props.batch.data.name}`
window.location.href = `/login?redirect-to=/batches/${props.batch.data.name}`
}
enroll.submit(
{},

View File

@@ -36,7 +36,7 @@
:columns="assessmentColumns"
:rows="studentDetails.data.assessments"
row-key="title"
class="border rounded-lg"
class="border border-outline-gray-modals rounded-lg"
:options="{
selectable: false,
showTooltip: false,
@@ -78,7 +78,7 @@
:columns="courseColumns"
:rows="studentDetails.data.courses"
row-key="title"
class="border rounded-lg"
class="border border-outline-gray-modals rounded-lg"
:options="{
selectable: false,
showTooltip: false,

View File

@@ -1,226 +0,0 @@
<template>
<div>
<div class="flex items-center justify-between mb-4">
<div class="text-ink-gray-9 font-medium">
{{ studentCount.data ?? 0 }} {{ __('Students') }}
</div>
<Button v-if="!readOnlyMode" @click="openStudentModal()">
<template #prefix>
<Plus class="h-4 w-4" />
</template>
{{ __('Add') }}
</Button>
</div>
<div v-if="students.data?.length">
<ListView
class="max-h-[75vh]"
:columns="studentColumns"
:rows="students.data"
row-key="name"
:options="{
showTooltip: false,
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
>
<ListHeaderItem
:item="item"
v-for="item in studentColumns"
:title="item.label"
>
<template #prefix="{ item }">
<FeatherIcon
v-if="item.icon"
:name="item.icon"
class="h-4 w-4 stroke-1.5"
/>
</template>
</ListHeaderItem>
</ListHeader>
<ListRows>
<ListRow
:row="row"
v-for="row in students.data"
class="group cursor-pointer hover:bg-surface-gray-2 rounded"
@click="openStudentProgressModal(row)"
>
<template #default="{ column, item }">
<ListRowItem
:item="row[column.key]"
:align="column.align"
class="text-sm"
>
<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
v-if="column.key == 'progress'"
class="flex items-center space-x-4 w-full"
>
<ProgressBar :progress="row[column.key]" size="sm" />
<div class="text-xs">{{ row[column.key] }}%</div>
</div>
<div v-else>
{{ row[column.key] }}
</div>
</ListRowItem>
</template>
</ListRow>
</ListRows>
<ListSelectBanner>
<template #actions="{ unselectAll, selections }">
<div class="flex gap-2">
<Button
variant="ghost"
@click="removeStudents(selections, unselectAll)"
>
<Trash2 class="h-4 w-4 stroke-1.5" />
</Button>
</div>
</template>
</ListSelectBanner>
<div class="mt-4 flex justify-center" v-if="students.hasNextPage">
<Button @click="students.next()">
{{ __('Load More') }}
</Button>
</div>
</ListView>
</div>
<div v-else-if="!students.loading" class="text-sm italic text-ink-gray-5">
{{ __('There are no students in this batch.') }}
</div>
</div>
<StudentModal
:batch="props.batch.data.name"
v-model="showStudentModal"
v-model:reloadStudents="students"
v-model:batchModal="props.batch"
/>
<BatchStudentProgress
:student="selectedStudent"
v-model="showStudentProgressModal"
/>
</template>
<script setup>
import {
Avatar,
Button,
createListResource,
createResource,
FeatherIcon,
ListHeader,
ListHeaderItem,
ListSelectBanner,
ListRow,
ListRows,
ListView,
ListRowItem,
toast,
} from 'frappe-ui'
import { Plus, Trash2 } from 'lucide-vue-next'
import { ref } from 'vue'
import StudentModal from '@/components/Modals/StudentModal.vue'
import ProgressBar from '@/components/ProgressBar.vue'
import BatchStudentProgress from '@/components/Modals/BatchStudentProgress.vue'
const showStudentModal = ref(false)
const showStudentProgressModal = ref(false)
const selectedStudent = ref(null)
const readOnlyMode = window.read_only_mode
const props = defineProps({
batch: {
type: Object,
default: null,
},
})
const studentCount = createResource({
url: 'frappe.client.get_count',
cache: ['batch_student_count', props.batch?.data?.name],
params: {
doctype: 'LMS Batch Enrollment',
filters: { batch: props.batch?.data?.name },
},
auto: true,
})
const students = createListResource({
doctype: 'LMS Batch Enrollment',
url: 'lms.lms.utils.get_batch_students',
cache: ['batch_students', props.batch?.data?.name],
pageLength: 50,
filters: {
batch: props.batch?.data?.name,
},
auto: true,
})
const studentColumns = [
{
label: 'Full Name',
key: 'full_name',
width: '25rem',
icon: 'user',
},
{
label: 'Progress',
key: 'progress',
width: '15rem',
icon: 'activity',
},
{
label: 'Last Active',
key: 'last_active',
width: '10rem',
align: 'center',
icon: 'clock',
},
]
const openStudentModal = () => {
showStudentModal.value = true
}
const openStudentProgressModal = (row) => {
showStudentProgressModal.value = true
selectedStudent.value = row
}
const deleteStudents = createResource({
url: 'lms.lms.api.delete_documents',
makeParams(values) {
return {
doctype: 'LMS Batch Enrollment',
documents: values.students,
}
},
})
const removeStudents = (selections, unselectAll) => {
deleteStudents.submit(
{
students: Array.from(selections),
},
{
onSuccess(data) {
students.reload()
studentCount.reload()
props.batch.reload()
toast.success(__('Students deleted successfully'))
unselectAll()
},
}
)
}
</script>

View File

@@ -72,7 +72,7 @@
</div>
</div>
<div v-if="myCourses.data?.length">
<div v-if="myCourses.data?.length" class="mt-10">
<div class="flex items-center justify-between mb-3">
<span class="font-semibold text-lg text-ink-gray-9">
{{

View File

@@ -44,6 +44,10 @@ const routes = [
name: 'Batches',
component: () => import('@/pages/Batches/Batches.vue'),
},
{
path: '/batches/details/:batchName',
redirect: (to) => `/batches/${to.params.batchName}`,
},
{
path: '/batches/:batchName',
name: 'BatchDetail',