feat: show live class joining and leaving time in attendance list

This commit is contained in:
Jannat Patel
2025-06-12 23:18:35 +05:30
parent bf50e3f898
commit d594419200
11 changed files with 91 additions and 50 deletions

View File

@@ -1,7 +1,9 @@
<template> <template>
<FrappeUIProvider> <FrappeUIProvider>
<Layout> <Layout>
<router-view /> <div class="text-base">
<router-view />
</div>
</Layout> </Layout>
<Dialogs /> <Dialogs />
</FrappeUIProvider> </FrappeUIProvider>

View File

@@ -191,7 +191,7 @@ import {
h, h,
onUnmounted, onUnmounted,
} from 'vue' } from 'vue'
import { getSidebarLinks } from '../utils' import { getSidebarLinks } from '@/utils'
import { usersStore } from '@/stores/user' import { usersStore } from '@/stores/user'
import { sessionStore } from '@/stores/session' import { sessionStore } from '@/stores/session'
import { useSidebar } from '@/stores/sidebar' import { useSidebar } from '@/stores/sidebar'

View File

@@ -70,9 +70,8 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { Badge } from 'frappe-ui' import { formatTime } from '@/utils'
import { formatTime } from '../utils' import { Clock, Globe } from 'lucide-vue-next'
import { Clock, BookOpen, Globe } from 'lucide-vue-next'
import DateRange from '@/components/Common/DateRange.vue' import DateRange from '@/components/Common/DateRange.vue'
import CourseInstructors from '@/components/CourseInstructors.vue' import CourseInstructors from '@/components/CourseInstructors.vue'
import UserAvatar from '@/components/UserAvatar.vue' import UserAvatar from '@/components/UserAvatar.vue'

View File

@@ -94,7 +94,7 @@
</template> </template>
<script setup> <script setup>
import { createResource, TextEditor, Button, Dropdown, toast } from 'frappe-ui' import { createResource, TextEditor, Button, Dropdown, toast } from 'frappe-ui'
import { timeAgo } from '../utils' import { timeAgo } from '@/utils'
import UserAvatar from '@/components/UserAvatar.vue' import UserAvatar from '@/components/UserAvatar.vue'
import { ChevronLeft, MoreHorizontal } from 'lucide-vue-next' import { ChevronLeft, MoreHorizontal } from 'lucide-vue-next'
import { ref, inject, onMounted, onUnmounted } from 'vue' import { ref, inject, onMounted, onUnmounted } from 'vue'

View File

@@ -69,7 +69,7 @@
<script setup> <script setup>
import { createResource, Button } from 'frappe-ui' import { createResource, Button } from 'frappe-ui'
import UserAvatar from '@/components/UserAvatar.vue' import UserAvatar from '@/components/UserAvatar.vue'
import { singularize, timeAgo } from '../utils' import { singularize, timeAgo } from '@/utils'
import { ref, onMounted, inject, onUnmounted } from 'vue' import { ref, onMounted, inject, onUnmounted } from 'vue'
import DiscussionReplies from '@/components/DiscussionReplies.vue' import DiscussionReplies from '@/components/DiscussionReplies.vue'
import DiscussionModal from '@/components/Modals/DiscussionModal.vue' import DiscussionModal from '@/components/Modals/DiscussionModal.vue'

View File

@@ -54,7 +54,7 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { getSidebarLinks } from '../utils' import { getSidebarLinks } from '@/utils'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { watch, ref, onMounted } from 'vue' import { watch, ref, onMounted } from 'vue'
import { sessionStore } from '@/stores/session' import { sessionStore } from '@/stores/session'

View File

@@ -3,44 +3,59 @@
v-model="show" v-model="show"
:options="{ :options="{
title: __('Attendance for Class - {0}').format(live_class?.title), title: __('Attendance for Class - {0}').format(live_class?.title),
size: 'xl', size: '4xl',
}" }"
> >
<template #body-content> <template #body-content>
<div class="space-y-5"> <div
class="grid grid-cols-2 gap-12 text-sm font-semibold text-ink-gray-5 pb-2"
>
<div>
{{ __('Member') }}
</div>
<div class="grid grid-cols-3 gap-20">
<div>
{{ __('Joined at') }}
</div>
<div class="text-center">
{{ __('Left at') }}
</div>
<div>
{{ __('Attended for') }}
</div>
</div>
</div>
<div class="divide-y text-base">
<div <div
v-for="participant in participants.data" v-for="participant in participants.data"
@click="redirectToProfile(participant.member_username)" @click="redirectToProfile(participant.member_username)"
class="cursor-pointer text-base w-fit" class="grid grid-cols-2 items-center w-full text-base w-fit py-2"
> >
<Tooltip placement="right"> <div class="flex items-center space-x-2">
<div class="flex items-center space-x-2"> <Avatar
<Avatar :image="participant.member_image"
:image="participant.member_image" :label="participant.member_name"
:label="participant.member_name" size="xl"
size="xl" />
/> <div class="space-y-1">
<div class="space-y-1"> <div class="font-medium">
<div class="font-medium"> {{ participant.member_name }}
{{ participant.member_name }} </div>
</div> <div>
<div> {{ participant.member }}
{{ participant.member }}
</div>
</div> </div>
</div> </div>
<template #body> </div>
<div
class="max-w-[30ch] rounded bg-surface-gray-7 px-2 py-1 text-p-xs text-ink-white leading-5 shadow-xl" <div class="grid grid-cols-3 gap-20 text-right">
> <div>
{{ dayjs(participant.joined_at).format('HH:mm a') }} - {{ dayjs(participant.joined_at).format('HH:mm a') }}
{{ dayjs(participant.left_at).format('HH:mm a') }} </div>
<br /> <div>
{{ __('attended for') }} {{ participant.duration }} {{ dayjs(participant.left_at).format('HH:mm a') }}
{{ __('minutes') }} </div>
</div> <div>{{ participant.duration }} {{ __('minutes') }}</div>
</template> </div>
</Tooltip>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -116,7 +116,7 @@ import {
EllipsisVertical, EllipsisVertical,
} from 'lucide-vue-next' } from 'lucide-vue-next'
import { inject, ref, getCurrentInstance, computed } from 'vue' import { inject, ref, getCurrentInstance, computed } from 'vue'
import { formatTime } from '../utils' import { formatTime } from '@/utils'
import { Button, createResource, call } from 'frappe-ui' import { Button, createResource, call } from 'frappe-ui'
import EvaluationModal from '@/components/Modals/EvaluationModal.vue' import EvaluationModal from '@/components/Modals/EvaluationModal.vue'
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue' import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'

View File

@@ -1,9 +1,6 @@
<template> <template>
<div> <div>
<div <div v-if="quizzes.length && !showQuiz && readOnly" class="leading-5">
v-if="quizzes.length && !showQuiz && readOnly"
class="bg-surface-blue-2 space-y-1 py-3 px-4 rounded-md text-sm text-ink-blue-3 leading-5"
>
{{ {{
__('This video contains {0} {1}:').format( __('This video contains {0} {1}:').format(
quizzes.length, quizzes.length,
@@ -12,8 +9,10 @@
}} }}
<div v-for="(quiz, index) in quizzes" class="pl-3 mt-1"> <div v-for="(quiz, index) in quizzes" class="pl-3 mt-1">
<span> {{ index + 1 }}. {{ quiz.quiz }} </span> <span>
{{ __('at {0}').format(formatTimestamp(quiz.time)) }} {{ index + 1 }}. <span class="font-semibold"> {{ quiz.quiz }} </span>
</span>
{{ __('at {0} minutes').format(formatTimestamp(quiz.time)) }}
</div> </div>
</div> </div>
<div <div
@@ -118,7 +117,7 @@
/> />
</template> </template>
<script setup> <script setup>
import { ref, onMounted, computed } from 'vue' import { ref, onMounted, computed, watch, getCurrentInstance } from 'vue'
import { Pause, Maximize, Volume2, VolumeX } from 'lucide-vue-next' import { Pause, Maximize, Volume2, VolumeX } from 'lucide-vue-next'
import { Button } from 'frappe-ui' import { Button } from 'frappe-ui'
import { formatSeconds, formatTimestamp } from '@/utils' import { formatSeconds, formatTimestamp } from '@/utils'
@@ -133,9 +132,13 @@ let duration = ref(0)
let muted = ref(false) let muted = ref(false)
const showQuizModal = ref(false) const showQuizModal = ref(false)
const showQuiz = ref(false) const showQuiz = ref(false)
const showQuizLoader = ref(false)
const quizLoadTimer = ref(0)
const currentQuiz = ref(null) const currentQuiz = ref(null)
const nextQuiz = ref({}) const nextQuiz = ref({})
const app = getCurrentInstance()
const { $dialog } = app.appContext.config.globalProperties
console.log($dialog)
const props = defineProps({ const props = defineProps({
file: { file: {
type: String, type: String,
@@ -175,12 +178,31 @@ const updateCurrentTime = () => {
playing.value = false playing.value = false
videoRef.value.onTimeupdate = null videoRef.value.onTimeupdate = null
currentQuiz.value = nextQuiz.value.quiz currentQuiz.value = nextQuiz.value.quiz
showQuiz.value = true quizLoadTimer.value = 5
} }
} }
}, 0) }, 0)
} }
watch(quizLoadTimer, () => {
console.log('watch start')
if (quizLoadTimer.value > 0) {
setTimeout(() => {
quizLoadTimer.value -= 1
if (quizLoadTimer.value === 0) {
showQuizLoader.value = false
showQuiz.value = true
} else {
$dialog({
message: __(
'Complete the upcoming quiz to continue watching the video. Quiz will open in {0} seconds.'
).format(quizLoadTimer.value),
})
}
}, 1000)
}
})
const resumeVideo = (restart = false) => { const resumeVideo = (restart = false) => {
showQuiz.value = false showQuiz.value = false
currentQuiz.value = null currentQuiz.value = null

View File

@@ -122,9 +122,6 @@ onMounted(() => {
const jobs = createResource({ const jobs = createResource({
url: 'lms.lms.api.get_job_opportunities', url: 'lms.lms.api.get_job_opportunities',
cache: ['jobs'], cache: ['jobs'],
onSuccess(data) {
jobCount.value = data.length
},
}) })
const updateJobs = () => { const updateJobs = () => {
@@ -169,6 +166,10 @@ watch(country, (val) => {
updateJobs() updateJobs()
}) })
watch(jobs, () => {
jobCount.value = jobs.data?.length || 0
})
const jobTypes = computed(() => { const jobTypes = computed(() => {
return [ return [
'', '',

View File

@@ -3,6 +3,7 @@ import VideoBlock from '@/components/VideoBlock.vue'
import UploadPlugin from '@/components/UploadPlugin.vue' import UploadPlugin from '@/components/UploadPlugin.vue'
import { h, createApp } from 'vue' import { h, createApp } from 'vue'
import { Upload as UploadIcon } from 'lucide-vue-next' import { Upload as UploadIcon } from 'lucide-vue-next'
import { createDialog } from '@/utils/dialogs'
import translationPlugin from '../translation' import translationPlugin from '../translation'
export class Upload { export class Upload {
@@ -54,6 +55,7 @@ export class Upload {
}, },
}) })
app.use(translationPlugin) app.use(translationPlugin)
app.config.globalProperties.$dialog = createDialog
app.mount(this.wrapper) app.mount(this.wrapper)
return return
} else if (this.isAudio(file.file_type)) { } else if (this.isAudio(file.file_type)) {