Merge pull request #1542 from pateljannat/zoom-refactor
feat: multiple zoom accounts and zoom attendance
This commit is contained in:
21
frontend/components.d.ts
vendored
21
frontend/components.d.ts
vendored
@@ -27,9 +27,9 @@ declare module 'vue' {
|
|||||||
BatchOverlay: typeof import('./src/components/BatchOverlay.vue')['default']
|
BatchOverlay: typeof import('./src/components/BatchOverlay.vue')['default']
|
||||||
BatchStudentProgress: typeof import('./src/components/Modals/BatchStudentProgress.vue')['default']
|
BatchStudentProgress: typeof import('./src/components/Modals/BatchStudentProgress.vue')['default']
|
||||||
BatchStudents: typeof import('./src/components/BatchStudents.vue')['default']
|
BatchStudents: typeof import('./src/components/BatchStudents.vue')['default']
|
||||||
BrandSettings: typeof import('./src/components/BrandSettings.vue')['default']
|
BrandSettings: typeof import('./src/components/Settings/BrandSettings.vue')['default']
|
||||||
BulkCertificates: typeof import('./src/components/Modals/BulkCertificates.vue')['default']
|
BulkCertificates: typeof import('./src/components/Modals/BulkCertificates.vue')['default']
|
||||||
Categories: typeof import('./src/components/Categories.vue')['default']
|
Categories: typeof import('./src/components/Settings/Categories.vue')['default']
|
||||||
CertificationLinks: typeof import('./src/components/CertificationLinks.vue')['default']
|
CertificationLinks: typeof import('./src/components/CertificationLinks.vue')['default']
|
||||||
ChapterModal: typeof import('./src/components/Modals/ChapterModal.vue')['default']
|
ChapterModal: typeof import('./src/components/Modals/ChapterModal.vue')['default']
|
||||||
CodeEditor: typeof import('./src/components/Controls/CodeEditor.vue')['default']
|
CodeEditor: typeof import('./src/components/Controls/CodeEditor.vue')['default']
|
||||||
@@ -48,10 +48,10 @@ declare module 'vue' {
|
|||||||
EditCoverImage: typeof import('./src/components/Modals/EditCoverImage.vue')['default']
|
EditCoverImage: typeof import('./src/components/Modals/EditCoverImage.vue')['default']
|
||||||
EditProfile: typeof import('./src/components/Modals/EditProfile.vue')['default']
|
EditProfile: typeof import('./src/components/Modals/EditProfile.vue')['default']
|
||||||
EmailTemplateModal: typeof import('./src/components/Modals/EmailTemplateModal.vue')['default']
|
EmailTemplateModal: typeof import('./src/components/Modals/EmailTemplateModal.vue')['default']
|
||||||
EmailTemplates: typeof import('./src/components/EmailTemplates.vue')['default']
|
EmailTemplates: typeof import('./src/components/Settings/EmailTemplates.vue')['default']
|
||||||
EmptyState: typeof import('./src/components/EmptyState.vue')['default']
|
EmptyState: typeof import('./src/components/EmptyState.vue')['default']
|
||||||
EvaluationModal: typeof import('./src/components/Modals/EvaluationModal.vue')['default']
|
EvaluationModal: typeof import('./src/components/Modals/EvaluationModal.vue')['default']
|
||||||
Evaluators: typeof import('./src/components/Evaluators.vue')['default']
|
Evaluators: typeof import('./src/components/Settings/Evaluators.vue')['default']
|
||||||
Event: typeof import('./src/components/Modals/Event.vue')['default']
|
Event: typeof import('./src/components/Modals/Event.vue')['default']
|
||||||
ExplanationVideos: typeof import('./src/components/Modals/ExplanationVideos.vue')['default']
|
ExplanationVideos: typeof import('./src/components/Modals/ExplanationVideos.vue')['default']
|
||||||
FeedbackModal: typeof import('./src/components/Modals/FeedbackModal.vue')['default']
|
FeedbackModal: typeof import('./src/components/Modals/FeedbackModal.vue')['default']
|
||||||
@@ -65,16 +65,17 @@ declare module 'vue' {
|
|||||||
LessonHelp: typeof import('./src/components/LessonHelp.vue')['default']
|
LessonHelp: typeof import('./src/components/LessonHelp.vue')['default']
|
||||||
Link: typeof import('./src/components/Controls/Link.vue')['default']
|
Link: typeof import('./src/components/Controls/Link.vue')['default']
|
||||||
LiveClass: typeof import('./src/components/LiveClass.vue')['default']
|
LiveClass: typeof import('./src/components/LiveClass.vue')['default']
|
||||||
|
LiveClassAttendance: typeof import('./src/components/Modals/LiveClassAttendance.vue')['default']
|
||||||
LiveClassModal: typeof import('./src/components/Modals/LiveClassModal.vue')['default']
|
LiveClassModal: typeof import('./src/components/Modals/LiveClassModal.vue')['default']
|
||||||
LMSLogo: typeof import('./src/components/Icons/LMSLogo.vue')['default']
|
LMSLogo: typeof import('./src/components/Icons/LMSLogo.vue')['default']
|
||||||
Members: typeof import('./src/components/Members.vue')['default']
|
Members: typeof import('./src/components/Settings/Members.vue')['default']
|
||||||
MobileLayout: typeof import('./src/components/MobileLayout.vue')['default']
|
MobileLayout: typeof import('./src/components/MobileLayout.vue')['default']
|
||||||
MultiSelect: typeof import('./src/components/Controls/MultiSelect.vue')['default']
|
MultiSelect: typeof import('./src/components/Controls/MultiSelect.vue')['default']
|
||||||
NoPermission: typeof import('./src/components/NoPermission.vue')['default']
|
NoPermission: typeof import('./src/components/NoPermission.vue')['default']
|
||||||
NoSidebarLayout: typeof import('./src/components/NoSidebarLayout.vue')['default']
|
NoSidebarLayout: typeof import('./src/components/NoSidebarLayout.vue')['default']
|
||||||
NotPermitted: typeof import('./src/components/NotPermitted.vue')['default']
|
NotPermitted: typeof import('./src/components/NotPermitted.vue')['default']
|
||||||
PageModal: typeof import('./src/components/Modals/PageModal.vue')['default']
|
PageModal: typeof import('./src/components/Modals/PageModal.vue')['default']
|
||||||
PaymentSettings: typeof import('./src/components/PaymentSettings.vue')['default']
|
PaymentSettings: typeof import('./src/components/Settings/PaymentSettings.vue')['default']
|
||||||
Play: typeof import('./src/components/Icons/Play.vue')['default']
|
Play: typeof import('./src/components/Icons/Play.vue')['default']
|
||||||
ProgressBar: typeof import('./src/components/ProgressBar.vue')['default']
|
ProgressBar: typeof import('./src/components/ProgressBar.vue')['default']
|
||||||
Question: typeof import('./src/components/Modals/Question.vue')['default']
|
Question: typeof import('./src/components/Modals/Question.vue')['default']
|
||||||
@@ -84,9 +85,9 @@ declare module 'vue' {
|
|||||||
ReviewModal: typeof import('./src/components/Modals/ReviewModal.vue')['default']
|
ReviewModal: typeof import('./src/components/Modals/ReviewModal.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
SettingDetails: typeof import('./src/components/SettingDetails.vue')['default']
|
SettingDetails: typeof import('./src/components/Settings/SettingDetails.vue')['default']
|
||||||
SettingFields: typeof import('./src/components/SettingFields.vue')['default']
|
SettingFields: typeof import('./src/components/Settings/SettingFields.vue')['default']
|
||||||
Settings: typeof import('./src/components/Modals/Settings.vue')['default']
|
Settings: typeof import('./src/components/Settings/Settings.vue')['default']
|
||||||
SidebarLink: typeof import('./src/components/SidebarLink.vue')['default']
|
SidebarLink: typeof import('./src/components/SidebarLink.vue')['default']
|
||||||
StudentHeatmap: typeof import('./src/components/StudentHeatmap.vue')['default']
|
StudentHeatmap: typeof import('./src/components/StudentHeatmap.vue')['default']
|
||||||
StudentModal: typeof import('./src/components/Modals/StudentModal.vue')['default']
|
StudentModal: typeof import('./src/components/Modals/StudentModal.vue')['default']
|
||||||
@@ -97,5 +98,7 @@ declare module 'vue' {
|
|||||||
UserAvatar: typeof import('./src/components/UserAvatar.vue')['default']
|
UserAvatar: typeof import('./src/components/UserAvatar.vue')['default']
|
||||||
UserDropdown: typeof import('./src/components/UserDropdown.vue')['default']
|
UserDropdown: typeof import('./src/components/UserDropdown.vue')['default']
|
||||||
VideoBlock: typeof import('./src/components/VideoBlock.vue')['default']
|
VideoBlock: typeof import('./src/components/VideoBlock.vue')['default']
|
||||||
|
ZoomAccountModal: typeof import('./src/components/Modals/ZoomAccountModal.vue')['default']
|
||||||
|
ZoomSettings: typeof import('./src/components/Settings/ZoomSettings.vue')['default']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col border hover:border-outline-gray-4 rounded-md p-4 h-full"
|
class="flex flex-col border hover:border-outline-gray-3 rounded-md p-4 h-full"
|
||||||
style="min-height: 150px"
|
style="min-height: 150px"
|
||||||
>
|
>
|
||||||
<div class="text-lg leading-5 font-semibold mb-2 text-ink-gray-9">
|
<div class="text-lg leading-5 font-semibold mb-2 text-ink-gray-9">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col border rounded-md p-3 h-full hover:border-outline-gray-4"
|
class="flex flex-col border rounded-md p-3 h-full hover:border-outline-gray-3"
|
||||||
>
|
>
|
||||||
<div class="flex space-x-4 mb-4">
|
<div class="flex space-x-4 mb-4">
|
||||||
<div class="flex flex-col space-y-2 flex-1">
|
<div class="flex flex-col space-y-2 flex-1">
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center justify-between mb-5">
|
<div
|
||||||
|
v-if="hasPermission() && !props.zoomAccount"
|
||||||
|
class="flex items-center space-x-2 mb-5 bg-surface-amber-1 py-1 px-2 rounded-md text-ink-amber-3"
|
||||||
|
>
|
||||||
|
<AlertCircle class="size-4 stroke-1.5" />
|
||||||
|
<span>
|
||||||
|
{{ __('Please add a zoom account to the batch to create live classes.') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
<div class="text-lg font-semibold text-ink-gray-9">
|
<div class="text-lg font-semibold text-ink-gray-9">
|
||||||
{{ __('Live Class') }}
|
{{ __('Live Class') }}
|
||||||
</div>
|
</div>
|
||||||
@@ -12,10 +22,18 @@
|
|||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="liveClasses.data?.length" class="grid grid-cols-2 gap-5">
|
<div v-if="liveClasses.data?.length" class="grid grid-cols-3 gap-5 mt-5">
|
||||||
<div
|
<div
|
||||||
v-for="cls in liveClasses.data"
|
v-for="cls in liveClasses.data"
|
||||||
class="flex flex-col border rounded-md h-full text-ink-gray-7 p-3"
|
class="flex flex-col border rounded-md h-full text-ink-gray-7 hover:border-outline-gray-3 p-3"
|
||||||
|
:class="{
|
||||||
|
'cursor-pointer': hasPermission() && cls.attendees > 0,
|
||||||
|
}"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
openAttendanceModal(cls)
|
||||||
|
}
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<div class="font-semibold text-ink-gray-9 text-lg mb-1">
|
<div class="font-semibold text-ink-gray-9 text-lg mb-1">
|
||||||
{{ cls.title }}
|
{{ cls.title }}
|
||||||
@@ -23,7 +41,7 @@
|
|||||||
<div class="short-introduction">
|
<div class="short-introduction">
|
||||||
{{ cls.description }}
|
{{ cls.description }}
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-3">
|
<div class="mt-auto space-y-3">
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<Calendar class="w-4 h-4 stroke-1.5" />
|
<Calendar class="w-4 h-4 stroke-1.5" />
|
||||||
<span>
|
<span>
|
||||||
@@ -33,18 +51,20 @@
|
|||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<Clock class="w-4 h-4 stroke-1.5" />
|
<Clock class="w-4 h-4 stroke-1.5" />
|
||||||
<span>
|
<span>
|
||||||
{{ formatTime(cls.time) }}
|
{{ formatTime(cls.time) }} -
|
||||||
|
{{ dayjs(getClassEnd(cls)).format('HH:mm') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="cls.date >= dayjs().format('YYYY-MM-DD')"
|
v-if="canAccessClass(cls)"
|
||||||
class="flex items-center space-x-2 text-ink-gray-9 mt-auto"
|
class="flex items-center space-x-2 text-ink-gray-9 mt-auto"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
v-if="user.data?.is_moderator || user.data?.is_evaluator"
|
v-if="user.data?.is_moderator || user.data?.is_evaluator"
|
||||||
:href="cls.start_url"
|
:href="cls.start_url"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="w-1/2 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="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" />
|
<Monitor class="h-4 w-4 stroke-1.5" />
|
||||||
{{ __('Start') }}
|
{{ __('Start') }}
|
||||||
@@ -58,42 +78,63 @@
|
|||||||
{{ __('Join') }}
|
{{ __('Join') }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex items-center space-x-2 text-yellow-700">
|
<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" />
|
<Info class="w-4 h-4 stroke-1.5" />
|
||||||
<span>
|
<span>
|
||||||
{{ __('This class has ended') }}
|
{{ __('Ended') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-sm italic text-ink-gray-5">
|
<div v-else class="text-sm italic text-ink-gray-5 mt-2">
|
||||||
{{ __('No live classes scheduled') }}
|
{{ __('No live classes scheduled') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LiveClassModal
|
<LiveClassModal
|
||||||
:batch="props.batch"
|
:batch="props.batch"
|
||||||
|
:zoomAccount="props.zoomAccount"
|
||||||
v-model="showLiveClassModal"
|
v-model="showLiveClassModal"
|
||||||
v-model:reloadLiveClasses="liveClasses"
|
v-model:reloadLiveClasses="liveClasses"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<LiveClassAttendance v-model="showAttendance" :live_class="attendanceFor" />
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { createListResource, Button } from 'frappe-ui'
|
import { createListResource, Button, Tooltip } from 'frappe-ui'
|
||||||
import { Plus, Clock, Calendar, Video, Monitor, Info } from 'lucide-vue-next'
|
import {
|
||||||
import { inject } from 'vue'
|
Plus,
|
||||||
import LiveClassModal from '@/components/Modals/LiveClassModal.vue'
|
Clock,
|
||||||
import { ref } from 'vue'
|
Calendar,
|
||||||
|
Video,
|
||||||
|
Monitor,
|
||||||
|
Info,
|
||||||
|
AlertCircle,
|
||||||
|
} from 'lucide-vue-next'
|
||||||
|
import { inject, ref } from 'vue'
|
||||||
import { formatTime } from '@/utils/'
|
import { formatTime } from '@/utils/'
|
||||||
|
import LiveClassModal from '@/components/Modals/LiveClassModal.vue'
|
||||||
|
import LiveClassAttendance from '@/components/Modals/LiveClassAttendance.vue'
|
||||||
|
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
const showLiveClassModal = ref(false)
|
const showLiveClassModal = ref(false)
|
||||||
const dayjs = inject('$dayjs')
|
const dayjs = inject('$dayjs')
|
||||||
const readOnlyMode = window.read_only_mode
|
const readOnlyMode = window.read_only_mode
|
||||||
|
const showAttendance = ref(false)
|
||||||
|
const attendanceFor = ref(null)
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
batch: {
|
batch: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
zoomAccount: String,
|
||||||
})
|
})
|
||||||
|
|
||||||
const liveClasses = createListResource({
|
const liveClasses = createListResource({
|
||||||
@@ -106,6 +147,8 @@ const liveClasses = createListResource({
|
|||||||
'description',
|
'description',
|
||||||
'time',
|
'time',
|
||||||
'date',
|
'date',
|
||||||
|
'duration',
|
||||||
|
'attendees',
|
||||||
'start_url',
|
'start_url',
|
||||||
'join_url',
|
'join_url',
|
||||||
'owner',
|
'owner',
|
||||||
@@ -120,8 +163,38 @@ const openLiveClassModal = () => {
|
|||||||
|
|
||||||
const canCreateClass = () => {
|
const canCreateClass = () => {
|
||||||
if (readOnlyMode) return false
|
if (readOnlyMode) return false
|
||||||
|
if (!props.zoomAccount) return false
|
||||||
|
return hasPermission()
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasPermission = () => {
|
||||||
return user.data?.is_moderator || user.data?.is_evaluator
|
return user.data?.is_moderator || user.data?.is_evaluator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canAccessClass = (cls) => {
|
||||||
|
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 getClassEnd = (cls) => {
|
||||||
|
const classStart = new Date(`${cls.date}T${cls.time}`)
|
||||||
|
return new Date(classStart.getTime() + cls.duration * 60000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasClassEnded = (cls) => {
|
||||||
|
const classEnd = getClassEnd(cls)
|
||||||
|
const now = new Date()
|
||||||
|
return now > classEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
const openAttendanceModal = (cls) => {
|
||||||
|
if (!hasPermission()) return
|
||||||
|
if (cls.attendees <= 0) return
|
||||||
|
showAttendance.value = true
|
||||||
|
attendanceFor.value = cls
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
.short-introduction {
|
.short-introduction {
|
||||||
|
|||||||
91
frontend/src/components/Modals/LiveClassAttendance.vue
Normal file
91
frontend/src/components/Modals/LiveClassAttendance.vue
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
v-model="show"
|
||||||
|
:options="{
|
||||||
|
title: __('Attendance for Class - {0}').format(live_class?.title),
|
||||||
|
size: 'xl',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #body-content>
|
||||||
|
<div class="space-y-5">
|
||||||
|
<div
|
||||||
|
v-for="participant in participants.data"
|
||||||
|
@click="redirectToProfile(participant.member_username)"
|
||||||
|
class="cursor-pointer text-base w-fit"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Avatar, createListResource, Dialog, Tooltip } from 'frappe-ui'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { inject } from 'vue'
|
||||||
|
|
||||||
|
const show = defineModel()
|
||||||
|
const router = useRouter()
|
||||||
|
const dayjs = inject('$dayjs')
|
||||||
|
|
||||||
|
interface LiveClass {
|
||||||
|
name: String
|
||||||
|
title: String
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
live_class: LiveClass | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const participants = createListResource({
|
||||||
|
doctype: 'LMS Live Class Participant',
|
||||||
|
filter: {
|
||||||
|
live_class: props.live_class?.name,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
'name',
|
||||||
|
'member',
|
||||||
|
'member_name',
|
||||||
|
'member_image',
|
||||||
|
'member_username',
|
||||||
|
'joined_at',
|
||||||
|
'left_at',
|
||||||
|
'duration',
|
||||||
|
],
|
||||||
|
auto: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const redirectToProfile = (username: string) => {
|
||||||
|
router.push({
|
||||||
|
name: 'Profile',
|
||||||
|
params: { username },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
{
|
{
|
||||||
label: 'Submit',
|
label: 'Submit',
|
||||||
variant: 'solid',
|
variant: 'solid',
|
||||||
onClick: (close) => submitLiveClass(close),
|
onClick: ({ close }) => submitLiveClass(close),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}"
|
}"
|
||||||
@@ -16,14 +16,29 @@
|
|||||||
<template #body-content>
|
<template #body-content>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div class="space-y-4">
|
||||||
<FormControl
|
<FormControl
|
||||||
type="text"
|
type="text"
|
||||||
v-model="liveClass.title"
|
v-model="liveClass.title"
|
||||||
:label="__('Title')"
|
:label="__('Title')"
|
||||||
class="mb-4"
|
|
||||||
:required="true"
|
:required="true"
|
||||||
/>
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-model="liveClass.date"
|
||||||
|
type="date"
|
||||||
|
:label="__('Date')"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
<Tooltip :text="__('Duration of the live class in minutes')">
|
||||||
|
<FormControl
|
||||||
|
type="number"
|
||||||
|
v-model="liveClass.duration"
|
||||||
|
:label="__('Duration')"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-4">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
:text="
|
:text="
|
||||||
__(
|
__(
|
||||||
@@ -35,7 +50,6 @@
|
|||||||
v-model="liveClass.time"
|
v-model="liveClass.time"
|
||||||
type="time"
|
type="time"
|
||||||
:label="__('Time')"
|
:label="__('Time')"
|
||||||
class="mb-4"
|
|
||||||
:required="true"
|
:required="true"
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -52,24 +66,6 @@
|
|||||||
:required="true"
|
:required="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<FormControl
|
|
||||||
v-model="liveClass.date"
|
|
||||||
type="date"
|
|
||||||
class="mb-4"
|
|
||||||
:label="__('Date')"
|
|
||||||
:required="true"
|
|
||||||
/>
|
|
||||||
<Tooltip :text="__('Duration of the live class in minutes')">
|
|
||||||
<FormControl
|
|
||||||
type="number"
|
|
||||||
v-model="liveClass.duration"
|
|
||||||
:label="__('Duration')"
|
|
||||||
class="mb-4"
|
|
||||||
:required="true"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="liveClass.auto_recording"
|
v-model="liveClass.auto_recording"
|
||||||
type="select"
|
type="select"
|
||||||
@@ -107,7 +103,11 @@ const dayjs = inject('$dayjs')
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
batch: {
|
batch: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null,
|
required: true,
|
||||||
|
},
|
||||||
|
zoomAccount: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -159,6 +159,7 @@ const createLiveClass = createResource({
|
|||||||
return {
|
return {
|
||||||
doctype: 'LMS Live Class',
|
doctype: 'LMS Live Class',
|
||||||
batch_name: values.batch,
|
batch_name: values.batch,
|
||||||
|
zoom_account: props.zoomAccount,
|
||||||
...values,
|
...values,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -167,6 +168,20 @@ const createLiveClass = createResource({
|
|||||||
const submitLiveClass = (close) => {
|
const submitLiveClass = (close) => {
|
||||||
return createLiveClass.submit(liveClass, {
|
return createLiveClass.submit(liveClass, {
|
||||||
validate() {
|
validate() {
|
||||||
|
validateFormFields()
|
||||||
|
},
|
||||||
|
onSuccess() {
|
||||||
|
liveClasses.value.reload()
|
||||||
|
refreshForm()
|
||||||
|
close()
|
||||||
|
},
|
||||||
|
onError(err) {
|
||||||
|
toast.error(err.messages?.[0] || err)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateFormFields = () => {
|
||||||
if (!liveClass.title) {
|
if (!liveClass.title) {
|
||||||
return __('Please enter a title.')
|
return __('Please enter a title.')
|
||||||
}
|
}
|
||||||
@@ -197,15 +212,6 @@ const submitLiveClass = (close) => {
|
|||||||
if (!liveClass.duration) {
|
if (!liveClass.duration) {
|
||||||
return __('Please select a duration.')
|
return __('Please select a duration.')
|
||||||
}
|
}
|
||||||
},
|
|
||||||
onSuccess() {
|
|
||||||
liveClasses.value.reload()
|
|
||||||
close()
|
|
||||||
},
|
|
||||||
onError(err) {
|
|
||||||
toast.error(err.messages?.[0] || err)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const valideTime = () => {
|
const valideTime = () => {
|
||||||
@@ -221,4 +227,14 @@ const valideTime = () => {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const refreshForm = () => {
|
||||||
|
liveClass.title = ''
|
||||||
|
liveClass.description = ''
|
||||||
|
liveClass.date = ''
|
||||||
|
liveClass.time = ''
|
||||||
|
liveClass.duration = ''
|
||||||
|
liveClass.timezone = getUserTimezone()
|
||||||
|
liveClass.auto_recording = 'No Recording'
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
206
frontend/src/components/Modals/ZoomAccountModal.vue
Normal file
206
frontend/src/components/Modals/ZoomAccountModal.vue
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
v-model="show"
|
||||||
|
:options="{
|
||||||
|
title:
|
||||||
|
accountID === 'new' ? __('New Zoom Account') : __('Edit Zoom Account'),
|
||||||
|
size: 'xl',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: __('Save'),
|
||||||
|
variant: 'solid',
|
||||||
|
onClick: ({ close }) => {
|
||||||
|
saveAccount(close)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #body-content>
|
||||||
|
<div class="mb-4">
|
||||||
|
<FormControl
|
||||||
|
v-model="account.enabled"
|
||||||
|
:label="__('Enabled')"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-5">
|
||||||
|
<FormControl
|
||||||
|
v-model="account.name"
|
||||||
|
:label="__('Account Name')"
|
||||||
|
type="text"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-model="account.client_id"
|
||||||
|
:label="__('Client ID')"
|
||||||
|
type="text"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
v-model="account.member"
|
||||||
|
:label="__('Member')"
|
||||||
|
doctype="Course Evaluator"
|
||||||
|
:onCreate="(value, close) => openSettings('Members', close)"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-model="account.client_secret"
|
||||||
|
:label="__('Client Secret')"
|
||||||
|
type="password"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-model="account.account_id"
|
||||||
|
:label="__('Account ID')"
|
||||||
|
type="text"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { call, Dialog, FormControl, toast } from 'frappe-ui'
|
||||||
|
import { inject, reactive, watch } from 'vue'
|
||||||
|
import { User } from '@/components/Settings/types'
|
||||||
|
import { openSettings, cleanError } from '@/utils'
|
||||||
|
import Link from '@/components/Controls/Link.vue'
|
||||||
|
|
||||||
|
interface ZoomAccount {
|
||||||
|
name: string
|
||||||
|
account_name: string
|
||||||
|
enabled: boolean
|
||||||
|
member: string
|
||||||
|
account_id: string
|
||||||
|
client_id: string
|
||||||
|
client_secret: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ZoomAccounts {
|
||||||
|
data: ZoomAccount[]
|
||||||
|
reload: () => void
|
||||||
|
insert: {
|
||||||
|
submit: (
|
||||||
|
data: ZoomAccount,
|
||||||
|
options: { onSuccess: () => void; onError: (err: any) => void }
|
||||||
|
) => void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const show = defineModel('show')
|
||||||
|
const user = inject<User | null>('$user')
|
||||||
|
const zoomAccounts = defineModel<ZoomAccounts>('zoomAccounts')
|
||||||
|
|
||||||
|
const account = reactive({
|
||||||
|
name: '',
|
||||||
|
enabled: false,
|
||||||
|
member: user?.data?.name || '',
|
||||||
|
account_id: '',
|
||||||
|
client_id: '',
|
||||||
|
client_secret: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
accountID: {
|
||||||
|
type: String,
|
||||||
|
default: 'new',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.accountID,
|
||||||
|
(val) => {
|
||||||
|
if (val != 'new') {
|
||||||
|
zoomAccounts.value?.data.forEach((acc) => {
|
||||||
|
if (acc.name === val) {
|
||||||
|
account.name = acc.name
|
||||||
|
account.enabled = acc.enabled || false
|
||||||
|
account.member = acc.member
|
||||||
|
account.account_id = acc.account_id
|
||||||
|
account.client_id = acc.client_id
|
||||||
|
account.client_secret = acc.client_secret
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(show, (val) => {
|
||||||
|
if (!val) {
|
||||||
|
account.name = ''
|
||||||
|
account.enabled = false
|
||||||
|
account.member = user?.data?.name || ''
|
||||||
|
account.account_id = ''
|
||||||
|
account.client_id = ''
|
||||||
|
account.client_secret = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const saveAccount = (close) => {
|
||||||
|
if (props.accountID == 'new') {
|
||||||
|
createAccount(close)
|
||||||
|
} else {
|
||||||
|
updateAccount(close)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createAccount = (close) => {
|
||||||
|
zoomAccounts.value?.insert.submit(
|
||||||
|
{
|
||||||
|
account_name: account.name,
|
||||||
|
...account,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess() {
|
||||||
|
zoomAccounts.value?.reload()
|
||||||
|
close()
|
||||||
|
toast.success(__('Zoom Account created successfully'))
|
||||||
|
},
|
||||||
|
onError(err) {
|
||||||
|
close()
|
||||||
|
toast.error(
|
||||||
|
cleanError(err.messages[0]) || __('Error creating Zoom Account')
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateAccount = async (close) => {
|
||||||
|
if (props.accountID != account.name) {
|
||||||
|
await renameDoc()
|
||||||
|
}
|
||||||
|
setValue(close)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renameDoc = async () => {
|
||||||
|
await call('frappe.client.rename_doc', {
|
||||||
|
doctype: 'LMS Zoom Settings',
|
||||||
|
old_name: props.accountID,
|
||||||
|
new_name: account.name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const setValue = (close) => {
|
||||||
|
zoomAccounts.value?.setValue.submit(
|
||||||
|
{
|
||||||
|
...account,
|
||||||
|
name: account.name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess() {
|
||||||
|
zoomAccounts.value?.reload()
|
||||||
|
close()
|
||||||
|
toast.success(__('Zoom Account updated successfully'))
|
||||||
|
},
|
||||||
|
onError(err) {
|
||||||
|
close()
|
||||||
|
toast.error(
|
||||||
|
cleanError(err.messages[0]) || __('Error updating Zoom Account')
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { createResource, Button, Badge } from 'frappe-ui'
|
import { createResource, Button, Badge } from 'frappe-ui'
|
||||||
import SettingFields from '@/components/SettingFields.vue'
|
import SettingFields from '@/components/Settings/SettingFields.vue'
|
||||||
import { watch, ref } from 'vue'
|
import { watch, ref } from 'vue'
|
||||||
|
|
||||||
const isDirty = ref(false)
|
const isDirty = ref(false)
|
||||||
@@ -5,9 +5,9 @@
|
|||||||
<div class="text-xl font-semibold text-ink-gray-9">
|
<div class="text-xl font-semibold text-ink-gray-9">
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-ink-gray-5">
|
<!-- <div class="text-xs text-ink-gray-5">
|
||||||
{{ __(description) }}
|
{{ __(description) }}
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-5">
|
<div class="flex items-center space-x-5">
|
||||||
<Button @click="openTemplateForm('new')">
|
<Button @click="openTemplateForm('new')">
|
||||||
@@ -118,23 +118,7 @@ import { useRouter } from 'vue-router'
|
|||||||
import { ref, watch, reactive, inject } from 'vue'
|
import { ref, watch, reactive, inject } from 'vue'
|
||||||
import { RefreshCw, Plus, X } from 'lucide-vue-next'
|
import { RefreshCw, Plus, X } from 'lucide-vue-next'
|
||||||
import { useOnboarding } from 'frappe-ui/frappe'
|
import { useOnboarding } from 'frappe-ui/frappe'
|
||||||
|
import type { User } from '@/components/Settings/types'
|
||||||
interface User {
|
|
||||||
data: {
|
|
||||||
email: string
|
|
||||||
name: string
|
|
||||||
enabled: boolean
|
|
||||||
user_image: string
|
|
||||||
full_name: string
|
|
||||||
user_type: ['System User', 'Website User']
|
|
||||||
username: string
|
|
||||||
is_moderator: boolean
|
|
||||||
is_system_manager: boolean
|
|
||||||
is_evaluator: boolean
|
|
||||||
is_instructor: boolean
|
|
||||||
is_fc_site: boolean
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const show = defineModel('show')
|
const show = defineModel('show')
|
||||||
@@ -30,9 +30,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import SettingFields from '@/components/SettingFields.vue'
|
import SettingFields from '@/components/Settings/SettingFields.vue'
|
||||||
import { createResource, Badge, Button } from 'frappe-ui'
|
import { createResource, Badge, Button } from 'frappe-ui'
|
||||||
import { watch, ref } from 'vue'
|
import { watch } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
label: {
|
label: {
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Button, Badge, toast } from 'frappe-ui'
|
import { Button, Badge, toast } from 'frappe-ui'
|
||||||
import SettingFields from '@/components/SettingFields.vue'
|
import SettingFields from '@/components/Settings/SettingFields.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
fields: {
|
fields: {
|
||||||
@@ -56,6 +56,11 @@
|
|||||||
:label="activeTab.label"
|
:label="activeTab.label"
|
||||||
:description="activeTab.description"
|
:description="activeTab.description"
|
||||||
/>
|
/>
|
||||||
|
<ZoomSettings
|
||||||
|
v-else-if="activeTab.label === 'Zoom Accounts'"
|
||||||
|
:label="activeTab.label"
|
||||||
|
:description="activeTab.description"
|
||||||
|
/>
|
||||||
<PaymentSettings
|
<PaymentSettings
|
||||||
v-else-if="activeTab.label === 'Payment Gateway'"
|
v-else-if="activeTab.label === 'Payment Gateway'"
|
||||||
:label="activeTab.label"
|
:label="activeTab.label"
|
||||||
@@ -86,14 +91,15 @@
|
|||||||
import { Dialog, createDocumentResource, createResource } from 'frappe-ui'
|
import { Dialog, createDocumentResource, createResource } from 'frappe-ui'
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { useSettings } from '@/stores/settings'
|
import { useSettings } from '@/stores/settings'
|
||||||
import SettingDetails from '../SettingDetails.vue'
|
import SettingDetails from '@/components/Settings/SettingDetails.vue'
|
||||||
import SidebarLink from '@/components/SidebarLink.vue'
|
import SidebarLink from '@/components/SidebarLink.vue'
|
||||||
import Members from '@/components/Members.vue'
|
import Members from '@/components/Settings/Members.vue'
|
||||||
import Evaluators from '@/components/Evaluators.vue'
|
import Evaluators from '@/components/Settings/Evaluators.vue'
|
||||||
import Categories from '@/components/Categories.vue'
|
import Categories from '@/components/Settings/Categories.vue'
|
||||||
import EmailTemplates from '@/components/EmailTemplates.vue'
|
import EmailTemplates from '@/components/Settings/EmailTemplates.vue'
|
||||||
import BrandSettings from '@/components/BrandSettings.vue'
|
import BrandSettings from '@/components/Settings/BrandSettings.vue'
|
||||||
import PaymentSettings from '@/components/PaymentSettings.vue'
|
import PaymentSettings from '@/components/Settings/PaymentSettings.vue'
|
||||||
|
import ZoomSettings from '@/components/Settings/ZoomSettings.vue'
|
||||||
|
|
||||||
const show = defineModel()
|
const show = defineModel()
|
||||||
const doctype = ref('LMS Settings')
|
const doctype = ref('LMS Settings')
|
||||||
@@ -149,13 +155,13 @@ const tabsStructure = computed(() => {
|
|||||||
type: 'Column Break',
|
type: 'Column Break',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Batch Confirmation Template',
|
label: 'Batch Confirmation Email Template',
|
||||||
name: 'batch_confirmation_template',
|
name: 'batch_confirmation_template',
|
||||||
doctype: 'Email Template',
|
doctype: 'Email Template',
|
||||||
type: 'Link',
|
type: 'Link',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Certification Template',
|
label: 'Certification Email Template',
|
||||||
name: 'certification_template',
|
name: 'certification_template',
|
||||||
doctype: 'Email Template',
|
doctype: 'Email Template',
|
||||||
type: 'Link',
|
type: 'Link',
|
||||||
@@ -239,6 +245,11 @@ const tabsStructure = computed(() => {
|
|||||||
description: 'Manage the email templates for your learning system',
|
description: 'Manage the email templates for your learning system',
|
||||||
icon: 'MailPlus',
|
icon: 'MailPlus',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Zoom Accounts',
|
||||||
|
description: 'Manage the Zoom accounts for your learning system',
|
||||||
|
icon: 'Video',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
188
frontend/src/components/Settings/ZoomSettings.vue
Normal file
188
frontend/src/components/Settings/ZoomSettings.vue
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col min-h-0 text-base">
|
||||||
|
<div class="flex items-center justify-between mb-5">
|
||||||
|
<div class="flex flex-col space-y-2">
|
||||||
|
<div class="text-xl font-semibold text-ink-gray-9">
|
||||||
|
{{ label }}
|
||||||
|
</div>
|
||||||
|
<!-- <div class="text-xs text-ink-gray-5">
|
||||||
|
{{ __(description) }}
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-5">
|
||||||
|
<Button @click="openForm('new')">
|
||||||
|
<template #prefix>
|
||||||
|
<Plus class="h-3 w-3 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
{{ __('New') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="zoomAccounts.data?.length" class="overflow-y-scroll">
|
||||||
|
<ListView
|
||||||
|
:columns="columns"
|
||||||
|
:rows="zoomAccounts.data"
|
||||||
|
row-key="name"
|
||||||
|
:options="{
|
||||||
|
showTooltip: false,
|
||||||
|
onRowClick: (row) => {
|
||||||
|
openForm(row.name)
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<ListHeader
|
||||||
|
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
|
||||||
|
>
|
||||||
|
<ListHeaderItem :item="item" v-for="item in columns">
|
||||||
|
<template #prefix="{ item }">
|
||||||
|
<component
|
||||||
|
v-if="item.icon"
|
||||||
|
:is="item.icon"
|
||||||
|
class="h-4 w-4 stroke-1.5 ml-4"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ListHeaderItem>
|
||||||
|
</ListHeader>
|
||||||
|
|
||||||
|
<ListRows>
|
||||||
|
<ListRow :row="row" v-for="row in zoomAccounts.data">
|
||||||
|
<template #default="{ column, item }">
|
||||||
|
<ListRowItem :item="row[column.key]" :align="column.align">
|
||||||
|
<div v-if="column.key == 'enabled'">
|
||||||
|
<Badge v-if="row[column.key]" theme="blue">
|
||||||
|
{{ __('Enabled') }}
|
||||||
|
</Badge>
|
||||||
|
<Badge v-else theme="gray">
|
||||||
|
{{ __('Disabled') }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div v-else class="leading-5 text-sm">
|
||||||
|
{{ row[column.key] }}
|
||||||
|
</div>
|
||||||
|
</ListRowItem>
|
||||||
|
</template>
|
||||||
|
</ListRow>
|
||||||
|
</ListRows>
|
||||||
|
|
||||||
|
<ListSelectBanner>
|
||||||
|
<template #actions="{ unselectAll, selections }">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
@click="removeAccount(selections, unselectAll)"
|
||||||
|
>
|
||||||
|
<Trash2 class="h-4 w-4 stroke-1.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ListSelectBanner>
|
||||||
|
</ListView>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ZoomAccountModal
|
||||||
|
v-model="showForm"
|
||||||
|
v-model:zoomAccounts="zoomAccounts"
|
||||||
|
:accountID="currentAccount"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Badge,
|
||||||
|
call,
|
||||||
|
createListResource,
|
||||||
|
ListView,
|
||||||
|
ListHeader,
|
||||||
|
ListHeaderItem,
|
||||||
|
ListRows,
|
||||||
|
ListRow,
|
||||||
|
ListRowItem,
|
||||||
|
ListSelectBanner,
|
||||||
|
toast,
|
||||||
|
} from 'frappe-ui'
|
||||||
|
import { computed, inject, onMounted, ref } from 'vue'
|
||||||
|
import { Plus, Trash2 } from 'lucide-vue-next'
|
||||||
|
import { cleanError } from '@/utils'
|
||||||
|
import { User } from '@/components/Settings/types'
|
||||||
|
import ZoomAccountModal from '@/components/Modals/ZoomAccountModal.vue'
|
||||||
|
|
||||||
|
const user = inject<User | null>('$user')
|
||||||
|
const showForm = ref(false)
|
||||||
|
const currentAccount = ref<string | null>(null)
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
label: String,
|
||||||
|
description: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
const zoomAccounts = createListResource({
|
||||||
|
doctype: 'LMS Zoom Settings',
|
||||||
|
fields: [
|
||||||
|
'name',
|
||||||
|
'enabled',
|
||||||
|
'member',
|
||||||
|
'member_name',
|
||||||
|
'account_id',
|
||||||
|
'client_id',
|
||||||
|
'client_secret',
|
||||||
|
],
|
||||||
|
cache: ['zoomAccounts'],
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchZoomAccounts()
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchZoomAccounts = () => {
|
||||||
|
if (!user?.data?.is_moderator && !user?.data?.is_evaluator) return
|
||||||
|
|
||||||
|
if (!user?.data?.is_moderator) {
|
||||||
|
zoomAccounts.update({
|
||||||
|
filters: {
|
||||||
|
member: user.data.name,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
zoomAccounts.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
const openForm = (accountID: string) => {
|
||||||
|
currentAccount.value = accountID
|
||||||
|
showForm.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeAccount = (selections, unselectAll) => {
|
||||||
|
call('lms.lms.api.delete_documents', {
|
||||||
|
doctype: 'LMS Zoom Settings',
|
||||||
|
documents: Array.from(selections),
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
zoomAccounts.reload()
|
||||||
|
toast.success(__('Email Templates deleted successfully'))
|
||||||
|
unselectAll()
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error(
|
||||||
|
cleanError(err.messages[0]) || __('Error deleting email templates')
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: __('Account'),
|
||||||
|
key: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Member'),
|
||||||
|
key: 'member_name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Enabled'),
|
||||||
|
key: 'enabled',
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
</script>
|
||||||
16
frontend/src/components/Settings/types.ts
Normal file
16
frontend/src/components/Settings/types.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export interface User {
|
||||||
|
data: {
|
||||||
|
email: string
|
||||||
|
name: string
|
||||||
|
enabled: boolean
|
||||||
|
user_image: string
|
||||||
|
full_name: string
|
||||||
|
user_type: ['System User', 'Website User']
|
||||||
|
username: string
|
||||||
|
is_moderator: boolean
|
||||||
|
is_system_manager: boolean
|
||||||
|
is_evaluator: boolean
|
||||||
|
is_instructor: boolean
|
||||||
|
is_fc_site: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,7 +72,7 @@ import { usersStore } from '@/stores/user'
|
|||||||
import { useSettings } from '@/stores/settings'
|
import { useSettings } from '@/stores/settings'
|
||||||
import { markRaw, watch, ref, onMounted, computed } from 'vue'
|
import { markRaw, watch, ref, onMounted, computed } from 'vue'
|
||||||
import { createDialog } from '@/utils/dialogs'
|
import { createDialog } from '@/utils/dialogs'
|
||||||
import SettingsModal from '@/components/Modals/Settings.vue'
|
import SettingsModal from '@/components/Settings/Settings.vue'
|
||||||
import FrappeCloudIcon from '@/components/Icons/FrappeCloudIcon.vue'
|
import FrappeCloudIcon from '@/components/Icons/FrappeCloudIcon.vue'
|
||||||
import {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
|
|||||||
@@ -70,7 +70,10 @@
|
|||||||
<BatchStudents :batch="batch" />
|
<BatchStudents :batch="batch" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="tab.label == 'Classes'">
|
<div v-else-if="tab.label == 'Classes'">
|
||||||
<LiveClass :batch="batch.data.name" />
|
<LiveClass
|
||||||
|
:batch="batch.data.name"
|
||||||
|
:zoomAccount="batch.data.zoom_account"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="tab.label == 'Assessments'">
|
<div v-else-if="tab.label == 'Assessments'">
|
||||||
<Assessments :batch="batch.data.name" />
|
<Assessments :batch="batch.data.name" />
|
||||||
|
|||||||
@@ -159,6 +159,16 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
<Link
|
||||||
|
doctype="LMS Zoom Settings"
|
||||||
|
:label="__('Zoom Account')"
|
||||||
|
v-model="batch.zoom_account"
|
||||||
|
:onCreate="
|
||||||
|
(value, close) => {
|
||||||
|
openSettings('Zoom Accounts', close)
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-5">
|
<div class="space-y-5">
|
||||||
<FormControl
|
<FormControl
|
||||||
@@ -327,6 +337,7 @@ const batch = reactive({
|
|||||||
paid_batch: false,
|
paid_batch: false,
|
||||||
currency: '',
|
currency: '',
|
||||||
amount: 0,
|
amount: 0,
|
||||||
|
zoom_account: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const instructors = ref([])
|
const instructors = ref([])
|
||||||
|
|||||||
@@ -35,7 +35,8 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="jobs.data?.length || jobCount > 0"
|
v-if="jobs.data?.length || jobCount > 0"
|
||||||
class="grid grid-cols-1 md:grid-cols-3 gap-2"
|
class="grid grid-cols-1 gap-2"
|
||||||
|
:class="user.data ? 'md:grid-cols-3' : 'md:grid-cols-2'"
|
||||||
>
|
>
|
||||||
<FormControl
|
<FormControl
|
||||||
type="text"
|
type="text"
|
||||||
@@ -52,6 +53,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<Link
|
<Link
|
||||||
|
v-if="user.data"
|
||||||
doctype="Country"
|
doctype="Country"
|
||||||
v-model="country"
|
v-model="country"
|
||||||
:placeholder="__('Country')"
|
:placeholder="__('Country')"
|
||||||
@@ -164,7 +166,7 @@ const updateFilters = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getJobCount = () => {
|
const getJobCount = () => {
|
||||||
call('frappe.client.get_count', {
|
call('lms.lms.api.get_count', {
|
||||||
doctype: 'Job Opportunity',
|
doctype: 'Job Opportunity',
|
||||||
filters: {
|
filters: {
|
||||||
status: 'Open',
|
status: 'Open',
|
||||||
|
|||||||
20
frontend/tsconfig.json
Normal file
20
frontend/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"strict": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
10
frontend/tsconfig.node.json
Normal file
10
frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
@@ -116,6 +116,7 @@ scheduler_events = {
|
|||||||
"lms.lms.doctype.lms_certificate_request.lms_certificate_request.schedule_evals",
|
"lms.lms.doctype.lms_certificate_request.lms_certificate_request.schedule_evals",
|
||||||
"lms.lms.api.update_course_statistics",
|
"lms.lms.api.update_course_statistics",
|
||||||
"lms.lms.doctype.lms_certificate_request.lms_certificate_request.mark_eval_as_completed",
|
"lms.lms.doctype.lms_certificate_request.lms_certificate_request.mark_eval_as_completed",
|
||||||
|
"lms.lms.doctype.lms_live_class.lms_live_class.update_attendance",
|
||||||
],
|
],
|
||||||
"daily": [
|
"daily": [
|
||||||
"lms.job.doctype.job_opportunity.job_opportunity.update_job_openings",
|
"lms.job.doctype.job_opportunity.job_opportunity.update_job_openings",
|
||||||
|
|||||||
@@ -838,6 +838,14 @@ def delete_documents(doctype, documents):
|
|||||||
frappe.delete_doc(doctype, doc)
|
frappe.delete_doc(doctype, doc)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def get_count(doctype, filters):
|
||||||
|
return frappe.db.count(
|
||||||
|
doctype,
|
||||||
|
filters=filters,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_payment_gateway_details(payment_gateway):
|
def get_payment_gateway_details(payment_gateway):
|
||||||
fields = []
|
fields = []
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"description",
|
"description",
|
||||||
"column_break_hlqw",
|
"column_break_hlqw",
|
||||||
"instructors",
|
"instructors",
|
||||||
|
"zoom_account",
|
||||||
"section_break_rgfj",
|
"section_break_rgfj",
|
||||||
"medium",
|
"medium",
|
||||||
"category",
|
"category",
|
||||||
@@ -354,6 +355,12 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "section_break_cssv",
|
"fieldname": "section_break_cssv",
|
||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "zoom_account",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Zoom Account",
|
||||||
|
"options": "LMS Zoom Settings"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
@@ -372,7 +379,7 @@
|
|||||||
"link_fieldname": "batch_name"
|
"link_fieldname": "batch_name"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2025-05-21 13:30:28.904260",
|
"modified": "2025-05-26 15:30:55.083507",
|
||||||
"modified_by": "sayali@frappe.io",
|
"modified_by": "sayali@frappe.io",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Batch",
|
"name": "LMS Batch",
|
||||||
|
|||||||
@@ -146,7 +146,15 @@ class LMSBatch(Document):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def create_live_class(
|
def create_live_class(
|
||||||
batch_name, title, duration, date, time, timezone, auto_recording, description=None
|
batch_name,
|
||||||
|
zoom_account,
|
||||||
|
title,
|
||||||
|
duration,
|
||||||
|
date,
|
||||||
|
time,
|
||||||
|
timezone,
|
||||||
|
auto_recording,
|
||||||
|
description=None,
|
||||||
):
|
):
|
||||||
frappe.only_for("Moderator")
|
frappe.only_for("Moderator")
|
||||||
payload = {
|
payload = {
|
||||||
@@ -161,7 +169,7 @@ def create_live_class(
|
|||||||
"timezone": timezone,
|
"timezone": timezone,
|
||||||
}
|
}
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": "Bearer " + authenticate(),
|
"Authorization": "Bearer " + authenticate(zoom_account),
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
}
|
}
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
@@ -175,6 +183,8 @@ def create_live_class(
|
|||||||
"doctype": "LMS Live Class",
|
"doctype": "LMS Live Class",
|
||||||
"start_url": data.get("start_url"),
|
"start_url": data.get("start_url"),
|
||||||
"join_url": data.get("join_url"),
|
"join_url": data.get("join_url"),
|
||||||
|
"meeting_id": data.get("id"),
|
||||||
|
"uuid": data.get("uuid"),
|
||||||
"title": title,
|
"title": title,
|
||||||
"host": frappe.session.user,
|
"host": frappe.session.user,
|
||||||
"date": date,
|
"date": date,
|
||||||
@@ -183,6 +193,7 @@ def create_live_class(
|
|||||||
"password": data.get("password"),
|
"password": data.get("password"),
|
||||||
"description": description,
|
"description": description,
|
||||||
"auto_recording": auto_recording,
|
"auto_recording": auto_recording,
|
||||||
|
"zoom_account": zoom_account,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
class_details = frappe.get_doc(payload)
|
class_details = frappe.get_doc(payload)
|
||||||
@@ -194,10 +205,10 @@ def create_live_class(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def authenticate():
|
def authenticate(zoom_account):
|
||||||
zoom = frappe.get_single("Zoom Settings")
|
zoom = frappe.get_doc("LMS Zoom Settings", zoom_account)
|
||||||
if not zoom.enable:
|
if not zoom.enabled:
|
||||||
frappe.throw(_("Please enable Zoom Settings to use this feature."))
|
frappe.throw(_("Please enable the zoom account to use this feature."))
|
||||||
|
|
||||||
authenticate_url = f"https://zoom.us/oauth/token?grant_type=account_credentials&account_id={zoom.account_id}"
|
authenticate_url = f"https://zoom.us/oauth/token?grant_type=account_credentials&account_id={zoom.account_id}"
|
||||||
|
|
||||||
|
|||||||
@@ -9,21 +9,27 @@
|
|||||||
"field_order": [
|
"field_order": [
|
||||||
"title",
|
"title",
|
||||||
"host",
|
"host",
|
||||||
|
"zoom_account",
|
||||||
"batch_name",
|
"batch_name",
|
||||||
"event",
|
|
||||||
"column_break_astv",
|
"column_break_astv",
|
||||||
"description",
|
|
||||||
"section_break_glxh",
|
|
||||||
"date",
|
"date",
|
||||||
"duration",
|
|
||||||
"column_break_spvt",
|
|
||||||
"time",
|
"time",
|
||||||
|
"duration",
|
||||||
"timezone",
|
"timezone",
|
||||||
"section_break_yrpq",
|
"section_break_glxh",
|
||||||
|
"description",
|
||||||
|
"column_break_spvt",
|
||||||
|
"event",
|
||||||
|
"auto_recording",
|
||||||
|
"section_break_fhet",
|
||||||
|
"meeting_id",
|
||||||
|
"uuid",
|
||||||
|
"column_break_aony",
|
||||||
|
"attendees",
|
||||||
"password",
|
"password",
|
||||||
|
"section_break_yrpq",
|
||||||
"start_url",
|
"start_url",
|
||||||
"column_break_yokr",
|
"column_break_yokr",
|
||||||
"auto_recording",
|
|
||||||
"join_url"
|
"join_url"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
@@ -73,8 +79,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_glxh",
|
"fieldname": "section_break_glxh",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break"
|
||||||
"label": "Date and Time"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_spvt",
|
"fieldname": "column_break_spvt",
|
||||||
@@ -130,13 +135,50 @@
|
|||||||
"label": "Event",
|
"label": "Event",
|
||||||
"options": "Event",
|
"options": "Event",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "zoom_account",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Zoom Account",
|
||||||
|
"options": "LMS Zoom Settings",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "meeting_id",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Meeting ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "attendees",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Attendees",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_fhet",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "uuid",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "UUID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_aony",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
"in_create": 1,
|
"in_create": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [
|
||||||
"modified": "2024-11-11 18:59:26.396111",
|
{
|
||||||
"modified_by": "Administrator",
|
"link_doctype": "LMS Live Class Participant",
|
||||||
|
"link_fieldname": "live_class"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"modified": "2025-05-27 14:44:35.679712",
|
||||||
|
"modified_by": "sayali@frappe.io",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Live Class",
|
"name": "LMS Live Class",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
@@ -175,6 +217,7 @@
|
|||||||
"share": 1
|
"share": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"show_title_field_in_link": 1,
|
"show_title_field_in_link": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
|||||||
@@ -2,10 +2,13 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from frappe.utils import cint, get_datetime, format_date, nowdate, format_time
|
from frappe.utils import cint, get_datetime, format_date, nowdate, format_time
|
||||||
|
from lms.lms.doctype.lms_batch.lms_batch import authenticate
|
||||||
|
|
||||||
|
|
||||||
class LMSLiveClass(Document):
|
class LMSLiveClass(Document):
|
||||||
@@ -102,3 +105,56 @@ def send_mail(live_class, student):
|
|||||||
args=args,
|
args=args,
|
||||||
header=[_(f"Class Reminder: {live_class.title}"), "orange"],
|
header=[_(f"Class Reminder: {live_class.title}"), "orange"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def update_attendance():
|
||||||
|
past_live_classes = frappe.get_all(
|
||||||
|
"LMS Live Class",
|
||||||
|
{
|
||||||
|
"uuid": ["is", "set"],
|
||||||
|
"attendees": ["is", "not set"],
|
||||||
|
},
|
||||||
|
["name", "uuid", "zoom_account"],
|
||||||
|
)
|
||||||
|
|
||||||
|
for live_class in past_live_classes:
|
||||||
|
attendance_data = get_attendance(live_class)
|
||||||
|
create_attendance(live_class, attendance_data)
|
||||||
|
update_attendees_count(live_class, attendance_data)
|
||||||
|
|
||||||
|
|
||||||
|
def get_attendance(live_class):
|
||||||
|
headers = {
|
||||||
|
"Authorization": "Bearer " + authenticate(live_class.zoom_account),
|
||||||
|
"content-type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded_uuid = requests.utils.quote(live_class.uuid, safe="")
|
||||||
|
response = requests.get(
|
||||||
|
f"https://api.zoom.us/v2/past_meetings/{encoded_uuid}/participants", headers=headers
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
frappe.throw(
|
||||||
|
_("Failed to fetch attendance data from Zoom for class {0}: {1}").format(
|
||||||
|
live_class, response.text
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
return data.get("participants", [])
|
||||||
|
|
||||||
|
|
||||||
|
def create_attendance(live_class, data):
|
||||||
|
for participant in data:
|
||||||
|
doc = frappe.new_doc("LMS Live Class Participant")
|
||||||
|
doc.live_class = live_class.name
|
||||||
|
doc.member = participant.get("user_email")
|
||||||
|
doc.joined_at = participant.get("join_time")
|
||||||
|
doc.left_at = participant.get("leave_time")
|
||||||
|
doc.duration = participant.get("duration")
|
||||||
|
doc.insert()
|
||||||
|
|
||||||
|
|
||||||
|
def update_attendees_count(live_class, data):
|
||||||
|
frappe.db.set_value("LMS Live Class", live_class.name, "attendees", len(data))
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) 2025, Frappe and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
// frappe.ui.form.on("LMS Live Class Participant", {
|
||||||
|
// refresh(frm) {
|
||||||
|
|
||||||
|
// },
|
||||||
|
// });
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2025-05-27 12:09:57.712221",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"live_class",
|
||||||
|
"joined_at",
|
||||||
|
"column_break_dwbm",
|
||||||
|
"duration",
|
||||||
|
"left_at",
|
||||||
|
"section_break_xczy",
|
||||||
|
"member",
|
||||||
|
"member_name",
|
||||||
|
"column_break_bpjn",
|
||||||
|
"member_image",
|
||||||
|
"member_username"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "live_class",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"in_standard_filter": 1,
|
||||||
|
"label": "Live Class",
|
||||||
|
"options": "LMS Live Class",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "member",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_standard_filter": 1,
|
||||||
|
"label": "Member",
|
||||||
|
"options": "User",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "member.full_name",
|
||||||
|
"fieldname": "member_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Member Name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_dwbm",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "duration",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Duration",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "joined_at",
|
||||||
|
"fieldtype": "Datetime",
|
||||||
|
"label": "Joined At",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "left_at",
|
||||||
|
"fieldtype": "Datetime",
|
||||||
|
"label": "Left At",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_xczy",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_bpjn",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "member.user_image",
|
||||||
|
"fieldname": "member_image",
|
||||||
|
"fieldtype": "Attach Image",
|
||||||
|
"label": "Member Image"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "member.username",
|
||||||
|
"fieldname": "member_username",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Member Username"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2025-05-27 22:32:24.196643",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "LMS",
|
||||||
|
"name": "LMS Live Class Participant",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"sort_field": "creation",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
|
"title_field": "member_name"
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2025, Frappe and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class LMSLiveClassParticipant(Document):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Copyright (c) 2025, Frappe and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||||
|
|
||||||
|
|
||||||
|
# On IntegrationTestCase, the doctype test records and all
|
||||||
|
# link-field test record dependencies are recursively loaded
|
||||||
|
# Use these module variables to add/remove to/from that list
|
||||||
|
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||||
|
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||||
|
|
||||||
|
|
||||||
|
class UnitTestLMSLiveClassParticipant(UnitTestCase):
|
||||||
|
"""
|
||||||
|
Unit tests for LMSLiveClassParticipant.
|
||||||
|
Use this class for testing individual functions and methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class IntegrationTestLMSLiveClassParticipant(IntegrationTestCase):
|
||||||
|
"""
|
||||||
|
Integration tests for LMSLiveClassParticipant.
|
||||||
|
Use this class for testing interactions between multiple components.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
0
lms/lms/doctype/lms_zoom_settings/__init__.py
Normal file
0
lms/lms/doctype/lms_zoom_settings/__init__.py
Normal file
8
lms/lms/doctype/lms_zoom_settings/lms_zoom_settings.js
Normal file
8
lms/lms/doctype/lms_zoom_settings/lms_zoom_settings.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) 2025, Frappe and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
// frappe.ui.form.on("LMS Zoom Settings", {
|
||||||
|
// refresh(frm) {
|
||||||
|
|
||||||
|
// },
|
||||||
|
// });
|
||||||
128
lms/lms/doctype/lms_zoom_settings/lms_zoom_settings.json
Normal file
128
lms/lms/doctype/lms_zoom_settings/lms_zoom_settings.json
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"autoname": "field:account_name",
|
||||||
|
"creation": "2025-05-26 13:04:18.285735",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"enabled",
|
||||||
|
"section_break_xfow",
|
||||||
|
"account_name",
|
||||||
|
"member",
|
||||||
|
"member_name",
|
||||||
|
"column_break_fxxg",
|
||||||
|
"account_id",
|
||||||
|
"client_id",
|
||||||
|
"client_secret"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "enabled",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Enabled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "account_id",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Account ID",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "client_id",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Client ID",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "client_secret",
|
||||||
|
"fieldtype": "Password",
|
||||||
|
"label": "Client Secret",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "member",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"in_standard_filter": 1,
|
||||||
|
"label": "Member",
|
||||||
|
"options": "User",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "member.full_name",
|
||||||
|
"fieldname": "member_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Member Name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_xfow",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "account_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Account Name",
|
||||||
|
"reqd": 1,
|
||||||
|
"unique": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_fxxg",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2025-05-26 18:09:09.392368",
|
||||||
|
"modified_by": "sayali@frappe.io",
|
||||||
|
"module": "LMS",
|
||||||
|
"name": "LMS Zoom Settings",
|
||||||
|
"naming_rule": "By fieldname",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Moderator",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"if_owner": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Batch Evaluator",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"sort_field": "creation",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
9
lms/lms/doctype/lms_zoom_settings/lms_zoom_settings.py
Normal file
9
lms/lms/doctype/lms_zoom_settings/lms_zoom_settings.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2025, Frappe and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class LMSZoomSettings(Document):
|
||||||
|
pass
|
||||||
30
lms/lms/doctype/lms_zoom_settings/test_lms_zoom_settings.py
Normal file
30
lms/lms/doctype/lms_zoom_settings/test_lms_zoom_settings.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Copyright (c) 2025, Frappe and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||||
|
|
||||||
|
|
||||||
|
# On IntegrationTestCase, the doctype test records and all
|
||||||
|
# link-field test record dependencies are recursively loaded
|
||||||
|
# Use these module variables to add/remove to/from that list
|
||||||
|
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||||
|
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||||
|
|
||||||
|
|
||||||
|
class UnitTestLMSZoomSettings(UnitTestCase):
|
||||||
|
"""
|
||||||
|
Unit tests for LMSZoomSettings.
|
||||||
|
Use this class for testing individual functions and methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class IntegrationTestLMSZoomSettings(IntegrationTestCase):
|
||||||
|
"""
|
||||||
|
Integration tests for LMSZoomSettings.
|
||||||
|
Use this class for testing interactions between multiple components.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
@@ -1391,6 +1391,7 @@ def get_batch_details(batch):
|
|||||||
"certification",
|
"certification",
|
||||||
"timezone",
|
"timezone",
|
||||||
"category",
|
"category",
|
||||||
|
"zoom_account",
|
||||||
],
|
],
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -104,3 +104,6 @@ lms.patches.v2_0.delete_unused_custom_fields
|
|||||||
lms.patches.v2_0.update_certificate_request_status
|
lms.patches.v2_0.update_certificate_request_status
|
||||||
lms.patches.v2_0.update_job_city_and_country
|
lms.patches.v2_0.update_job_city_and_country
|
||||||
lms.patches.v2_0.update_course_evaluator_data
|
lms.patches.v2_0.update_course_evaluator_data
|
||||||
|
lms.patches.v2_0.move_zoom_settings #20-05-2025
|
||||||
|
lms.patches.v2_0.link_zoom_account_to_live_class
|
||||||
|
lms.patches.v2_0.link_zoom_account_to_batch
|
||||||
11
lms/patches/v2_0/link_zoom_account_to_batch.py
Normal file
11
lms/patches/v2_0/link_zoom_account_to_batch.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
live_classes = frappe.get_all("LMS Live Class", ["name", "batch_name"])
|
||||||
|
zoom_account = frappe.get_all("LMS Zoom Settings", pluck="name")
|
||||||
|
zoom_account = zoom_account[0] if zoom_account else None
|
||||||
|
|
||||||
|
if zoom_account:
|
||||||
|
for live_class in live_classes:
|
||||||
|
frappe.db.set_value("LMS Batch", live_class.batch_name, "zoom_account", zoom_account)
|
||||||
16
lms/patches/v2_0/link_zoom_account_to_live_class.py
Normal file
16
lms/patches/v2_0/link_zoom_account_to_live_class.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
live_classes = frappe.get_all("LMS Live Class", pluck="name")
|
||||||
|
zoom_account = frappe.get_all("LMS Zoom Settings", pluck="name")
|
||||||
|
zoom_account = zoom_account[0] if zoom_account else None
|
||||||
|
|
||||||
|
if zoom_account:
|
||||||
|
for live_class in live_classes:
|
||||||
|
frappe.db.set_value(
|
||||||
|
"LMS Live Class",
|
||||||
|
live_class,
|
||||||
|
"zoom_account",
|
||||||
|
zoom_account,
|
||||||
|
)
|
||||||
27
lms/patches/v2_0/move_zoom_settings.py
Normal file
27
lms/patches/v2_0/move_zoom_settings.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
create_settings()
|
||||||
|
|
||||||
|
|
||||||
|
def create_settings():
|
||||||
|
current_settings = frappe.get_single("Zoom Settings")
|
||||||
|
member = current_settings.owner
|
||||||
|
member_name = frappe.get_value("User", member, "full_name")
|
||||||
|
|
||||||
|
if not frappe.db.exists(
|
||||||
|
"LMS Zoom Settings",
|
||||||
|
{
|
||||||
|
"account_name": member_name,
|
||||||
|
},
|
||||||
|
):
|
||||||
|
new_settings = frappe.new_doc("LMS Zoom Settings")
|
||||||
|
new_settings.enabled = current_settings.enable
|
||||||
|
new_settings.account_name = member_name
|
||||||
|
new_settings.member = member
|
||||||
|
new_settings.member_name = member_name
|
||||||
|
new_settings.account_id = current_settings.account_id
|
||||||
|
new_settings.client_id = current_settings.client_id
|
||||||
|
new_settings.client_secret = current_settings.client_secret
|
||||||
|
new_settings.insert()
|
||||||
Reference in New Issue
Block a user