feat: Admin Home

This commit is contained in:
Jannat Patel
2025-08-22 16:47:25 +05:30
parent e0601c7b38
commit b8708382b1
9 changed files with 341 additions and 80 deletions
+10
View File
@@ -383,6 +383,15 @@ const checkIfCanAddProgram = async () => {
return programs.enrolled.length > 0 || programs.published.length > 0
}
const addHome = () => {
sidebarLinks.value.unshift({
label: 'Home',
icon: 'Home',
to: 'Home',
activeFor: ['Home'],
})
}
const openPageModal = (link) => {
showPageModal.value = true
pageToEdit.value = link
@@ -634,6 +643,7 @@ watch(userResource, () => {
if (userResource.data) {
isModerator.value = userResource.data.is_moderator
isInstructor.value = userResource.data.is_instructor
addHome()
addPrograms()
addProgrammingExercises()
addQuizzes()
+163 -4
View File
@@ -3,7 +3,7 @@
<div v-if="createdCourses.data?.length" class="mt-10">
<div class="flex items-center justify-between mb-3">
<span class="font-semibold text-lg">
{{ __('Courses by me') }}
{{ __('Courses Created') }}
</span>
<router-link
:to="{
@@ -31,7 +31,7 @@
<div v-if="createdBatches.data?.length" class="mt-10">
<div class="flex items-center justify-between mb-3">
<span class="font-semibold text-lg">
{{ __('Batches by me') }}
{{ __('Upcoming Batches') }}
</span>
<router-link
:to="{
@@ -54,15 +54,141 @@
<BatchCard :batch="batch" />
</router-link>
</div>
<div class="grid grid-cols-2 gap-5 mt-10">
<div v-if="evals?.data?.length">
<div class="font-semibold text-lg mb-3">
{{ __('Upcoming Evaluations') }}
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
<div
v-for="evaluation in evals?.data"
class="border rounded-md p-3 flex flex-col h-full cursor-pointer"
@click="redirectToProfile()"
>
<div class="font-semibold text-ink-gray-9 text-lg mb-1">
{{ evaluation.course_title }}
</div>
<div class="text-ink-gray-7 text-sm">
<div class="flex items-center mb-2">
<Calendar class="w-4 h-4 stroke-1.5" />
<span class="ml-2">
{{ dayjs(evaluation.date).format('DD MMMM YYYY') }}
</span>
</div>
<div class="flex items-center mb-2">
<Clock class="w-4 h-4 stroke-1.5" />
<span class="ml-2">
{{ formatTime(evaluation.start_time) }}
</span>
</div>
<div class="flex items-center">
<GraduationCap class="w-4 h-4 stroke-1.5" />
<span class="ml-2">
{{ evaluation.member_name }}
</span>
</div>
</div>
</div>
</div>
</div>
<div v-if="liveClasses?.data?.length">
<div class="font-semibold text-lg mb-3">
{{ __('Upcoming Live Classes') }}
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
<div v-for="cls in liveClasses?.data" class="border rounded-md p-3">
<div class="font-semibold text-ink-gray-9 text-lg mb-1">
{{ cls.title }}
</div>
<div class="text-ink-gray-7 text-sm leading-5 mb-4">
{{ cls.description }}
</div>
<div class="mt-auto space-y-3 text-ink-gray-7 text-sm">
<div class="flex items-center space-x-2">
<Calendar class="w-4 h-4 stroke-1.5" />
<span>
{{ dayjs(cls.date).format('DD MMMM YYYY') }}
</span>
</div>
<div class="flex items-center space-x-2">
<Clock class="w-4 h-4 stroke-1.5" />
<span>
{{ formatTime(cls.time) }} -
{{ dayjs(getClassEnd(cls)).format('HH:mm A') }}
</span>
</div>
<div
v-if="canAccessClass(cls)"
class="flex items-center space-x-2 text-ink-gray-9 mt-auto"
>
<a
v-if="user.data?.is_moderator || user.data?.is_evaluator"
:href="cls.start_url"
target="_blank"
class="cursor-pointer inline-flex items-center justify-center gap-2 transition-colors focus:outline-none text-ink-gray-8 bg-surface-gray-2 hover:bg-surface-gray-3 active:bg-surface-gray-4 focus-visible:ring focus-visible:ring-outline-gray-3 h-7 text-base px-2 rounded"
:class="cls.join_url ? 'w-full' : 'w-1/2'"
>
<Monitor class="h-4 w-4 stroke-1.5" />
{{ __('Start') }}
</a>
<a
:href="cls.join_url"
target="_blank"
class="w-full cursor-pointer inline-flex items-center justify-center gap-2 transition-colors focus:outline-none text-ink-gray-8 bg-surface-gray-2 hover:bg-surface-gray-3 active:bg-surface-gray-4 focus-visible:ring focus-visible:ring-outline-gray-3 h-7 text-base px-2 rounded"
>
<Video class="h-4 w-4 stroke-1.5" />
{{ __('Join') }}
</a>
</div>
<Tooltip
v-else-if="hasClassEnded(cls)"
:text="__('This class has ended')"
placement="right"
>
<div
class="flex items-center space-x-2 text-ink-amber-3 w-fit"
>
<Info class="w-4 h-4 stroke-1.5" />
<span>
{{ __('Ended') }}
</span>
</div>
</Tooltip>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { createResource } from 'frappe-ui'
import { MoveRight } from 'lucide-vue-next'
import { createResource, Tooltip } from 'frappe-ui'
import { inject } from 'vue'
import { useRouter } from 'vue-router'
import {
Calendar,
Clock,
GraduationCap,
Info,
Monitor,
MoveRight,
Video,
} from 'lucide-vue-next'
import { formatTime } from '@/utils'
import CourseCard from '@/components/CourseCard.vue'
import BatchCard from '@/components/BatchCard.vue'
const user = inject<any>('$user')
const dayjs = inject<any>('$dayjs')
const router = useRouter()
const props = defineProps<{
liveClasses?: { data?: any[] }
evals?: { data?: any[] }
}>()
const createdCourses = createResource({
url: 'lms.lms.utils.get_created_courses',
auto: true,
@@ -72,4 +198,37 @@ const createdBatches = createResource({
url: 'lms.lms.utils.get_created_batches',
auto: true,
})
const getClassEnd = (cls: { date: string; time: string; duration: number }) => {
const classStart = new Date(`${cls.date}T${cls.time}`)
return new Date(classStart.getTime() + cls.duration * 60000)
}
const canAccessClass = (cls: {
date: string
time: string
duration: number
}) => {
if (cls.date < dayjs().format('YYYY-MM-DD')) return false
if (cls.date > dayjs().format('YYYY-MM-DD')) return false
if (hasClassEnded(cls)) return false
return true
}
const hasClassEnded = (cls: {
date: string
time: string
duration: number
}) => {
const classEnd = getClassEnd(cls)
const now = new Date()
return now > classEnd
}
const redirectToProfile = () => {
router.push({
name: 'ProfileEvaluationSchedule',
params: { username: user.data?.username },
})
}
</script>
+69 -42
View File
@@ -11,45 +11,25 @@
{{ __('Hey') }}, {{ user.data?.full_name }} 👋
</div>
<div class="text-lg text-ink-gray-6">
<span v-if="isAdmin">
{{ __('Manage your courses and batches at a glance') }}
</span>
<span v-else-if="myLiveClasses.data?.length > 0 || evalCount > 0">
<span v-if="myLiveClasses.data?.length > 0">
{{
__('You have {0} upcoming live classes').format(
myLiveClasses.data.length
)
}}
</span>
<span v-if="evalCount > 0">
{{ __(' and {0} evaluation').format(evalCount) }}
</span>
<span>
{{ __(' scheduled.') }}
</span>
</span>
<span v-else-if="myLiveClasses.data?.length > 0">
{{
__('You have {0} upcoming live classes.').format(
myLiveClasses.data.length
)
}}
</span>
<span v-else-if="evalCount > 0">
{{ __('You have {0} evaluations scheduled.').format(evalCount) }}
</span>
<span v-else>
{{ __('Resume where you left off') }}
</span>
{{ subtitle }}
</div>
</div>
<div>
<TabButtons v-if="isAdmin" v-model="currentTab" :buttons="tabs" />
<div v-else class="bg-surface-amber-2 px-2 py-1 rounded-md">
<span> 🔥 </span>
<span>
{{ streakInfo.data?.current_streak }}
</span>
</div>
</div>
</div>
<AdminHome v-if="isAdmin && currentTab === 'instructor'" />
<AdminHome
v-if="isAdmin && currentTab === 'instructor'"
:liveClasses="adminLiveClasses"
:evals="adminEvals"
/>
<StudentHome v-else :myLiveClasses="myLiveClasses" />
</div>
</template>
@@ -77,16 +57,6 @@ onMounted(() => {
})
})
const myLiveClasses = createResource({
url: 'lms.lms.utils.get_my_live_classes',
auto: true,
})
const tabs = [
{ label: __('Student'), value: 'student' },
{ label: __('Instructor'), value: 'instructor' },
]
const isAdmin = computed(() => {
return (
user.data?.is_moderator ||
@@ -95,6 +65,63 @@ const isAdmin = computed(() => {
)
})
const myLiveClasses = createResource({
url: 'lms.lms.utils.get_my_live_classes',
auto: !isAdmin.value ? true : false,
})
const adminLiveClasses = createResource({
url: 'lms.lms.utils.get_admin_live_classes',
auto: isAdmin.value ? true : false,
})
const adminEvals = createResource({
url: 'lms.lms.utils.get_admin_evals',
auto: isAdmin.value ? true : false,
})
const streakInfo = createResource({
url: 'lms.lms.utils.get_streak_info',
auto: true,
})
const subtitle = computed(() => {
if (isAdmin.value) {
if (adminLiveClasses.data?.length > 0 && adminEvals.data?.length > 0) {
return __(
'You have {0} upcoming live classes and {1} evaluations scheduled.'
).format(adminLiveClasses.data.length, adminEvals.data.length)
} else if (adminLiveClasses.data?.length > 0) {
return __('You have {0} upcoming live classes.').format(
adminLiveClasses.data.length
)
} else if (adminEvals.data?.length > 0) {
return __('You have {0} evaluations scheduled.').format(
adminEvals.data.length
)
}
return __('Manage your courses and batches at a glance')
} else {
if (myLiveClasses.data?.length > 0 && evalCount.value > 0) {
return __(
'You have {0} upcoming live classes and {1} evaluations scheduled.'
).format(myLiveClasses.data.length, evalCount.value)
} else if (myLiveClasses.data?.length > 0) {
return __('You have {0} upcoming live classes.').format(
myLiveClasses.data.length
)
} else if (evalCount.value > 0) {
return __('You have {0} evaluations scheduled.').format(evalCount.value)
}
return __('Resume where you left off')
}
})
const tabs = [
{ label: __('Student'), value: 'student' },
{ label: __('Instructor'), value: 'instructor' },
]
usePageMeta(() => {
return {
title: __('Home'),
-5
View File
@@ -159,11 +159,6 @@ const myBatches = createResource({
auto: true,
})
const streakInfo = createResource({
url: 'lms.lms.utils.get_streak_info',
auto: true,
})
const getClassEnd = (cls: { date: string; time: string; duration: number }) => {
const classStart = new Date(`${cls.date}T${cls.time}`)
return new Date(classStart.getTime() + cls.duration * 60000)
+2
View File
@@ -258,6 +258,8 @@ router.beforeEach(async (to, from, next) => {
}
if (!isLoggedIn) {
if (to.name == 'Home') router.push({ name: 'Courses' })
await allowGuestAccess.promise
if (!allowGuestAccess.data) {
window.location.href = '/login'
+1 -1
View File
@@ -61,7 +61,7 @@ export const sessionStore = defineStore('lms-session', () => {
field: 'livecode_url',
},
cache: 'livecodeURL',
auto: true,
auto: user.value ? true : false,
})
return {
-1
View File
@@ -4,7 +4,6 @@ import { createResource } from 'frappe-ui'
import { sessionStore } from './session'
export const useSettings = defineStore('settings', () => {
const { isLoggedIn } = sessionStore()
const isSettingsOpen = ref(false)
const activeTab = ref(null)
-6
View File
@@ -403,12 +403,6 @@ export function getUserTimezone() {
export function getSidebarLinks() {
return [
{
label: 'Home',
icon: 'Home',
to: 'Home',
activeFor: ['Home'],
},
{
label: 'Courses',
icon: 'BookOpen',
+96 -21
View File
@@ -2242,17 +2242,22 @@ def get_created_courses():
if frappe.session.user == "Guest":
return created_courses
courses = frappe.get_all(
"Course Instructor",
{
"instructor": frappe.session.user,
"parenttype": "LMS Course",
},
pluck="parent",
limit=3,
order_by="creation desc",
CourseInstructor = frappe.qb.DocType("Course Instructor")
Course = frappe.qb.DocType("LMS Course")
query = (
frappe.qb.from_(CourseInstructor)
.join(Course)
.on(CourseInstructor.parent == Course.name)
.select(Course.name)
.where(CourseInstructor.instructor == frappe.session.user)
.orderby(Course.published_on, order=frappe.qb.desc)
.limit(3)
)
results = query.run(as_dict=True)
courses = [row["name"] for row in results]
for course in courses:
course_details = get_course_details(course)
created_courses.append(course_details)
@@ -2266,14 +2271,23 @@ def get_created_batches():
if frappe.session.user == "Guest":
return created_batches
batches = frappe.get_all(
"Course Instructor",
{"instructor": frappe.session.user, "parenttype": "LMS Batch"},
pluck="parent",
limit=4,
order_by="creation asc",
CourseInstructor = frappe.qb.DocType("Course Instructor")
Batch = frappe.qb.DocType("LMS Batch")
query = (
frappe.qb.from_(CourseInstructor)
.join(Batch)
.on(CourseInstructor.parent == Batch.name)
.select(Batch.name)
.where(CourseInstructor.instructor == frappe.session.user)
.where(Batch.start_date >= getdate())
.orderby(Batch.start_date, order=frappe.qb.asc)
.limit(4)
)
results = query.run(as_dict=True)
batches = [row["name"] for row in results]
for batch in batches:
batch_details = get_batch_details(batch)
created_batches.append(batch_details)
@@ -2281,6 +2295,70 @@ def get_created_batches():
return created_batches
@frappe.whitelist()
def get_admin_live_classes():
if frappe.session.user == "Guest":
return []
CourseInstructor = frappe.qb.DocType("Course Instructor")
LMSLiveClass = frappe.qb.DocType("LMS Live Class")
query = (
frappe.qb.from_(CourseInstructor)
.join(LMSLiveClass)
.on(CourseInstructor.parent == LMSLiveClass.batch_name)
.select(
LMSLiveClass.name,
LMSLiveClass.title,
LMSLiveClass.description,
LMSLiveClass.time,
LMSLiveClass.date,
LMSLiveClass.duration,
LMSLiveClass.attendees,
LMSLiveClass.start_url,
LMSLiveClass.join_url,
LMSLiveClass.owner,
)
.where(CourseInstructor.instructor == frappe.session.user)
.where(LMSLiveClass.date >= getdate())
.orderby(LMSLiveClass.date, order=frappe.qb.asc)
.limit(4)
)
results = query.run(as_dict=True)
return results
@frappe.whitelist()
def get_admin_evals():
if frappe.session.user == "Guest":
return []
evals = frappe.get_all(
"LMS Certificate Request",
{
"evaluator": frappe.session.user,
"date": [">=", getdate()],
},
[
"name",
"date",
"start_time",
"course",
"evaluator",
"google_meet_link",
"member",
"member_name",
],
limit=4,
order_by="date asc",
)
for evaluation in evals:
evaluation.course_title = frappe.db.get_value("LMS Course", evaluation.course, "title")
return evals
@frappe.whitelist()
def get_streak_info():
if frappe.session.user == "Guest":
@@ -2347,11 +2425,8 @@ def get_streak_info():
max_streak = max(max_streak, streak)
prev_day = d
return 1
""" return {
return {
"current_streak": streak,
"max_streak": max_streak,
"last_activity_date": prev_day.strftime("%Y-%m-%d") if prev_day else None,
"total_days_active": len(all_dates),
"total_days_in_month": (getdate() - getdate(getdate().year, getdate().month, 1)).days + 1,
} """
}