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>
<FrappeUIProvider>
<Layout>
<router-view />
<div class="text-base">
<router-view />
</div>
</Layout>
<Dialogs />
</FrappeUIProvider>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,6 @@
<template>
<div>
<div
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"
>
<div v-if="quizzes.length && !showQuiz && readOnly" class="leading-5">
{{
__('This video contains {0} {1}:').format(
quizzes.length,
@@ -12,8 +9,10 @@
}}
<div v-for="(quiz, index) in quizzes" class="pl-3 mt-1">
<span> {{ index + 1 }}. {{ quiz.quiz }} </span>
{{ __('at {0}').format(formatTimestamp(quiz.time)) }}
<span>
{{ index + 1 }}. <span class="font-semibold"> {{ quiz.quiz }} </span>
</span>
{{ __('at {0} minutes').format(formatTimestamp(quiz.time)) }}
</div>
</div>
<div
@@ -118,7 +117,7 @@
/>
</template>
<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 { Button } from 'frappe-ui'
import { formatSeconds, formatTimestamp } from '@/utils'
@@ -133,9 +132,13 @@ let duration = ref(0)
let muted = ref(false)
const showQuizModal = ref(false)
const showQuiz = ref(false)
const showQuizLoader = ref(false)
const quizLoadTimer = ref(0)
const currentQuiz = ref(null)
const nextQuiz = ref({})
const app = getCurrentInstance()
const { $dialog } = app.appContext.config.globalProperties
console.log($dialog)
const props = defineProps({
file: {
type: String,
@@ -175,12 +178,31 @@ const updateCurrentTime = () => {
playing.value = false
videoRef.value.onTimeupdate = null
currentQuiz.value = nextQuiz.value.quiz
showQuiz.value = true
quizLoadTimer.value = 5
}
}
}, 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) => {
showQuiz.value = false
currentQuiz.value = null

View File

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

View File

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