Merge pull request #1627 from pateljannat/badge-management
feat: badge management from settings
This commit is contained in:
6
frontend/components.d.ts
vendored
6
frontend/components.d.ts
vendored
@@ -19,6 +19,11 @@ declare module 'vue' {
|
|||||||
AssignmentForm: typeof import('./src/components/Modals/AssignmentForm.vue')['default']
|
AssignmentForm: typeof import('./src/components/Modals/AssignmentForm.vue')['default']
|
||||||
AudioBlock: typeof import('./src/components/AudioBlock.vue')['default']
|
AudioBlock: typeof import('./src/components/AudioBlock.vue')['default']
|
||||||
Autocomplete: typeof import('./src/components/Controls/Autocomplete.vue')['default']
|
Autocomplete: typeof import('./src/components/Controls/Autocomplete.vue')['default']
|
||||||
|
BadgeAssignmentForm: typeof import('./src/components/Settings/BadgeAssignmentForm.vue')['default']
|
||||||
|
BadgeAssignments: typeof import('./src/components/Settings/BadgeAssignments.vue')['default']
|
||||||
|
BadgeForm: typeof import('./src/components/Settings/BadgeForm.vue')['default']
|
||||||
|
Badges: typeof import('./src/components/Settings/Badges.vue')['default']
|
||||||
|
Bagdes: typeof import('./src/components/Settings/Bagdes.vue')['default']
|
||||||
BatchCard: typeof import('./src/components/BatchCard.vue')['default']
|
BatchCard: typeof import('./src/components/BatchCard.vue')['default']
|
||||||
BatchCourseModal: typeof import('./src/components/Modals/BatchCourseModal.vue')['default']
|
BatchCourseModal: typeof import('./src/components/Modals/BatchCourseModal.vue')['default']
|
||||||
BatchCourses: typeof import('./src/components/BatchCourses.vue')['default']
|
BatchCourses: typeof import('./src/components/BatchCourses.vue')['default']
|
||||||
@@ -99,6 +104,7 @@ declare module 'vue' {
|
|||||||
Tags: typeof import('./src/components/Tags.vue')['default']
|
Tags: typeof import('./src/components/Tags.vue')['default']
|
||||||
UnsplashImageBrowser: typeof import('./src/components/UnsplashImageBrowser.vue')['default']
|
UnsplashImageBrowser: typeof import('./src/components/UnsplashImageBrowser.vue')['default']
|
||||||
UpcomingEvaluations: typeof import('./src/components/UpcomingEvaluations.vue')['default']
|
UpcomingEvaluations: typeof import('./src/components/UpcomingEvaluations.vue')['default']
|
||||||
|
Uploader: typeof import('./src/components/Controls/Uploader.vue')['default']
|
||||||
UploadPlugin: typeof import('./src/components/UploadPlugin.vue')['default']
|
UploadPlugin: typeof import('./src/components/UploadPlugin.vue')['default']
|
||||||
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']
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<Combobox v-model="selectedValue" nullable v-slot="{ open: isComboboxOpen }">
|
<div>
|
||||||
|
<div v-if="label" class="text-xs text-ink-gray-5 mb-1">
|
||||||
|
{{ __(label) }}
|
||||||
|
<span class="text-ink-red-3" v-if="attrs.required">*</span>
|
||||||
|
</div>
|
||||||
|
<Combobox
|
||||||
|
v-model="selectedValue"
|
||||||
|
nullable
|
||||||
|
v-slot="{ open: isComboboxOpen }"
|
||||||
|
>
|
||||||
<Popover class="w-full" v-model:show="showOptions">
|
<Popover class="w-full" v-model:show="showOptions">
|
||||||
<template #target="{ open: openPopover, togglePopover }">
|
<template #target="{ open: openPopover, togglePopover }">
|
||||||
<slot name="target" v-bind="{ open: openPopover, togglePopover }">
|
<slot name="target" v-bind="{ open: openPopover, togglePopover }">
|
||||||
@@ -29,7 +38,9 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #body="{ isOpen }">
|
<template #body="{ isOpen }">
|
||||||
<div v-show="isOpen">
|
<div v-show="isOpen">
|
||||||
<div class="mt-1 rounded-lg bg-surface-white py-1 text-base border-2">
|
<div
|
||||||
|
class="mt-1 rounded-lg bg-surface-white py-1 text-base border-2"
|
||||||
|
>
|
||||||
<div class="relative px-1.5 pt-0.5">
|
<div class="relative px-1.5 pt-0.5">
|
||||||
<ComboboxInput
|
<ComboboxInput
|
||||||
ref="search"
|
ref="search"
|
||||||
@@ -123,6 +134,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</Popover>
|
</Popover>
|
||||||
</Combobox>
|
</Combobox>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -149,6 +161,10 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'md',
|
default: 'md',
|
||||||
},
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
variant: {
|
variant: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'subtle',
|
default: 'subtle',
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex w-full flex-col gap-1.5">
|
<div class="flex w-full flex-col gap-1.5">
|
||||||
|
<div v-if="label" class="text-xs text-ink-gray-5">
|
||||||
|
{{ __(label) }}
|
||||||
|
</div>
|
||||||
<codemirror
|
<codemirror
|
||||||
v-model="code"
|
v-model="code"
|
||||||
:extensions="extensions"
|
:extensions="extensions"
|
||||||
@@ -9,6 +12,9 @@
|
|||||||
:style="{ height: height, maxHeight: maxHeight }"
|
:style="{ height: height, maxHeight: maxHeight }"
|
||||||
:disabled="readonly"
|
:disabled="readonly"
|
||||||
@blur="emitEditorValue"
|
@blur="emitEditorValue"
|
||||||
|
:class="{
|
||||||
|
'border border-outline-gray-1': showBorder,
|
||||||
|
}"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
v-if="showSaveButton"
|
v-if="showSaveButton"
|
||||||
@@ -40,6 +46,7 @@ const props = withDefaults(
|
|||||||
showLineNumbers?: boolean
|
showLineNumbers?: boolean
|
||||||
completions?: Function | null
|
completions?: Function | null
|
||||||
label?: string
|
label?: string
|
||||||
|
showBorder?: boolean
|
||||||
required?: boolean
|
required?: boolean
|
||||||
readonly?: boolean
|
readonly?: boolean
|
||||||
}>(),
|
}>(),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
height: height,
|
height: height,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<span class="text-xs text-ink-gray-7" v-if="label">
|
<span class="text-xs text-ink-gray-7 mb-1" v-if="label">
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
|
|||||||
76
frontend/src/components/Controls/Uploader.vue
Normal file
76
frontend/src/components/Controls/Uploader.vue
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-4">
|
||||||
|
<div v-if="label" class="text-xs text-ink-gray-5 mb-2">
|
||||||
|
{{ __(label) }}
|
||||||
|
<span class="text-ink-red-3">*</span>
|
||||||
|
</div>
|
||||||
|
<FileUploader
|
||||||
|
v-if="!modelValue"
|
||||||
|
:fileTypes="['image/*']"
|
||||||
|
:validateFile="validateFile"
|
||||||
|
@success="(file: File) => saveImage(file)"
|
||||||
|
>
|
||||||
|
<template v-slot="{ file, progress, uploading, openFileSelector }">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="border rounded-md w-fit py-7 px-20">
|
||||||
|
<Image class="size-5 stroke-1 text-ink-gray-7" />
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<Button @click="openFileSelector">
|
||||||
|
{{ __('Upload') }}
|
||||||
|
</Button>
|
||||||
|
<div class="mt-1 text-ink-gray-5 text-sm leading-5">
|
||||||
|
{{ __(description) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</FileUploader>
|
||||||
|
<div v-else class="mb-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<img :src="modelValue" class="border rounded-md w-44 h-auto" />
|
||||||
|
<div class="ml-4">
|
||||||
|
<Button @click="removeImage()">
|
||||||
|
{{ __('Remove') }}
|
||||||
|
</Button>
|
||||||
|
<div
|
||||||
|
v-if="description"
|
||||||
|
class="mt-2 text-ink-gray-5 text-sm leading-5"
|
||||||
|
>
|
||||||
|
{{ __(description) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { validateFile } from '@/utils'
|
||||||
|
import { Button, FileUploader } from 'frappe-ui'
|
||||||
|
import { Image } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
modelValue: string
|
||||||
|
label?: string
|
||||||
|
description?: string
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
modelValue: '',
|
||||||
|
label: '',
|
||||||
|
description: '',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const saveImage = (file: any) => {
|
||||||
|
emit('update:modelValue', file.file_url)
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeImage = () => {
|
||||||
|
emit('update:modelValue', '')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
v-model="account.member"
|
v-model="account.member"
|
||||||
:label="__('Member')"
|
:label="__('Member')"
|
||||||
doctype="Course Evaluator"
|
doctype="Course Evaluator"
|
||||||
:onCreate="(value, close) => openSettings('Members', close)"
|
:onCreate="(value: string, close: () => void) => openSettings('Members', close)"
|
||||||
:required="true"
|
:required="true"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
@@ -86,6 +86,12 @@ interface ZoomAccounts {
|
|||||||
options: { onSuccess: () => void; onError: (err: any) => void }
|
options: { onSuccess: () => void; onError: (err: any) => void }
|
||||||
) => void
|
) => void
|
||||||
}
|
}
|
||||||
|
setValue: {
|
||||||
|
submit: (
|
||||||
|
data: ZoomAccount,
|
||||||
|
options: { onSuccess: () => void; onError: (err: any) => void }
|
||||||
|
) => void
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const show = defineModel('show')
|
const show = defineModel('show')
|
||||||
@@ -137,7 +143,7 @@ watch(show, (val) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const saveAccount = (close) => {
|
const saveAccount = (close: () => void) => {
|
||||||
if (props.accountID == 'new') {
|
if (props.accountID == 'new') {
|
||||||
createAccount(close)
|
createAccount(close)
|
||||||
} else {
|
} else {
|
||||||
@@ -145,7 +151,7 @@ const saveAccount = (close) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createAccount = (close) => {
|
const createAccount = (close: () => void) => {
|
||||||
zoomAccounts.value?.insert.submit(
|
zoomAccounts.value?.insert.submit(
|
||||||
{
|
{
|
||||||
account_name: account.name,
|
account_name: account.name,
|
||||||
@@ -167,7 +173,7 @@ const createAccount = (close) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateAccount = async (close) => {
|
const updateAccount = async (close: () => void) => {
|
||||||
if (props.accountID != account.name) {
|
if (props.accountID != account.name) {
|
||||||
await renameDoc()
|
await renameDoc()
|
||||||
}
|
}
|
||||||
@@ -182,11 +188,12 @@ const renameDoc = async () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const setValue = (close) => {
|
const setValue = (close: () => void) => {
|
||||||
zoomAccounts.value?.setValue.submit(
|
zoomAccounts.value?.setValue.submit(
|
||||||
{
|
{
|
||||||
...account,
|
...account,
|
||||||
name: account.name,
|
name: account.name,
|
||||||
|
account_name: props.accountID,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
@@ -194,7 +201,7 @@ const setValue = (close) => {
|
|||||||
close()
|
close()
|
||||||
toast.success(__('Zoom Account updated successfully'))
|
toast.success(__('Zoom Account updated successfully'))
|
||||||
},
|
},
|
||||||
onError(err) {
|
onError(err: any) {
|
||||||
close()
|
close()
|
||||||
toast.error(
|
toast.error(
|
||||||
cleanError(err.messages[0]) || __('Error updating Zoom Account')
|
cleanError(err.messages[0]) || __('Error updating Zoom Account')
|
||||||
|
|||||||
142
frontend/src/components/Settings/BadgeAssignmentForm.vue
Normal file
142
frontend/src/components/Settings/BadgeAssignmentForm.vue
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
v-model="show"
|
||||||
|
:options="{
|
||||||
|
title:
|
||||||
|
props.badgeAssignmentID === 'new'
|
||||||
|
? __('Assign a Badge')
|
||||||
|
: __('Edit Badge Assignment'),
|
||||||
|
size: 'sm',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: __('Save'),
|
||||||
|
variant: 'solid',
|
||||||
|
onClick: ({ close }) => {
|
||||||
|
saveBadgeAssignment(close)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #body-content>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<Link
|
||||||
|
doctype="User"
|
||||||
|
v-model="badgeAssignment.member"
|
||||||
|
:label="__('Member')"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
doctype="LMS Badge"
|
||||||
|
v-model="badgeAssignment.badge"
|
||||||
|
:label="__('Badge')"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-ink-gray-5 mb-1">
|
||||||
|
{{ __('Issued On') }}
|
||||||
|
<span class="text-ink-red-3">*</span>
|
||||||
|
</label>
|
||||||
|
<DatePicker
|
||||||
|
v-model="badgeAssignment.issued_on"
|
||||||
|
:placeholder="__('Select Date')"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Dialog, DatePicker, toast } from 'frappe-ui'
|
||||||
|
import type {
|
||||||
|
BadgeAssignments,
|
||||||
|
BadgeAssignment,
|
||||||
|
} from '@/components/Settings/types'
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { cleanError } from '@/utils'
|
||||||
|
import Link from '@/components/Controls/Link.vue'
|
||||||
|
|
||||||
|
const show = defineModel<boolean>({ required: true, default: false })
|
||||||
|
const defaultBadgeAssignment = {
|
||||||
|
name: '',
|
||||||
|
badge: '',
|
||||||
|
member: '',
|
||||||
|
issued_on: '',
|
||||||
|
member_name: '',
|
||||||
|
member_username: '',
|
||||||
|
member_image: '',
|
||||||
|
}
|
||||||
|
const badgeAssignments = defineModel<BadgeAssignments>('badgeAssignments')
|
||||||
|
const badgeAssignment = ref<BadgeAssignment>(defaultBadgeAssignment)
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
badgeAssignmentID: string
|
||||||
|
badge: string | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.badgeAssignmentID,
|
||||||
|
(newID) => {
|
||||||
|
if (newID === 'new') {
|
||||||
|
badgeAssignment.value = {
|
||||||
|
...defaultBadgeAssignment,
|
||||||
|
badge: props.badge || '',
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const assignment = badgeAssignments.value?.data?.find(
|
||||||
|
(assignment) => assignment.name === newID
|
||||||
|
)
|
||||||
|
if (assignment) {
|
||||||
|
badgeAssignment.value = { ...assignment }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const saveBadgeAssignment = (close: () => void) => {
|
||||||
|
if (props.badgeAssignmentID === 'new') {
|
||||||
|
createBadgeAssignment(close)
|
||||||
|
} else {
|
||||||
|
updateBadgeAssignment(close)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateBadgeAssignment = async (close: () => void) => {
|
||||||
|
badgeAssignments.value?.setValue.submit(
|
||||||
|
{
|
||||||
|
...badgeAssignment.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(__('Badge assignment updated successfully'))
|
||||||
|
close()
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(
|
||||||
|
__('Failed to update badge assignment: ') + cleanError(error)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const createBadgeAssignment = (close: () => void) => {
|
||||||
|
badgeAssignments.value?.insert.submit(
|
||||||
|
{
|
||||||
|
...badgeAssignment.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(__('Badge assignment created successfully'))
|
||||||
|
close()
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(
|
||||||
|
__('Failed to create badge assignment: ') + cleanError(error)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
192
frontend/src/components/Settings/BadgeAssignments.vue
Normal file
192
frontend/src/components/Settings/BadgeAssignments.vue
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
<template>
|
||||||
|
<div class="text-base">
|
||||||
|
<div class="flex items-center justify-between space-x-2 mb-5">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<ChevronLeft
|
||||||
|
class="size-5 stroke-1.5 text-ink-gray-5 cursor-pointer"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
show = false
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<div class="text-xl font-semibold text-ink-gray-9">
|
||||||
|
{{ props.badgeName }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button @click="openForm('new')">
|
||||||
|
<template #prefix>
|
||||||
|
<Plus class="size-4 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
{{ __('New') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div v-if="assignments.data?.length">
|
||||||
|
<ListView
|
||||||
|
:rows="assignments.data"
|
||||||
|
:columns="columns"
|
||||||
|
rowKey="name"
|
||||||
|
:options="{
|
||||||
|
showTooltip: false,
|
||||||
|
onRowClick: (row: BadgeAssignment) => {
|
||||||
|
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 }">
|
||||||
|
<FeatherIcon
|
||||||
|
v-if="item.icon"
|
||||||
|
:name="item.icon"
|
||||||
|
class="h-4 w-4 stroke-1.5"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ListHeaderItem>
|
||||||
|
</ListHeader>
|
||||||
|
<ListRows>
|
||||||
|
<ListRow :row="row" v-for="row in assignments.data">
|
||||||
|
<template #default="{ column, item }">
|
||||||
|
<ListRowItem :item="row[column.key]" :align="column.align">
|
||||||
|
<template #prefix>
|
||||||
|
<div v-if="column.key == 'member_name'">
|
||||||
|
<Avatar
|
||||||
|
class="flex items-center"
|
||||||
|
:image="row['member_image']"
|
||||||
|
:label="item"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div 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="deleteBadgeAssignment(selections, unselectAll)"
|
||||||
|
>
|
||||||
|
<Trash2 class="h-4 w-4 stroke-1.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ListSelectBanner>
|
||||||
|
</ListView>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex flex-col items-center justify-center mt-44">
|
||||||
|
<GraduationCap class="size-10 mx-auto stroke-1 text-ink-gray-5" />
|
||||||
|
<div class="text-lg font-semibold text-ink-gray-7 mb-2.5">
|
||||||
|
{{ __('No Assignments') }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="leading-5 text-base w-2/5 text-base text-center text-ink-gray-7"
|
||||||
|
>
|
||||||
|
{{ __('This badge has not been assigned to any students yet') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<BadgeAssignmentForm
|
||||||
|
v-model="showForm"
|
||||||
|
:badgeAssignmentID="currentAssignmentID"
|
||||||
|
:badge="props.badgeName"
|
||||||
|
v-model:badgeAssignments="assignments"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Button,
|
||||||
|
createListResource,
|
||||||
|
FeatherIcon,
|
||||||
|
ListView,
|
||||||
|
ListHeader,
|
||||||
|
ListHeaderItem,
|
||||||
|
ListRows,
|
||||||
|
ListRow,
|
||||||
|
ListRowItem,
|
||||||
|
ListSelectBanner,
|
||||||
|
toast,
|
||||||
|
} from 'frappe-ui'
|
||||||
|
import { ChevronLeft, GraduationCap, Plus, Trash2 } from 'lucide-vue-next'
|
||||||
|
import { computed, inject, ref } from 'vue'
|
||||||
|
import type { BadgeAssignment } from '@/components/Settings/types'
|
||||||
|
import BadgeAssignmentForm from '@/components/Settings/BadgeAssignmentForm.vue'
|
||||||
|
|
||||||
|
const show = defineModel<boolean>()
|
||||||
|
const dayjs = inject('$dayjs') as any
|
||||||
|
const showForm = ref(false)
|
||||||
|
const currentAssignmentID = ref<string>('')
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
badgeName: string | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const assignments = createListResource({
|
||||||
|
doctype: 'LMS Badge Assignment',
|
||||||
|
fields: [
|
||||||
|
'name',
|
||||||
|
'member',
|
||||||
|
'member_name',
|
||||||
|
'member_username',
|
||||||
|
'member_image',
|
||||||
|
'issued_on',
|
||||||
|
'badge',
|
||||||
|
],
|
||||||
|
filters: {
|
||||||
|
badge: props.badgeName,
|
||||||
|
},
|
||||||
|
order_by: 'issued_on desc',
|
||||||
|
transform(data: BadgeAssignment[]) {
|
||||||
|
return data.map((item: BadgeAssignment) => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
issued_on: item.issued_on
|
||||||
|
? dayjs(item.issued_on).format('DD MMM YYYY')
|
||||||
|
: null,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
auto: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const openForm = (assignmentID: string) => {
|
||||||
|
currentAssignmentID.value = assignmentID
|
||||||
|
showForm.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteBadgeAssignment = (
|
||||||
|
selections: Set<string>,
|
||||||
|
unselectAll: () => void
|
||||||
|
) => {
|
||||||
|
Array.from(selections).forEach(async (assignment: string) => {
|
||||||
|
await assignments.delete.submit(assignment)
|
||||||
|
})
|
||||||
|
unselectAll()
|
||||||
|
toast.success(__('Badge assignments deleted successfully'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: __('Member'),
|
||||||
|
key: 'member_name',
|
||||||
|
icon: 'user',
|
||||||
|
width: '60%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Issued On'),
|
||||||
|
key: 'issued_on',
|
||||||
|
icon: 'calendar',
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
</script>
|
||||||
219
frontend/src/components/Settings/BadgeForm.vue
Normal file
219
frontend/src/components/Settings/BadgeForm.vue
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
v-model="show"
|
||||||
|
:options="{
|
||||||
|
title: badge ? __('Edit Badge') : __('Create a new Badge'),
|
||||||
|
size: '3xl',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #body-content>
|
||||||
|
<div class="grid grid-cols-2 gap-x-5">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<FormControl
|
||||||
|
v-model="badge.enabled"
|
||||||
|
:label="__('Enabled')"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-model="badge.title"
|
||||||
|
:label="__('Title')"
|
||||||
|
type="text"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
<Autocomplete
|
||||||
|
@update:modelValue="(opt: any) => (badge.reference_doctype = opt.value)"
|
||||||
|
:modelValue="badge.reference_doctype"
|
||||||
|
:options="referenceDoctypeOptions"
|
||||||
|
:required="true"
|
||||||
|
:label="__('Assign For')"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-model="badge.description"
|
||||||
|
:label="__('Description')"
|
||||||
|
:required="true"
|
||||||
|
type="textarea"
|
||||||
|
/>
|
||||||
|
<Uploader
|
||||||
|
v-model="badge.image"
|
||||||
|
label="Badge Image"
|
||||||
|
description="An image that represents the badge."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<FormControl
|
||||||
|
v-model="badge.grant_only_once"
|
||||||
|
:label="__('Grant Only Once')"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-model="badge.event"
|
||||||
|
:label="__('Event')"
|
||||||
|
type="select"
|
||||||
|
:options="eventOptions"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-model="badge.user_field"
|
||||||
|
:label="__('Assign To')"
|
||||||
|
type="select"
|
||||||
|
:options="userFieldOptions"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
<CodeEditor
|
||||||
|
v-model="badge.condition"
|
||||||
|
:label="__('Condition')"
|
||||||
|
type="JavaScript"
|
||||||
|
:required="true"
|
||||||
|
:showBorder="true"
|
||||||
|
height="82px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-4"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ close }">
|
||||||
|
<div class="pb-5 float-right">
|
||||||
|
<Button variant="solid" @click="saveBadge(close)">
|
||||||
|
{{ __('Save') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Button, call, Dialog, FormControl, toast } from 'frappe-ui'
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { cleanError } from '@/utils'
|
||||||
|
import type { Badges, Badge } from '@/components/Settings/types'
|
||||||
|
import Autocomplete from '@/components/Controls/Autocomplete.vue'
|
||||||
|
import CodeEditor from '@/components/Controls/CodeEditor.vue'
|
||||||
|
import Uploader from '@/components/Controls/Uploader.vue'
|
||||||
|
|
||||||
|
const defaultBadge = {
|
||||||
|
name: '',
|
||||||
|
title: '',
|
||||||
|
enabled: true,
|
||||||
|
description: '',
|
||||||
|
image: '',
|
||||||
|
grant_only_once: false,
|
||||||
|
event: 'New',
|
||||||
|
reference_doctype: '',
|
||||||
|
condition: '',
|
||||||
|
user_field: 'member',
|
||||||
|
field_to_check: '',
|
||||||
|
}
|
||||||
|
const show = defineModel<boolean>({ required: true, default: false })
|
||||||
|
const badges = defineModel<Badges>('badges')
|
||||||
|
const badge = ref<Badge>(defaultBadge)
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
badgeName: string | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.badgeName,
|
||||||
|
(val) => {
|
||||||
|
if (val != 'new') {
|
||||||
|
badges.value?.data.forEach((bdg: Badge) => {
|
||||||
|
if (bdg.name === val) {
|
||||||
|
badge.value = bdg
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
badge.value = { ...defaultBadge }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const saveBadge = (close: () => void) => {
|
||||||
|
if (props.badgeName == 'new') {
|
||||||
|
createBadge(close)
|
||||||
|
} else {
|
||||||
|
updateBadge(close)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateBadge = async (close: () => void) => {
|
||||||
|
if (props.badgeName != badge.value?.title) {
|
||||||
|
await renameDoc()
|
||||||
|
}
|
||||||
|
setValue(close)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renameDoc = async () => {
|
||||||
|
await call('frappe.client.rename_doc', {
|
||||||
|
doctype: 'LMS Badge',
|
||||||
|
old_name: props.badgeName,
|
||||||
|
new_name: badge.value?.title,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const setValue = (close: () => void) => {
|
||||||
|
badges.value?.setValue.submit(
|
||||||
|
{
|
||||||
|
...badge.value,
|
||||||
|
name: badge.value.title,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess() {
|
||||||
|
badges.value?.reload()
|
||||||
|
close()
|
||||||
|
toast.success(__('Badge updated successfully'))
|
||||||
|
},
|
||||||
|
onError(err: any) {
|
||||||
|
close()
|
||||||
|
toast.error(cleanError(err.messages[0]) || err)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const createBadge = (close: () => void) => {
|
||||||
|
badges.value?.insert.submit(
|
||||||
|
{
|
||||||
|
...badge.value,
|
||||||
|
name: badge.value.name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess() {
|
||||||
|
badges.value?.reload()
|
||||||
|
close()
|
||||||
|
toast.success(__('Badge created successfully'))
|
||||||
|
},
|
||||||
|
onError(err) {
|
||||||
|
close()
|
||||||
|
toast.error(cleanError(err.messages[0]) || __('Error creating badge'))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const referenceDoctypeOptions = computed(() => {
|
||||||
|
return [
|
||||||
|
{ label: __('Course'), value: 'LMS Course' },
|
||||||
|
{ label: __('Batch'), value: 'LMS Batch' },
|
||||||
|
{ label: __('User'), value: 'Member' },
|
||||||
|
{ label: __('Quiz Submission'), value: 'LMS Quiz Submission' },
|
||||||
|
{ label: __('Assignment Submission'), value: 'LMS Assignment Submission' },
|
||||||
|
{
|
||||||
|
label: __('Programming Exercise Submission'),
|
||||||
|
value: 'LMS Programming Exercise Submission',
|
||||||
|
},
|
||||||
|
{ label: __('Course Enrollment'), value: 'LMS Enrollment' },
|
||||||
|
{ label: __('Batch Enrollment'), value: 'LMS Batch Enrollment' },
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const eventOptions = computed(() => {
|
||||||
|
let options = ['New', 'Value Change', 'Auto Assign']
|
||||||
|
return options.map((event) => ({ label: __(event), value: event }))
|
||||||
|
})
|
||||||
|
|
||||||
|
const userFieldOptions = computed(() => {
|
||||||
|
return [
|
||||||
|
{ label: __('Member'), value: 'member' },
|
||||||
|
{ label: __('Owner'), value: 'owner' },
|
||||||
|
]
|
||||||
|
})
|
||||||
|
</script>
|
||||||
247
frontend/src/components/Settings/Badges.vue
Normal file
247
frontend/src/components/Settings/Badges.vue
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
<template>
|
||||||
|
<BadgeAssignments
|
||||||
|
v-if="showAssignments"
|
||||||
|
v-model="showAssignments"
|
||||||
|
:badgeName="showAssignmentsFor"
|
||||||
|
/>
|
||||||
|
<div v-else 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-ink-gray-6 leading-5">
|
||||||
|
{{ __(description) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button @click="openForm('new')">
|
||||||
|
<template #prefix>
|
||||||
|
<Plus class="h-3 w-3 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
{{ __('New') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div v-if="badges.data?.length" class="overflow-y-scroll">
|
||||||
|
<ListView
|
||||||
|
:columns="columns"
|
||||||
|
:rows="badges.data"
|
||||||
|
row-key="name"
|
||||||
|
:options="{
|
||||||
|
showTooltip: false,
|
||||||
|
selectable: false,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<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" :key="item.key">
|
||||||
|
<template #prefix="{ item }">
|
||||||
|
<FeatherIcon
|
||||||
|
v-if="item.icon"
|
||||||
|
:name="item.icon"
|
||||||
|
class="h-4 w-4 stroke-1.5"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ListHeaderItem>
|
||||||
|
</ListHeader>
|
||||||
|
<ListRows>
|
||||||
|
<ListRow :row="row" v-for="row in badges.data" :key="row.name">
|
||||||
|
<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="green">
|
||||||
|
{{ __('Enabled') }}
|
||||||
|
</Badge>
|
||||||
|
<Badge v-else theme="gray">
|
||||||
|
{{ __('Disabled') }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="column.key == 'reference_doctype'">
|
||||||
|
{{
|
||||||
|
doctypeLabel[
|
||||||
|
row[column.key] as keyof typeof doctypeLabel
|
||||||
|
] || row[column.key]
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<FormControl
|
||||||
|
v-else-if="column.key == 'grant_only_once'"
|
||||||
|
v-model="row[column.key]"
|
||||||
|
type="checkbox"
|
||||||
|
:disabled="true"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else-if="column.key != 'action'"
|
||||||
|
class="leading-5 text-sm"
|
||||||
|
>
|
||||||
|
{{ row[column.key] }}
|
||||||
|
</div>
|
||||||
|
<Dropdown
|
||||||
|
v-else
|
||||||
|
:options="getMoreOptions(row.name)"
|
||||||
|
:button="{
|
||||||
|
icon: 'more-horizontal',
|
||||||
|
onblur: (e: Event) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
placement="right"
|
||||||
|
/>
|
||||||
|
</ListRowItem>
|
||||||
|
</template>
|
||||||
|
</ListRow>
|
||||||
|
</ListRows>
|
||||||
|
</ListView>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<BadgeForm
|
||||||
|
v-model="showForm"
|
||||||
|
:badgeName="selectedBadge"
|
||||||
|
v-model:badges="badges"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
createListResource,
|
||||||
|
Dropdown,
|
||||||
|
FeatherIcon,
|
||||||
|
FormControl,
|
||||||
|
ListView,
|
||||||
|
ListHeader,
|
||||||
|
ListHeaderItem,
|
||||||
|
ListRows,
|
||||||
|
ListRow,
|
||||||
|
ListRowItem,
|
||||||
|
toast,
|
||||||
|
} from 'frappe-ui'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { Plus } from 'lucide-vue-next'
|
||||||
|
import { cleanError } from '@/utils'
|
||||||
|
import BadgeForm from '@/components/Settings/BadgeForm.vue'
|
||||||
|
import BadgeAssignments from '@/components/Settings/BadgeAssignments.vue'
|
||||||
|
|
||||||
|
const showForm = ref<boolean>(false)
|
||||||
|
const selectedBadge = ref<string | null>(null)
|
||||||
|
const showAssignments = ref<boolean>(false)
|
||||||
|
const showAssignmentsFor = ref<string | null>(null)
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
label: string
|
||||||
|
description: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const badges = createListResource({
|
||||||
|
doctype: 'LMS Badge',
|
||||||
|
fields: [
|
||||||
|
'name',
|
||||||
|
'title',
|
||||||
|
'enabled',
|
||||||
|
'description',
|
||||||
|
'image',
|
||||||
|
'grant_only_once',
|
||||||
|
'event',
|
||||||
|
'reference_doctype',
|
||||||
|
'condition',
|
||||||
|
'user_field',
|
||||||
|
'field_to_check',
|
||||||
|
],
|
||||||
|
order_by: 'creation desc',
|
||||||
|
auto: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const getMoreOptions = (badgeName: string) => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: __('Edit'),
|
||||||
|
icon: 'edit',
|
||||||
|
onClick() {
|
||||||
|
openForm(badgeName)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Assignments'),
|
||||||
|
icon: 'download',
|
||||||
|
onClick() {
|
||||||
|
showAssignmentsFor.value = badgeName
|
||||||
|
showAssignments.value = true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Delete'),
|
||||||
|
icon: 'trash-2',
|
||||||
|
onClick() {
|
||||||
|
deleteBadge(badgeName)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const openForm = (badgeName: string) => {
|
||||||
|
selectedBadge.value = badgeName
|
||||||
|
showForm.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteBadge = (badgeName: string) => {
|
||||||
|
badges.delete
|
||||||
|
.submit(badgeName)
|
||||||
|
.then(() => {
|
||||||
|
badges.reload()
|
||||||
|
toast.success(__('Badge deleted successfully'))
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
toast.error(cleanError(err.messages[0]) || __('Error deleting badge'))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const doctypeLabel = computed(() => {
|
||||||
|
return {
|
||||||
|
'LMS Course': __('Course'),
|
||||||
|
'LMS Batch': __('Batch'),
|
||||||
|
'LMS Enrollment': __('Course Enrollment'),
|
||||||
|
'LMS Batch Enrollment': __('Batch Enrollment'),
|
||||||
|
'LMS Quiz Submission': __('Quiz Submission'),
|
||||||
|
'LMS Assignment Submission': __('Assignment Submission'),
|
||||||
|
'LMS Programming Exercise Submission': __(
|
||||||
|
'Programming Exercise Submission'
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const columns = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: __('Badge'),
|
||||||
|
key: 'title',
|
||||||
|
icon: 'award',
|
||||||
|
align: 'left',
|
||||||
|
width: '25%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Assigned For'),
|
||||||
|
key: 'reference_doctype',
|
||||||
|
icon: 'info',
|
||||||
|
align: 'left',
|
||||||
|
width: '35%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Status'),
|
||||||
|
key: 'enabled',
|
||||||
|
icon: 'check-square',
|
||||||
|
align: 'left',
|
||||||
|
width: '15%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Grant Only Once'),
|
||||||
|
key: 'grant_only_once',
|
||||||
|
icon: 'check',
|
||||||
|
align: 'center',
|
||||||
|
width: '20%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'action',
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-y-auto">
|
<div class="overflow-y-auto">
|
||||||
<SettingFields :fields="fields" :data="data.data" />
|
<SettingFields :fields="fields" :data="branding.data" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row-reverse mt-auto">
|
<div class="flex flex-row-reverse mt-auto">
|
||||||
<Button variant="solid" :loading="saveSettings.loading" @click="update">
|
<Button variant="solid" :loading="saveSettings.loading" @click="update">
|
||||||
@@ -38,10 +38,6 @@ const props = defineProps({
|
|||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
data: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
@@ -51,6 +47,12 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const branding = createResource({
|
||||||
|
url: 'lms.lms.api.get_branding',
|
||||||
|
auto: true,
|
||||||
|
cache: 'brand',
|
||||||
|
})
|
||||||
|
|
||||||
const saveSettings = createResource({
|
const saveSettings = createResource({
|
||||||
url: 'frappe.client.set_value',
|
url: 'frappe.client.set_value',
|
||||||
makeParams(values) {
|
makeParams(values) {
|
||||||
@@ -86,7 +88,7 @@ const update = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(props.data, (newData) => {
|
watch(branding, (newData) => {
|
||||||
if (newData && !isDirty.value) {
|
if (newData && !isDirty.value) {
|
||||||
isDirty.value = true
|
isDirty.value = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,30 +34,9 @@
|
|||||||
:key="activeTab.label"
|
:key="activeTab.label"
|
||||||
class="flex flex-1 flex-col px-10 py-8 bg-surface-modal"
|
class="flex flex-1 flex-col px-10 py-8 bg-surface-modal"
|
||||||
>
|
>
|
||||||
<Members
|
<component
|
||||||
v-if="activeTab.label === 'Members'"
|
v-if="activeTab.template"
|
||||||
:label="activeTab.label"
|
:is="activeTab.template"
|
||||||
:description="activeTab.description"
|
|
||||||
v-model:show="show"
|
|
||||||
/>
|
|
||||||
<Evaluators
|
|
||||||
v-else-if="activeTab.label === 'Evaluators'"
|
|
||||||
:label="activeTab.label"
|
|
||||||
:description="activeTab.description"
|
|
||||||
v-model:show="show"
|
|
||||||
/>
|
|
||||||
<Categories
|
|
||||||
v-else-if="activeTab.label === 'Categories'"
|
|
||||||
:label="activeTab.label"
|
|
||||||
:description="activeTab.description"
|
|
||||||
/>
|
|
||||||
<EmailTemplates
|
|
||||||
v-else-if="activeTab.label === 'Email Templates'"
|
|
||||||
:label="activeTab.label"
|
|
||||||
:description="activeTab.description"
|
|
||||||
/>
|
|
||||||
<ZoomSettings
|
|
||||||
v-else-if="activeTab.label === 'Zoom Accounts'"
|
|
||||||
:label="activeTab.label"
|
:label="activeTab.label"
|
||||||
:description="activeTab.description"
|
:description="activeTab.description"
|
||||||
/>
|
/>
|
||||||
@@ -68,13 +47,6 @@
|
|||||||
:data="data"
|
:data="data"
|
||||||
:fields="activeTab.fields"
|
:fields="activeTab.fields"
|
||||||
/>
|
/>
|
||||||
<BrandSettings
|
|
||||||
v-else-if="activeTab.label === 'Branding'"
|
|
||||||
:label="activeTab.label"
|
|
||||||
:description="activeTab.description"
|
|
||||||
:fields="activeTab.fields"
|
|
||||||
:data="branding"
|
|
||||||
/>
|
|
||||||
<SettingDetails
|
<SettingDetails
|
||||||
v-else
|
v-else
|
||||||
:fields="activeTab.fields"
|
:fields="activeTab.fields"
|
||||||
@@ -88,8 +60,8 @@
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Dialog, createDocumentResource, createResource } from 'frappe-ui'
|
import { Dialog, createDocumentResource } from 'frappe-ui'
|
||||||
import { ref, computed, watch } from 'vue'
|
import { computed, markRaw, ref, watch } from 'vue'
|
||||||
import { useSettings } from '@/stores/settings'
|
import { useSettings } from '@/stores/settings'
|
||||||
import SettingDetails from '@/components/Settings/SettingDetails.vue'
|
import SettingDetails from '@/components/Settings/SettingDetails.vue'
|
||||||
import SidebarLink from '@/components/SidebarLink.vue'
|
import SidebarLink from '@/components/SidebarLink.vue'
|
||||||
@@ -100,6 +72,7 @@ import EmailTemplates from '@/components/Settings/EmailTemplates.vue'
|
|||||||
import BrandSettings from '@/components/Settings/BrandSettings.vue'
|
import BrandSettings from '@/components/Settings/BrandSettings.vue'
|
||||||
import PaymentSettings from '@/components/Settings/PaymentSettings.vue'
|
import PaymentSettings from '@/components/Settings/PaymentSettings.vue'
|
||||||
import ZoomSettings from '@/components/Settings/ZoomSettings.vue'
|
import ZoomSettings from '@/components/Settings/ZoomSettings.vue'
|
||||||
|
import Badges from '@/components/Settings/Badges.vue'
|
||||||
|
|
||||||
const show = defineModel()
|
const show = defineModel()
|
||||||
const doctype = ref('LMS Settings')
|
const doctype = ref('LMS Settings')
|
||||||
@@ -114,12 +87,6 @@ const data = createDocumentResource({
|
|||||||
auto: true,
|
auto: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const branding = createResource({
|
|
||||||
url: 'lms.lms.api.get_branding',
|
|
||||||
auto: true,
|
|
||||||
cache: 'brand',
|
|
||||||
})
|
|
||||||
|
|
||||||
const tabsStructure = computed(() => {
|
const tabsStructure = computed(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -245,6 +212,7 @@ const tabsStructure = computed(() => {
|
|||||||
description:
|
description:
|
||||||
'Add new members or manage roles and permissions of existing members',
|
'Add new members or manage roles and permissions of existing members',
|
||||||
icon: 'UserRoundPlus',
|
icon: 'UserRoundPlus',
|
||||||
|
template: markRaw(Members),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Evaluators',
|
label: 'Evaluators',
|
||||||
@@ -252,31 +220,44 @@ const tabsStructure = computed(() => {
|
|||||||
icon: 'UserCheck',
|
icon: 'UserCheck',
|
||||||
description:
|
description:
|
||||||
'Add new evaluators or check the slots existing evaluators',
|
'Add new evaluators or check the slots existing evaluators',
|
||||||
|
template: markRaw(Evaluators),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Zoom Accounts',
|
||||||
|
description:
|
||||||
|
'Manage zoom accounts to conduct live classes from batches',
|
||||||
|
icon: 'Video',
|
||||||
|
template: markRaw(ZoomSettings),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Badges',
|
||||||
|
description:
|
||||||
|
'Create badges and assign them to students to acknowledge their achievements',
|
||||||
|
icon: 'Award',
|
||||||
|
template: markRaw(Badges),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Categories',
|
label: 'Categories',
|
||||||
description: 'Double click to edit the category',
|
description: 'Double click to edit the category',
|
||||||
icon: 'Network',
|
icon: 'Network',
|
||||||
|
template: markRaw(Categories),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Email Templates',
|
label: 'Email Templates',
|
||||||
description: 'Manage the email templates for your learning system',
|
description: 'Manage the email templates for your learning system',
|
||||||
icon: 'MailPlus',
|
icon: 'MailPlus',
|
||||||
},
|
template: markRaw(EmailTemplates),
|
||||||
{
|
|
||||||
label: 'Zoom Accounts',
|
|
||||||
description: 'Manage the Zoom accounts for your learning system',
|
|
||||||
icon: 'Video',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Customise',
|
label: 'Customize',
|
||||||
hideLabel: false,
|
hideLabel: false,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
label: 'Branding',
|
label: 'Branding',
|
||||||
icon: 'Blocks',
|
icon: 'Blocks',
|
||||||
|
template: markRaw(BrandSettings),
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
label: 'Brand Name',
|
label: 'Brand Name',
|
||||||
|
|||||||
@@ -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-ink-gray-6 leading-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="openForm('new')">
|
<Button @click="openForm('new')">
|
||||||
@@ -35,10 +35,10 @@
|
|||||||
>
|
>
|
||||||
<ListHeaderItem :item="item" v-for="item in columns">
|
<ListHeaderItem :item="item" v-for="item in columns">
|
||||||
<template #prefix="{ item }">
|
<template #prefix="{ item }">
|
||||||
<component
|
<FeatherIcon
|
||||||
v-if="item.icon"
|
v-if="item.icon"
|
||||||
:is="item.icon"
|
:name="item.icon"
|
||||||
class="h-4 w-4 stroke-1.5 ml-4"
|
class="h-4 w-4 stroke-1.5"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</ListHeaderItem>
|
</ListHeaderItem>
|
||||||
@@ -48,8 +48,18 @@
|
|||||||
<ListRow :row="row" v-for="row in zoomAccounts.data">
|
<ListRow :row="row" v-for="row in zoomAccounts.data">
|
||||||
<template #default="{ column, item }">
|
<template #default="{ column, item }">
|
||||||
<ListRowItem :item="row[column.key]" :align="column.align">
|
<ListRowItem :item="row[column.key]" :align="column.align">
|
||||||
|
<template #prefix>
|
||||||
|
<div v-if="column.key == 'member_name'">
|
||||||
|
<Avatar
|
||||||
|
class="flex items-center"
|
||||||
|
:image="row['member_image']"
|
||||||
|
:label="item"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<div v-if="column.key == 'enabled'">
|
<div v-if="column.key == 'enabled'">
|
||||||
<Badge v-if="row[column.key]" theme="blue">
|
<Badge v-if="row[column.key]" theme="green">
|
||||||
{{ __('Enabled') }}
|
{{ __('Enabled') }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge v-else theme="gray">
|
<Badge v-else theme="gray">
|
||||||
@@ -87,10 +97,12 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
|
Avatar,
|
||||||
Button,
|
Button,
|
||||||
Badge,
|
Badge,
|
||||||
call,
|
call,
|
||||||
createListResource,
|
createListResource,
|
||||||
|
FeatherIcon,
|
||||||
ListView,
|
ListView,
|
||||||
ListHeader,
|
ListHeader,
|
||||||
ListHeaderItem,
|
ListHeaderItem,
|
||||||
@@ -122,6 +134,7 @@ const zoomAccounts = createListResource({
|
|||||||
'enabled',
|
'enabled',
|
||||||
'member',
|
'member',
|
||||||
'member_name',
|
'member_name',
|
||||||
|
'member_image',
|
||||||
'account_id',
|
'account_id',
|
||||||
'client_id',
|
'client_id',
|
||||||
'client_secret',
|
'client_secret',
|
||||||
@@ -170,18 +183,21 @@ const removeAccount = (selections, unselectAll) => {
|
|||||||
|
|
||||||
const columns = computed(() => {
|
const columns = computed(() => {
|
||||||
return [
|
return [
|
||||||
{
|
|
||||||
label: __('Account'),
|
|
||||||
key: 'name',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: __('Member'),
|
label: __('Member'),
|
||||||
key: 'member_name',
|
key: 'member_name',
|
||||||
|
icon: 'user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Account Name'),
|
||||||
|
key: 'name',
|
||||||
|
icon: 'video',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __('Status'),
|
label: __('Status'),
|
||||||
key: 'enabled',
|
key: 'enabled',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
|
icon: 'check-square',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,3 +14,61 @@ export interface User {
|
|||||||
is_fc_site: boolean
|
is_fc_site: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Badge {
|
||||||
|
name: string;
|
||||||
|
title: string;
|
||||||
|
enabled: boolean;
|
||||||
|
description: string;
|
||||||
|
image: string;
|
||||||
|
grant_only_once: boolean;
|
||||||
|
event: string;
|
||||||
|
reference_doctype: string;
|
||||||
|
condition: string;
|
||||||
|
user_field: string;
|
||||||
|
field_to_check: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface Badges {
|
||||||
|
data: Badge[],
|
||||||
|
reload: () => void
|
||||||
|
insert: {
|
||||||
|
submit: (
|
||||||
|
data: Badge,
|
||||||
|
options: { onSuccess: () => void; onError: (err: any) => void }
|
||||||
|
) => void
|
||||||
|
},
|
||||||
|
setValue: {
|
||||||
|
submit: (
|
||||||
|
data: Badge,
|
||||||
|
options: { onSuccess: () => void; onError: (err: any) => void }
|
||||||
|
) => void
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BadgeAssignment {
|
||||||
|
name: string;
|
||||||
|
member: string;
|
||||||
|
member_name: string;
|
||||||
|
member_username: string;
|
||||||
|
member_image: string;
|
||||||
|
badge: string;
|
||||||
|
issued_on: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BadgeAssignments {
|
||||||
|
data: BadgeAssignment[],
|
||||||
|
reload: () => void
|
||||||
|
insert: {
|
||||||
|
submit: (
|
||||||
|
data: BadgeAssignment,
|
||||||
|
options: { onSuccess: () => void; onError: (err: any) => void }
|
||||||
|
) => void
|
||||||
|
},
|
||||||
|
setValue: {
|
||||||
|
submit: (
|
||||||
|
data: BadgeAssignment,
|
||||||
|
options: { onSuccess: () => void; onError: (err: any) => void }
|
||||||
|
) => void
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -9,11 +9,11 @@
|
|||||||
"enabled",
|
"enabled",
|
||||||
"title",
|
"title",
|
||||||
"description",
|
"description",
|
||||||
|
"reference_doctype",
|
||||||
|
"event",
|
||||||
"image",
|
"image",
|
||||||
"column_break_wgum",
|
"column_break_wgum",
|
||||||
"grant_only_once",
|
"grant_only_once",
|
||||||
"event",
|
|
||||||
"reference_doctype",
|
|
||||||
"user_field",
|
"user_field",
|
||||||
"field_to_check",
|
"field_to_check",
|
||||||
"condition"
|
"condition"
|
||||||
@@ -91,6 +91,7 @@
|
|||||||
"reqd": 1
|
"reqd": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [
|
"links": [
|
||||||
{
|
{
|
||||||
@@ -98,7 +99,7 @@
|
|||||||
"link_fieldname": "badge"
|
"link_fieldname": "badge"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-05-27 17:25:55.399830",
|
"modified": "2025-07-04 13:02:19.048994",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Badge",
|
"name": "LMS Badge",
|
||||||
@@ -127,6 +128,7 @@
|
|||||||
"share": 1
|
"share": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
|
|||||||
@@ -27,17 +27,9 @@ class LMSBadge(Document):
|
|||||||
def rule_condition_satisfied(self, doc):
|
def rule_condition_satisfied(self, doc):
|
||||||
doc_before_save = doc.get_doc_before_save()
|
doc_before_save = doc.get_doc_before_save()
|
||||||
|
|
||||||
if self.event == "Manual Assignment":
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.event == "New" and doc_before_save != None:
|
if self.event == "New" and doc_before_save != None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if self.event == "Value Change":
|
|
||||||
field_to_check = self.field_to_check
|
|
||||||
if not field_to_check:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.condition:
|
if self.condition:
|
||||||
return eval_condition(doc, self.condition)
|
return eval_condition(doc, self.condition)
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,10 @@
|
|||||||
"field_order": [
|
"field_order": [
|
||||||
"member",
|
"member",
|
||||||
"member_name",
|
"member_name",
|
||||||
"issued_on",
|
"member_username",
|
||||||
|
"member_image",
|
||||||
"column_break_ugix",
|
"column_break_ugix",
|
||||||
|
"issued_on",
|
||||||
"badge",
|
"badge",
|
||||||
"badge_image",
|
"badge_image",
|
||||||
"badge_description"
|
"badge_description"
|
||||||
@@ -65,12 +67,25 @@
|
|||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Member Name",
|
"label": "Member Name",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "member.username",
|
||||||
|
"fieldname": "member_username",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Member Username"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "member.user_image",
|
||||||
|
"fieldname": "member_image",
|
||||||
|
"fieldtype": "Attach Image",
|
||||||
|
"label": "Member Image"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-01-06 12:32:28.450028",
|
"modified": "2025-07-07 20:37:22.449149",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "sayali@frappe.io",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Badge Assignment",
|
"name": "LMS Badge Assignment",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
@@ -122,6 +137,7 @@
|
|||||||
"share": 1
|
"share": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"show_title_field_in_link": 1,
|
"show_title_field_in_link": 1,
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"account_name",
|
"account_name",
|
||||||
"member",
|
"member",
|
||||||
"member_name",
|
"member_name",
|
||||||
|
"member_image",
|
||||||
"column_break_fxxg",
|
"column_break_fxxg",
|
||||||
"account_id",
|
"account_id",
|
||||||
"client_id",
|
"client_id",
|
||||||
@@ -71,12 +72,18 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_fxxg",
|
"fieldname": "column_break_fxxg",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "member.user_image",
|
||||||
|
"fieldname": "member_image",
|
||||||
|
"fieldtype": "Attach Image",
|
||||||
|
"label": "Member Image"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-05-26 18:09:09.392368",
|
"modified": "2025-07-08 12:20:48.314056",
|
||||||
"modified_by": "sayali@frappe.io",
|
"modified_by": "sayali@frappe.io",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Zoom Settings",
|
"name": "LMS Zoom Settings",
|
||||||
|
|||||||
Reference in New Issue
Block a user