feat: Admin Home
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -61,7 +61,7 @@ export const sessionStore = defineStore('lms-session', () => {
|
||||
field: 'livecode_url',
|
||||
},
|
||||
cache: 'livecodeURL',
|
||||
auto: true,
|
||||
auto: user.value ? true : false,
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -403,12 +403,6 @@ export function getUserTimezone() {
|
||||
|
||||
export function getSidebarLinks() {
|
||||
return [
|
||||
{
|
||||
label: 'Home',
|
||||
icon: 'Home',
|
||||
to: 'Home',
|
||||
activeFor: ['Home'],
|
||||
},
|
||||
{
|
||||
label: 'Courses',
|
||||
icon: 'BookOpen',
|
||||
|
||||
Reference in New Issue
Block a user