feat: show live class joining and leaving time in attendance list
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 [
|
||||||
'',
|
'',
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user