chore: resolved conflicts
This commit is contained in:
@@ -26,8 +26,8 @@
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col overflow-y-auto">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="p-5 space-y-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="font-semibold text-ink-gray-9">
|
||||
{{ __('Submission') }}
|
||||
</div>
|
||||
@@ -53,7 +53,7 @@
|
||||
!['Pass', 'Fail'].includes(submissionResource.doc?.status) &&
|
||||
submissionResource.doc?.owner == user.data?.name
|
||||
"
|
||||
class="bg-surface-blue-2 text-ink-blue-2 p-3 rounded-md leading-5 text-sm mb-4"
|
||||
class="bg-surface-blue-2 text-ink-blue-2 p-3 rounded-md leading-5 text-sm"
|
||||
>
|
||||
{{ __("You've successfully submitted the assignment.") }}
|
||||
{{
|
||||
@@ -63,12 +63,17 @@
|
||||
}}
|
||||
{{ __('Feel free to make edits to your submission if needed.') }}
|
||||
</div>
|
||||
<div v-if="showUploader()">
|
||||
<div class="text-xs text-ink-gray-5 mt-1 mb-2">
|
||||
{{ __('Add your assignment as {0}').format(assignment.data.type) }}
|
||||
<div v-if="showUploader()" class="border rounded-lg p-3">
|
||||
<div class="font-semibold mb-2">
|
||||
{{ __('Upload Assignment') }}
|
||||
</div>
|
||||
<div class="text-ink-gray-5 text-sm mt-1 mb-4">
|
||||
{{
|
||||
__('You can only upload {0} files').format(assignment.data.type)
|
||||
}}
|
||||
</div>
|
||||
<FileUploader
|
||||
v-if="!submissionFile"
|
||||
v-if="!submissionResource.doc?.assignment_attachment"
|
||||
:fileTypes="getType()"
|
||||
:uploadArgs="{
|
||||
private: true,
|
||||
@@ -87,21 +92,24 @@
|
||||
</template>
|
||||
</FileUploader>
|
||||
<div v-else>
|
||||
<div class="flex text-ink-gray-7">
|
||||
<div class="border self-start rounded-md p-2 mr-2">
|
||||
<FileText class="h-5 w-5 stroke-1.5" />
|
||||
</div>
|
||||
<div class="flex items-center text-ink-gray-7">
|
||||
<a
|
||||
:href="submissionFile.file_url"
|
||||
:href="submissionResource.doc.assignment_attachment"
|
||||
target="_blank"
|
||||
class="flex flex-col cursor-pointer !no-underline"
|
||||
class="cursor-pointer !no-underline text-sm leading-5"
|
||||
>
|
||||
<span class="text-sm leading-5">
|
||||
{{ submissionFile.file_name }}
|
||||
</span>
|
||||
<span class="text-sm text-ink-gray-5 mt-1">
|
||||
{{ getFileSize(submissionFile.file_size) }}
|
||||
</span>
|
||||
<div class="flex items-center">
|
||||
<div class="border rounded-md p-2 mr-2">
|
||||
<FileText class="h-5 w-5 stroke-1.5" />
|
||||
</div>
|
||||
<span>
|
||||
{{
|
||||
submissionResource.doc.assignment_attachment
|
||||
.split('/')
|
||||
.pop()
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
<X
|
||||
v-if="canModifyAssignment"
|
||||
@@ -142,13 +150,13 @@
|
||||
user.data?.name == submissionResource.doc?.owner &&
|
||||
submissionResource.doc?.comments
|
||||
"
|
||||
class="mt-8 p-3 bg-surface-blue-2 rounded-md"
|
||||
class="mt-8 p-3 border rounded-lg"
|
||||
>
|
||||
<div class="text-sm text-ink-gray-5 font-medium mb-2">
|
||||
{{ __('Comments by Evaluator') }}:
|
||||
<div class="text-ink-gray-5 mb-4">
|
||||
{{ __('Comments by Evaluator') }}
|
||||
</div>
|
||||
<div
|
||||
class="leading-5 text-ink-gray-9"
|
||||
class="leading-6 text-ink-gray-9"
|
||||
v-html="submissionResource.doc.comments"
|
||||
></div>
|
||||
</div>
|
||||
@@ -204,10 +212,8 @@ import {
|
||||
} from 'frappe-ui'
|
||||
import { computed, inject, onMounted, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { FileText, X } from 'lucide-vue-next'
|
||||
import { getFileSize } from '@/utils'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const submissionFile = ref(null)
|
||||
const answer = ref(null)
|
||||
const comments = ref(null)
|
||||
const router = useRouter()
|
||||
@@ -266,9 +272,7 @@ const newSubmission = createResource({
|
||||
assignment: props.assignmentID,
|
||||
member: user.data?.name,
|
||||
}
|
||||
if (showUploader()) {
|
||||
doc.assignment_attachment = submissionFile.value.file_url
|
||||
} else {
|
||||
if (!showUploader()) {
|
||||
doc.answer = answer.value
|
||||
}
|
||||
return {
|
||||
@@ -277,19 +281,6 @@ const newSubmission = createResource({
|
||||
},
|
||||
})
|
||||
|
||||
const imageResource = createResource({
|
||||
url: 'lms.lms.api.get_file_info',
|
||||
makeParams(values) {
|
||||
return {
|
||||
file_url: values.image,
|
||||
}
|
||||
},
|
||||
auto: false,
|
||||
onSuccess(data) {
|
||||
submissionFile.value = data
|
||||
},
|
||||
})
|
||||
|
||||
const submissionResource = createDocumentResource({
|
||||
doctype: 'LMS Assignment Submission',
|
||||
name: props.submissionName,
|
||||
@@ -302,11 +293,6 @@ const submissionResource = createDocumentResource({
|
||||
|
||||
watch(submissionResource, () => {
|
||||
if (submissionResource.doc) {
|
||||
if (submissionResource.doc.assignment_attachment) {
|
||||
imageResource.reload({
|
||||
image: submissionResource.doc.assignment_attachment,
|
||||
})
|
||||
}
|
||||
if (submissionResource.doc.answer) {
|
||||
answer.value = submissionResource.doc.answer
|
||||
}
|
||||
@@ -315,7 +301,10 @@ watch(submissionResource, () => {
|
||||
}
|
||||
if (submissionResource.isDirty) {
|
||||
isDirty.value = true
|
||||
} else if (showUploader() && !submissionFile.value) {
|
||||
} else if (
|
||||
showUploader() &&
|
||||
!submissionResource.doc.assignment_attachment
|
||||
) {
|
||||
isDirty.value = true
|
||||
} else if (!showUploader() && !answer.value) {
|
||||
isDirty.value = true
|
||||
@@ -325,11 +314,17 @@ watch(submissionResource, () => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(submissionFile, () => {
|
||||
if (props.submissionName == 'new' && submissionFile.value) {
|
||||
isDirty.value = true
|
||||
watch(
|
||||
() => submissionResource.doc,
|
||||
() => {
|
||||
if (
|
||||
props.submissionName == 'new' &&
|
||||
submissionResource.doc?.assignment_attachment
|
||||
) {
|
||||
isDirty.value = true
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const submitAssignment = () => {
|
||||
if (props.submissionName != 'new') {
|
||||
@@ -341,13 +336,13 @@ const submitAssignment = () => {
|
||||
submissionResource.setValue.submit(
|
||||
{
|
||||
...submissionResource.doc,
|
||||
assignment_attachment: submissionFile.value?.file_url,
|
||||
evaluator: evaluator,
|
||||
comments: comments.value,
|
||||
answer: answer.value,
|
||||
},
|
||||
{
|
||||
onSuccess(data) {
|
||||
isDirty.value = false
|
||||
toast.success(__('Changes saved successfully'))
|
||||
},
|
||||
}
|
||||
@@ -388,7 +383,7 @@ const addNewSubmission = () => {
|
||||
|
||||
const saveSubmission = (file) => {
|
||||
isDirty.value = true
|
||||
submissionFile.value = file
|
||||
submissionResource.doc.assignment_attachment = file.file_url
|
||||
}
|
||||
|
||||
const markLessonProgress = () => {
|
||||
@@ -439,7 +434,7 @@ const validateFile = (file) => {
|
||||
|
||||
const removeSubmission = () => {
|
||||
isDirty.value = true
|
||||
submissionFile.value = null
|
||||
submissionResource.doc.assignment_attachment = ''
|
||||
}
|
||||
|
||||
const canGradeSubmission = computed(() => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div v-if="batch.data" class="border-2 rounded-md p-5 lg:w-72">
|
||||
<div
|
||||
v-if="batch.data.seat_count && seats_left > 0"
|
||||
v-if="batch.data.seat_count && batch.data.seats_left > 0"
|
||||
class="text-sm bg-green-100 text-green-700 px-2 py-1 rounded-md"
|
||||
:class="
|
||||
batch.data.amount || batch.data.courses.length
|
||||
@@ -9,16 +9,16 @@
|
||||
: 'w-fit mb-4'
|
||||
"
|
||||
>
|
||||
{{ seats_left }}
|
||||
<span v-if="seats_left > 1">
|
||||
{{ batch.data.seats_left }}
|
||||
<span v-if="batch.data.seats_left > 1">
|
||||
{{ __('Seats Left') }}
|
||||
</span>
|
||||
<span v-else-if="seats_left == 1">
|
||||
<span v-else-if="batch.data.seats_left == 1">
|
||||
{{ __('Seat Left') }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="batch.data.seat_count && seats_left <= 0"
|
||||
v-else-if="batch.data.seat_count && batch.data.seats_left <= 0"
|
||||
class="text-xs bg-red-100 text-red-700 float-right px-2 py-0.5 rounded-md"
|
||||
>
|
||||
{{ __('Sold Out') }}
|
||||
@@ -54,6 +54,7 @@
|
||||
{{ batch.data.timezone }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="!readOnlyMode">
|
||||
<router-link
|
||||
v-if="canAccessBatch"
|
||||
@@ -190,15 +191,10 @@ const enrollInBatch = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const seats_left = computed(() => {
|
||||
if (props.batch.data?.seat_count) {
|
||||
return props.batch.data?.seat_count - props.batch.data?.students?.length
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const isStudent = computed(() => {
|
||||
return props.batch.data?.students?.includes(user.data?.name)
|
||||
return user.data
|
||||
? props.batch.data?.students?.includes(user.data?.name)
|
||||
: false
|
||||
})
|
||||
|
||||
const isModerator = computed(() => {
|
||||
@@ -218,6 +214,9 @@ const isInstructor = computed(() => {
|
||||
})
|
||||
|
||||
const canAccessBatch = computed(() => {
|
||||
if (!user.data) {
|
||||
return false
|
||||
}
|
||||
return isModerator.value || isStudent.value || isEvaluator.value
|
||||
})
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<ListRow
|
||||
:row="row"
|
||||
v-for="row in students.data"
|
||||
class="group cursor-pointer"
|
||||
class="group cursor-pointer hover:bg-surface-gray-2 rounded"
|
||||
@click="openStudentProgressModal(row)"
|
||||
>
|
||||
<template #default="{ column, item }">
|
||||
@@ -88,7 +88,7 @@
|
||||
</div>
|
||||
</template>
|
||||
</ListSelectBanner>
|
||||
<div class="mt-4" v-if="students.hasNextPage">
|
||||
<div class="mt-4 flex justify-center" v-if="students.hasNextPage">
|
||||
<Button @click="students.next()">
|
||||
{{ __('Load More') }}
|
||||
</Button>
|
||||
@@ -170,7 +170,7 @@ const studentColumns = [
|
||||
{
|
||||
label: 'Full Name',
|
||||
key: 'full_name',
|
||||
width: '20rem',
|
||||
width: '25rem',
|
||||
icon: 'user',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -19,9 +19,16 @@
|
||||
showOptions = true
|
||||
}
|
||||
"
|
||||
@click="
|
||||
(e) => {
|
||||
showOptions = true
|
||||
nextTick(() => {
|
||||
setFocus()
|
||||
})
|
||||
}
|
||||
"
|
||||
@focus="
|
||||
() => {
|
||||
showOptions = true
|
||||
if (!filterOptions.data || filterOptions.data.length === 0) {
|
||||
reload('')
|
||||
}
|
||||
@@ -33,10 +40,10 @@
|
||||
<template #body="{ isOpen, close }">
|
||||
<div v-show="isOpen">
|
||||
<div
|
||||
class="mt-1 rounded-lg bg-surface-white py-1 text-base border-2"
|
||||
class="flex flex-col mt-1 rounded-lg bg-surface-white py-1 text-base border-2 max-h-[13rem]"
|
||||
>
|
||||
<ComboboxOptions
|
||||
class="my-1 max-h-[12rem] overflow-y-auto px-1.5"
|
||||
class="flex-1 my-1 overflow-y-auto px-1.5"
|
||||
:class="options.length ? 'min-h-[6rem]' : 'min-h-[3.8rem]'"
|
||||
static
|
||||
>
|
||||
@@ -55,7 +62,11 @@
|
||||
>
|
||||
<div class="flex flex-col gap-1 p-1">
|
||||
<div class="text-base font-medium text-ink-gray-8">
|
||||
{{ option.description }}
|
||||
{{
|
||||
option.value == option.label
|
||||
? option.description
|
||||
: option.label
|
||||
}}
|
||||
</div>
|
||||
<div class="text-sm text-ink-gray-5">
|
||||
{{ option.value }}
|
||||
@@ -66,22 +77,19 @@
|
||||
<div v-else class="text-ink-gray-7 px-4">
|
||||
{{ __('No results found') }}
|
||||
</div>
|
||||
<div
|
||||
v-if="attrs.onCreate"
|
||||
class="absolute bottom-2 left-1 w-[95%] pt-2 bg-white border-t"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="w-full !justify-start"
|
||||
:label="__('Create New')"
|
||||
@click="attrs.onCreate(close)"
|
||||
>
|
||||
<template #prefix>
|
||||
<Plus class="h-4 w-4 stroke-1.5" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</ComboboxOptions>
|
||||
<div v-if="attrs.onCreate" class="px-1 pt-2 bg-white border-t">
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="w-full !justify-start"
|
||||
:label="__('Create New')"
|
||||
@click="attrs.onCreate(close)"
|
||||
>
|
||||
<template #prefix>
|
||||
<Plus class="h-4 w-4 stroke-1.5" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -115,7 +123,7 @@ import {
|
||||
} from '@headlessui/vue'
|
||||
import { createResource, Popover, Button } from 'frappe-ui'
|
||||
import { ref, computed, nextTick, useAttrs } from 'vue'
|
||||
import { watchDebounced } from '@vueuse/core'
|
||||
import { set, watchDebounced } from '@vueuse/core'
|
||||
import { X, Plus } from 'lucide-vue-next'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -149,18 +157,20 @@ const props = defineProps({
|
||||
|
||||
const values = defineModel()
|
||||
const attrs = useAttrs()
|
||||
const emails = ref([])
|
||||
const search = ref(null)
|
||||
const error = ref(null)
|
||||
const query = ref('')
|
||||
const text = ref('')
|
||||
const showOptions = ref(false)
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const selectedValue = computed({
|
||||
get: () => query.value || '',
|
||||
set: (val) => {
|
||||
query.value = ''
|
||||
val?.value && addValue(val.value)
|
||||
showOptions.value = false
|
||||
emit('update:modelValue', values.value)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -232,6 +242,7 @@ const addValue = (value) => {
|
||||
|
||||
const removeValue = (value) => {
|
||||
values.value = values.value.filter((v) => v !== value)
|
||||
emit('update:modelValue', values.value)
|
||||
}
|
||||
|
||||
function setFocus() {
|
||||
|
||||
@@ -34,7 +34,12 @@
|
||||
<img
|
||||
v-if="type == 'image'"
|
||||
:src="modelValue"
|
||||
class="border rounded-md w-44 h-auto"
|
||||
:class="[
|
||||
'border object-cover',
|
||||
shape === 'circle'
|
||||
? 'w-20 h-20 rounded-full'
|
||||
: 'w-44 h-auto min-h-20 rounded-md',
|
||||
]"
|
||||
/>
|
||||
<video v-else controls class="border rounded-md w-44 h-auto">
|
||||
<source :src="modelValue" />
|
||||
@@ -67,11 +72,12 @@ const emit = defineEmits<{
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: string
|
||||
modelValue: string | null
|
||||
label?: string
|
||||
description?: string
|
||||
type?: 'image' | 'video'
|
||||
required?: boolean
|
||||
shape?: 'square' | 'circle'
|
||||
}>(),
|
||||
{
|
||||
modelValue: '',
|
||||
@@ -79,6 +85,7 @@ const props = withDefaults(
|
||||
description: '',
|
||||
type: 'image',
|
||||
required: true,
|
||||
shape: 'square',
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<CertificationLinks :courseName="course.data.name" class="w-full" />
|
||||
</div>
|
||||
<router-link
|
||||
v-else-if="course.data.paid_course"
|
||||
v-else-if="course.data.paid_course && !isAdmin"
|
||||
:to="{
|
||||
name: 'Billing',
|
||||
params: {
|
||||
@@ -56,14 +56,15 @@
|
||||
</Button>
|
||||
</router-link>
|
||||
<Badge
|
||||
v-else-if="course.data.disable_self_learning"
|
||||
v-else-if="course.data.disable_self_learning && !isAdmin"
|
||||
theme="blue"
|
||||
size="lg"
|
||||
class="mb-4"
|
||||
>
|
||||
{{ __('Contact the Administrator to enroll for this course.') }}
|
||||
{{ __('Contact the Administrator to enroll for this course') }}
|
||||
</Badge>
|
||||
<Button
|
||||
v-else-if="!user.data?.is_moderator && !is_instructor()"
|
||||
v-else-if="!isAdmin"
|
||||
@click="enrollStudent()"
|
||||
variant="solid"
|
||||
class="w-full"
|
||||
@@ -88,40 +89,11 @@
|
||||
</template>
|
||||
{{ __('Get Certificate') }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="user.data?.is_moderator || is_instructor()"
|
||||
class="w-full mt-2"
|
||||
size="md"
|
||||
@click="showProgressSummary"
|
||||
>
|
||||
<template #prefix>
|
||||
<TrendingUp class="size-4 stroke-1.5" />
|
||||
{{ __('Progress Summary') }}
|
||||
</template>
|
||||
</Button>
|
||||
<router-link
|
||||
v-if="user?.data?.is_moderator || is_instructor()"
|
||||
:to="{
|
||||
name: 'CourseForm',
|
||||
params: {
|
||||
courseName: course.data.name,
|
||||
},
|
||||
}"
|
||||
>
|
||||
<Button variant="subtle" class="w-full mt-2" size="md">
|
||||
<template #prefix>
|
||||
<Pencil class="size-4 stroke-1.5" />
|
||||
</template>
|
||||
<span>
|
||||
{{ __('Edit') }}
|
||||
</span>
|
||||
</Button>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div
|
||||
class="font-medium text-ink-gray-9"
|
||||
:class="{ 'mt-8': !readOnlyMode }"
|
||||
:class="{ 'mt-8': course.data.membership && !readOnlyMode }"
|
||||
>
|
||||
{{ __('This course has:') }}
|
||||
</div>
|
||||
@@ -168,12 +140,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CourseProgressSummary
|
||||
v-if="user.data?.is_moderator || is_instructor()"
|
||||
v-model="showProgressModal"
|
||||
:courseName="course.data.name"
|
||||
:enrollments="course.data.enrollments"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import {
|
||||
@@ -191,12 +157,10 @@ import { Badge, Button, call, createResource, toast } from 'frappe-ui'
|
||||
import { formatAmount } from '@/utils/'
|
||||
import { useRouter } from 'vue-router'
|
||||
import CertificationLinks from '@/components/CertificationLinks.vue'
|
||||
import CourseProgressSummary from '@/components/Modals/CourseProgressSummary.vue'
|
||||
import { useTelemetry } from 'frappe-ui/frappe'
|
||||
|
||||
const router = useRouter()
|
||||
const user = inject('$user')
|
||||
const showProgressModal = ref(false)
|
||||
const readOnlyMode = window.read_only_mode
|
||||
const { capture } = useTelemetry()
|
||||
|
||||
@@ -216,7 +180,7 @@ const video_link = computed(() => {
|
||||
|
||||
function enrollStudent() {
|
||||
if (!user.data) {
|
||||
toast.success(__('You need to login first to enroll for this course'))
|
||||
toast.warning(__('You need to login first to enroll for this course'))
|
||||
setTimeout(() => {
|
||||
window.location.href = `/login?redirect-to=${window.location.pathname}`
|
||||
}, 500)
|
||||
@@ -295,7 +259,7 @@ const fetchCertificate = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const showProgressSummary = () => {
|
||||
showProgressModal.value = true
|
||||
}
|
||||
const isAdmin = computed(() => {
|
||||
return user.data?.is_moderator || is_instructor()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -15,7 +15,10 @@
|
||||
{{ __(title) }}
|
||||
</div>
|
||||
<Button size="sm" v-if="allowEdit" @click="openChapterModal()">
|
||||
{{ __('Add Chapter') }}
|
||||
<template #prefix>
|
||||
<Plus class="size-4 stroke-1.5" />
|
||||
</template>
|
||||
{{ __('Add') }}
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
@@ -174,6 +177,7 @@ import {
|
||||
FilePenLine,
|
||||
HelpCircle,
|
||||
MonitorPlay,
|
||||
Plus,
|
||||
Trash2,
|
||||
} from 'lucide-vue-next'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
@@ -107,7 +107,11 @@
|
||||
v-model:reloadLiveClasses="liveClasses"
|
||||
/>
|
||||
|
||||
<LiveClassAttendance v-model="showAttendance" :live_class="attendanceFor" />
|
||||
<LiveClassAttendance
|
||||
v-if="showAttendance"
|
||||
v-model="showAttendance"
|
||||
:live_class="attendanceFor"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import { createListResource, Button, Tooltip } from 'frappe-ui'
|
||||
|
||||
@@ -23,10 +23,8 @@
|
||||
(value, close) => {
|
||||
close()
|
||||
router.push({
|
||||
name: 'CourseForm',
|
||||
params: {
|
||||
courseName: 'new',
|
||||
},
|
||||
name: 'Courses',
|
||||
query: { newCourse: '1' },
|
||||
})
|
||||
}
|
||||
"
|
||||
|
||||
@@ -1,231 +0,0 @@
|
||||
<template>
|
||||
<Dialog
|
||||
v-model="show"
|
||||
:options="{
|
||||
title: __('Course Progress Summary'),
|
||||
size: '5xl',
|
||||
}"
|
||||
>
|
||||
<template #body-content>
|
||||
<div
|
||||
class="flex flex-col-reverse md:flex-row justify-between md:space-x-10 text-base mt-10"
|
||||
>
|
||||
<div class="w-full">
|
||||
<div class="flex items-center justify-between space-x-5 mb-4">
|
||||
<FormControl
|
||||
v-model="searchFilter"
|
||||
:placeholder="__('Search by Member')"
|
||||
type="text"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="max-h-[70vh] overflow-y-auto">
|
||||
<ListView
|
||||
v-if="progressList.loading || progressList.data?.length"
|
||||
:columns="progressColumns"
|
||||
:rows="progressList.data"
|
||||
rowKey="name"
|
||||
:options="{
|
||||
selectable: false,
|
||||
showTooltip: 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 progressColumns"
|
||||
:key="item.key"
|
||||
>
|
||||
<template #prefix="{ item }">
|
||||
<FeatherIcon
|
||||
:name="item.icon?.toString()"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
</template>
|
||||
</ListHeaderItem>
|
||||
</ListHeader>
|
||||
<ListRows v-for="row in progressList.data">
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'Profile',
|
||||
params: { username: row.member_username },
|
||||
}"
|
||||
>
|
||||
<ListRow :row="row">
|
||||
<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>
|
||||
{{ row[column.key].toString() }}
|
||||
</div>
|
||||
</ListRowItem>
|
||||
</template>
|
||||
</ListRow>
|
||||
</router-link>
|
||||
</ListRows>
|
||||
</ListView>
|
||||
<div
|
||||
v-if="progressList.data && progressList.hasNextPage"
|
||||
class="flex justify-center my-5"
|
||||
>
|
||||
<Button @click="progressList.next()">
|
||||
{{ __('Load More') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4 self-start w-full space-y-5">
|
||||
<div
|
||||
class="flex flex-col md:flex-row items-center space-y-2 md:space-y-0 md:space-x-4"
|
||||
>
|
||||
<NumberChart
|
||||
class="border rounded-md w-full"
|
||||
:config="{
|
||||
title: __('Enrollments'),
|
||||
value: memberCount || 0,
|
||||
}"
|
||||
/>
|
||||
<NumberChart
|
||||
class="border rounded-md w-full"
|
||||
:config="{
|
||||
title: __('Average Progress %'),
|
||||
value: chartDetails.data?.average_progress || 0,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<DonutChart
|
||||
:config="{
|
||||
data: chartDetails.data?.progress_distribution || [],
|
||||
title: __('Progress Distribution'),
|
||||
categoryColumn: 'category',
|
||||
valueColumn: 'count',
|
||||
colors: [
|
||||
getColor('red', 400),
|
||||
getColor('amber', 400),
|
||||
getColor('pink', 400),
|
||||
getColor('blue', 400),
|
||||
getColor('green', 400),
|
||||
],
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
createListResource,
|
||||
createResource,
|
||||
Dialog,
|
||||
DonutChart,
|
||||
FeatherIcon,
|
||||
FormControl,
|
||||
ListView,
|
||||
ListHeader,
|
||||
ListHeaderItem,
|
||||
ListRows,
|
||||
ListRow,
|
||||
ListRowItem,
|
||||
NumberChart,
|
||||
} from 'frappe-ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { getColor } from '@/utils'
|
||||
|
||||
const show = defineModel<boolean>({ default: false })
|
||||
const searchFilter = ref<string | null>(null)
|
||||
type Filters = {
|
||||
course: string | undefined
|
||||
|
||||
member_name?: string[]
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
courseName?: string
|
||||
enrollments?: number
|
||||
}>()
|
||||
|
||||
const memberCount = ref<number>(props.enrollments || 0)
|
||||
|
||||
const chartDetails = createResource({
|
||||
url: 'lms.lms.api.get_course_progress_distribution',
|
||||
params: {
|
||||
course: props.courseName,
|
||||
},
|
||||
auto: true,
|
||||
})
|
||||
|
||||
const progressList = createListResource({
|
||||
doctype: 'LMS Enrollment',
|
||||
filters: {
|
||||
course: props.courseName,
|
||||
},
|
||||
fields: [
|
||||
'name',
|
||||
'member',
|
||||
'member_name',
|
||||
'member_image',
|
||||
'member_username',
|
||||
'progress',
|
||||
],
|
||||
pageLength: 50,
|
||||
auto: true,
|
||||
})
|
||||
|
||||
watch([searchFilter], () => {
|
||||
let filterApplied = false
|
||||
let filters: Filters = {
|
||||
course: props.courseName,
|
||||
}
|
||||
|
||||
if (searchFilter.value) {
|
||||
filters.member_name = ['like', `%${searchFilter.value}%`]
|
||||
filterApplied = true
|
||||
}
|
||||
|
||||
progressList.update({
|
||||
filters: filters,
|
||||
})
|
||||
progressList.reload(
|
||||
{},
|
||||
{
|
||||
onSuccess(data: any[]) {
|
||||
memberCount.value = filterApplied ? data.length : props.enrollments || 0
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const progressColumns = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: __('Member'),
|
||||
key: 'member_name',
|
||||
width: '60%',
|
||||
icon: 'user',
|
||||
},
|
||||
{
|
||||
label: __('Progress'),
|
||||
key: 'progress',
|
||||
align: 'right',
|
||||
icon: 'trending-up',
|
||||
},
|
||||
]
|
||||
})
|
||||
</script>
|
||||
@@ -1,17 +1,25 @@
|
||||
<template>
|
||||
<Dialog
|
||||
v-model="show"
|
||||
:options="{
|
||||
size: '3xl',
|
||||
}"
|
||||
>
|
||||
<template #body-header>
|
||||
<div class="flex items-center mb-5">
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<div class="text-2xl font-semibold leading-6 text-ink-gray-9">
|
||||
{{ __('Edit Profile') }}
|
||||
</div>
|
||||
<Badge v-if="isDirty" class="ml-4" theme="orange">
|
||||
{{ __('Not Saved') }}
|
||||
</Badge>
|
||||
<div class="space-x-2">
|
||||
<Badge v-if="isDirty" theme="orange">
|
||||
{{ __('Not Saved') }}
|
||||
</Badge>
|
||||
<div class="pb-5 float-right">
|
||||
<Button variant="solid" @click="saveProfile()">
|
||||
{{ __('Save') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #body-content>
|
||||
@@ -19,52 +27,13 @@
|
||||
<div class="grid grid-cols-2 gap-10">
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<div class="text-xs text-ink-gray-5 mb-1">
|
||||
{{ __('Profile Image') }}
|
||||
</div>
|
||||
<FileUploader
|
||||
v-if="!profile.image"
|
||||
:fileTypes="['image/*']"
|
||||
:validateFile="validateFile"
|
||||
@success="(file) => saveImage(file)"
|
||||
>
|
||||
<template
|
||||
v-slot="{ file, progress, uploading, openFileSelector }"
|
||||
>
|
||||
<div class="mb-4">
|
||||
<Button @click="openFileSelector" :loading="uploading">
|
||||
{{
|
||||
uploading
|
||||
? `Uploading ${progress}%`
|
||||
: 'Upload a profile image'
|
||||
}}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</FileUploader>
|
||||
<div v-else class="mb-4">
|
||||
<div class="flex items-center">
|
||||
<img
|
||||
:src="profile.image?.file_url"
|
||||
class="object-cover h-[50px] w-[50px] rounded-full border-4 border-white object-cover"
|
||||
/>
|
||||
<Uploader
|
||||
v-model="profile.image"
|
||||
:label="__('Profile Image')"
|
||||
:required="true"
|
||||
shape="circle"
|
||||
/>
|
||||
|
||||
<div class="text-base flex flex-col ml-2">
|
||||
<span>
|
||||
{{ profile.image?.file_name }}
|
||||
</span>
|
||||
<span class="text-sm text-ink-gray-4 mt-1">
|
||||
{{ getFileSize(profile.image?.file_size) }}
|
||||
</span>
|
||||
</div>
|
||||
<X
|
||||
@click="removeImage()"
|
||||
class="bg-surface-gray-3 rounded-md cursor-pointer stroke-1.5 w-5 h-5 p-1 ml-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FormControl
|
||||
v-model="profile.first_name"
|
||||
:label="__('First Name')"
|
||||
@@ -90,7 +59,7 @@
|
||||
<FormControl
|
||||
v-model="profile.open_to"
|
||||
type="select"
|
||||
:options="[' ', 'Opportunities', 'Hiring']"
|
||||
:options="[' ', 'Work', 'Hiring']"
|
||||
:label="__('Open to')"
|
||||
:placeholder="__('Looking for new work or hiring talent?')"
|
||||
/>
|
||||
@@ -115,13 +84,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions="{ close }">
|
||||
<div class="pb-5 float-right">
|
||||
<Button variant="solid" @click="saveProfile(close)">
|
||||
{{ __('Save') }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
@@ -131,15 +93,14 @@ import {
|
||||
createResource,
|
||||
Dialog,
|
||||
FormControl,
|
||||
FileUploader,
|
||||
TextEditor,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { X } from 'lucide-vue-next'
|
||||
import { getFileSize, sanitizeHTML } from '@/utils'
|
||||
import { sanitizeHTML } from '@/utils'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
|
||||
const show = defineModel()
|
||||
const reloadProfile = defineModel('reloadProfile')
|
||||
const hasLanguageChanged = ref(false)
|
||||
const isDirty = ref(false)
|
||||
@@ -163,19 +124,6 @@ const profile = reactive({
|
||||
twitter: '',
|
||||
})
|
||||
|
||||
const imageResource = createResource({
|
||||
url: 'lms.lms.api.get_file_info',
|
||||
makeParams(values) {
|
||||
return {
|
||||
file_url: values.image,
|
||||
}
|
||||
},
|
||||
auto: false,
|
||||
onSuccess(data) {
|
||||
profile.image = data
|
||||
},
|
||||
})
|
||||
|
||||
const updateProfile = createResource({
|
||||
url: 'frappe.client.set_value',
|
||||
makeParams(values) {
|
||||
@@ -183,7 +131,7 @@ const updateProfile = createResource({
|
||||
doctype: 'User',
|
||||
name: props.profile.data.name,
|
||||
fieldname: {
|
||||
user_image: profile.image?.file_url || null,
|
||||
user_image: profile.image || null,
|
||||
...profile,
|
||||
},
|
||||
}
|
||||
@@ -193,13 +141,13 @@ const updateProfile = createResource({
|
||||
},
|
||||
})
|
||||
|
||||
const saveProfile = (close) => {
|
||||
const saveProfile = () => {
|
||||
profile.bio = sanitizeHTML(profile.bio)
|
||||
updateProfile.submit(
|
||||
{},
|
||||
{
|
||||
onSuccess() {
|
||||
close()
|
||||
show.value = false
|
||||
reloadProfile.value.reload()
|
||||
if (hasLanguageChanged.value) {
|
||||
hasLanguageChanged.value = false
|
||||
@@ -213,21 +161,6 @@ const saveProfile = (close) => {
|
||||
)
|
||||
}
|
||||
|
||||
const validateFile = (file) => {
|
||||
let extension = file.name.split('.').pop().toLowerCase()
|
||||
if (!['jpg', 'jpeg', 'png'].includes(extension)) {
|
||||
return 'Only image file is allowed.'
|
||||
}
|
||||
}
|
||||
|
||||
const saveImage = (file) => {
|
||||
profile.image = file
|
||||
}
|
||||
|
||||
const removeImage = () => {
|
||||
profile.image = null
|
||||
}
|
||||
|
||||
watch(
|
||||
() => profile,
|
||||
(newVal) => {
|
||||
@@ -240,7 +173,7 @@ watch(
|
||||
return
|
||||
}
|
||||
}
|
||||
if (profile.image?.file_url !== props.profile.data.user_image) {
|
||||
if (profile.image !== props.profile.data.user_image) {
|
||||
isDirty.value = true
|
||||
return
|
||||
}
|
||||
@@ -262,7 +195,7 @@ watch(
|
||||
profile.linkedin = newVal.linkedin
|
||||
profile.github = newVal.github
|
||||
profile.twitter = newVal.twitter
|
||||
if (newVal.user_image) imageResource.submit({ image: newVal.user_image })
|
||||
profile.image = newVal.user_image
|
||||
isDirty.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,10 @@
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip :text="__('Course')">
|
||||
<div class="flex items-center space-x-2 w-fit">
|
||||
<div
|
||||
class="flex space-x-2 w-fit cursor-pointer"
|
||||
@click="openLink('course', event.course)"
|
||||
>
|
||||
<BookOpen class="h-4 w-4 stroke-1.5" />
|
||||
<span>
|
||||
{{ event.course_title }}
|
||||
@@ -30,7 +33,10 @@
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip v-if="event.batch_title" :text="__('Batch')">
|
||||
<div class="flex items-center space-x-2 w-fit">
|
||||
<div
|
||||
class="flex space-x-2 w-fit cursor-pointer"
|
||||
@click="openLink('batch', event.batch_name)"
|
||||
>
|
||||
<Users class="h-4 w-4 stroke-1.5" />
|
||||
<span>
|
||||
{{ event.batch_title }}
|
||||
@@ -334,7 +340,7 @@ const certificateDetails = createResource({
|
||||
}
|
||||
},
|
||||
onError(err) {
|
||||
certificate.template = defaultTemplate.data.value
|
||||
certificate.template = defaultTemplate.data?.value
|
||||
},
|
||||
auto: false,
|
||||
})
|
||||
@@ -377,6 +383,16 @@ const openCertificate = (certificate) => {
|
||||
)
|
||||
}
|
||||
|
||||
const openLink = (type, name) => {
|
||||
let url = ''
|
||||
if (type === 'course') {
|
||||
url = `/lms/courses/${name}`
|
||||
} else if (type === 'batch') {
|
||||
url = `/lms/batches/${name}#students`
|
||||
}
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
const statusOptions = computed(() => {
|
||||
return [
|
||||
{
|
||||
|
||||
20
frontend/src/components/NumberChartGraph.vue
Normal file
20
frontend/src/components/NumberChartGraph.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="border rounded-lg p-3 space-y-2">
|
||||
<div class="text-ink-gray-5">
|
||||
{{ __(title) }}
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<slot name="prefix" />
|
||||
<div class="font-semibold text-2xl">
|
||||
{{ value }}
|
||||
</div>
|
||||
<slot name="suffix" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
title: string
|
||||
value: number | string
|
||||
}>()
|
||||
</script>
|
||||
@@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<Tooltip :text="`${props.progress}%`">
|
||||
<div class="w-full bg-surface-gray-3 rounded-full h-1">
|
||||
<div
|
||||
class="w-full bg-surface-gray-3 rounded-full h-1"
|
||||
:class="$attrs.class"
|
||||
>
|
||||
<div
|
||||
class="bg-surface-gray-7 rounded-full"
|
||||
:class="progressBarHeight"
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
<template>
|
||||
<div class="flex flex-col justify-between h-full">
|
||||
<div class="flex flex-col h-full">
|
||||
<div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="font-semibold mb-1 text-ink-gray-9">
|
||||
{{ __(label) }}
|
||||
</div>
|
||||
<Badge
|
||||
v-if="isDirty"
|
||||
:label="__('Not Saved')"
|
||||
variant="subtle"
|
||||
theme="orange"
|
||||
/>
|
||||
<div class="space-x-2">
|
||||
<Badge
|
||||
v-if="isDirty"
|
||||
:label="__('Not Saved')"
|
||||
variant="subtle"
|
||||
theme="orange"
|
||||
/>
|
||||
<Button
|
||||
variant="solid"
|
||||
:loading="saveSettings.loading"
|
||||
@click="update"
|
||||
>
|
||||
{{ __('Update') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-ink-gray-5">
|
||||
{{ __(description) }}
|
||||
@@ -19,11 +28,6 @@
|
||||
<div class="overflow-y-auto">
|
||||
<SettingFields :sections="sections" :data="branding.data" />
|
||||
</div>
|
||||
<div class="flex flex-row-reverse mt-auto">
|
||||
<Button variant="solid" :loading="saveSettings.loading" @click="update">
|
||||
{{ __('Update') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
|
||||
@@ -186,8 +186,9 @@ const openProfile = (username: string) => {
|
||||
}
|
||||
|
||||
const deleteEvaluator = (evaluator: string) => {
|
||||
call('lms.lms.api.delete_evaluator', {
|
||||
evaluator: evaluator,
|
||||
call('frappe.client.delete', {
|
||||
doctype: 'Course Evaluator',
|
||||
name: evaluator,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(__('Evaluator deleted successfully'))
|
||||
|
||||
@@ -2,21 +2,25 @@
|
||||
<Dialog
|
||||
v-model="show"
|
||||
:options="{
|
||||
title:
|
||||
gatewayID === 'new'
|
||||
? __('New Payment Gateway')
|
||||
: __('Edit Payment Gateway'),
|
||||
size: '3xl',
|
||||
}"
|
||||
>
|
||||
<template #body-header>
|
||||
<div class="text-lg font-semibold">
|
||||
{{
|
||||
gatewayID === 'new'
|
||||
? __('New Payment Gateway')
|
||||
: __('Edit Payment Gateway')
|
||||
}}
|
||||
</div>
|
||||
</template>
|
||||
<template #body-content>
|
||||
<SettingFields
|
||||
v-if="gatewayID != 'new' && paymentGateway.data"
|
||||
:fields="paymentGateway.data.fields"
|
||||
:sections="paymentGateway.data.sections"
|
||||
:data="paymentGateway.data.data"
|
||||
class="pt-5 my-0"
|
||||
/>
|
||||
<div v-else>
|
||||
<div v-else class="mt-5">
|
||||
<FormControl
|
||||
v-model="newGateway"
|
||||
:label="__('Select Payment Gateway')"
|
||||
@@ -26,9 +30,8 @@
|
||||
/>
|
||||
<SettingFields
|
||||
v-if="newGateway"
|
||||
:fields="newGatewayFields"
|
||||
:sections="newGatewayFields"
|
||||
:data="newGatewayData"
|
||||
class="pt-5 my-0"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -56,7 +59,7 @@ import SettingFields from '@/components/Settings/SettingFields.vue'
|
||||
const show = defineModel<boolean>({ required: true, default: false })
|
||||
const paymentGateways = defineModel<any>('paymentGateways')
|
||||
const newGateway = ref(null)
|
||||
const newGatewayFields = ref([])
|
||||
const newGatewayFields = ref<{ columns: { fields: any[] }[] }[]>([])
|
||||
const newGatewayData = ref<Record<string, any>>({})
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -72,6 +75,7 @@ const paymentGateway = createResource({
|
||||
},
|
||||
transform(data: any) {
|
||||
arrangeFields(data.fields)
|
||||
data.sections = makeSections(data.fields)
|
||||
return data
|
||||
},
|
||||
})
|
||||
@@ -102,10 +106,6 @@ const arrangeFields = (fields: any[]) => {
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
fields.splice(3, 0, {
|
||||
type: 'Column Break',
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
@@ -130,7 +130,7 @@ watch(newGateway, () => {
|
||||
gatewayFields.reload({ doctype: gatewayDoc.name }).then(() => {
|
||||
let fields = gatewayFields.data || []
|
||||
arrangeFields(fields)
|
||||
newGatewayFields.value = fields
|
||||
newGatewayFields.value = makeSections(fields)
|
||||
prepareGatewayData()
|
||||
})
|
||||
})
|
||||
@@ -192,19 +192,6 @@ const getGatewayFields = () => {
|
||||
}, {})
|
||||
}
|
||||
|
||||
const createGatewayRecord = (gatewayDoc: any, data: any = {}) => {
|
||||
call('frappe.client.insert', {
|
||||
doc: {
|
||||
doctype: 'Payment Gateway',
|
||||
gateway: newGateway.value,
|
||||
gateway_controller: gatewayDoc.issingle ? '' : gatewayDoc.name,
|
||||
gateway_settings: gatewayDoc.issingle ? '' : data.name,
|
||||
},
|
||||
}).then(() => {
|
||||
paymentGateways.value?.reload()
|
||||
})
|
||||
}
|
||||
|
||||
const allGatewayOptions = computed(() => {
|
||||
let options: string[] = []
|
||||
let gatewayList = allGateways.data?.map((gateway: any) => gateway.name) || []
|
||||
@@ -230,4 +217,20 @@ const prepareGatewayData = () => {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const makeSections = (fields: any[]) => {
|
||||
const columnCount = fields.length / 3
|
||||
let sections: { columns: { fields: any[] }[] }[] = [
|
||||
{
|
||||
columns: [],
|
||||
},
|
||||
]
|
||||
|
||||
for (let i = 0; i < columnCount; i++) {
|
||||
sections[0].columns.push({
|
||||
fields: fields.slice(i * 3, i * 3 + 3),
|
||||
})
|
||||
}
|
||||
return sections
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -8,22 +8,24 @@
|
||||
<h1 class="mb-3 px-2 pt-2 text-lg font-semibold text-ink-gray-9">
|
||||
{{ __('Settings') }}
|
||||
</h1>
|
||||
<div v-for="tab in tabs" :key="tab.label">
|
||||
<div
|
||||
v-if="!tab.hideLabel"
|
||||
class="mb-2 mt-3 flex cursor-pointer gap-1.5 px-1 text-base text-ink-gray-5 transition-all duration-300 ease-in-out"
|
||||
>
|
||||
<span>{{ __(tab.label) }}</span>
|
||||
</div>
|
||||
<nav class="space-y-1">
|
||||
<div v-for="item in tab.items" @click="activeTab = item">
|
||||
<SidebarLink
|
||||
:link="item"
|
||||
:key="item.label"
|
||||
:activeTab="activeTab?.label"
|
||||
/>
|
||||
<div class="space-y-5">
|
||||
<div v-for="tab in tabs" :key="tab.label">
|
||||
<div
|
||||
v-if="!tab.hideLabel"
|
||||
class="mb-2 mt-3 flex cursor-pointer gap-1.5 px-1 text-base text-ink-gray-5 transition-all duration-300 ease-in-out"
|
||||
>
|
||||
<span>{{ __(tab.label) }}</span>
|
||||
</div>
|
||||
</nav>
|
||||
<nav class="space-y-1">
|
||||
<div v-for="item in tab.items" @click="activeTab = item">
|
||||
<SidebarLink
|
||||
:link="item"
|
||||
:key="item.label"
|
||||
:activeTab="activeTab?.label"
|
||||
/>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -1,12 +1,33 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full text-base">
|
||||
<div class="flex items-center space-x-2 mb-8 -ml-1.5">
|
||||
<ChevronLeft
|
||||
class="size-5 stroke-1.5 text-ink-gray-7 cursor-pointer"
|
||||
@click="emit('updateStep', 'list')"
|
||||
/>
|
||||
<div class="text-xl font-semibold text-ink-gray-9">
|
||||
{{ __('Transaction Details') }}
|
||||
<div class="flex items-center justify-between mb-10 -ml-1.5">
|
||||
<div class="flex items-center space-x-2">
|
||||
<ChevronLeft
|
||||
class="size-5 stroke-1.5 text-ink-gray-7 cursor-pointer"
|
||||
@click="emit('updateStep', 'list')"
|
||||
/>
|
||||
<div class="text-xl font-semibold text-ink-gray-9">
|
||||
{{ __('Transaction Details') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<Button
|
||||
v-if="
|
||||
transactionData?.payment_for_document_type &&
|
||||
transactionData?.payment_for_document
|
||||
"
|
||||
@click="openDetails()"
|
||||
>
|
||||
{{ __('Open the ') }}
|
||||
{{
|
||||
transactionData.payment_for_document_type == 'LMS Course'
|
||||
? __('Course')
|
||||
: __('Batch')
|
||||
}}
|
||||
</Button>
|
||||
<Button variant="solid" @click="saveTransaction()">
|
||||
{{ __('Save') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="transactionData" class="overflow-y-auto">
|
||||
@@ -21,6 +42,12 @@
|
||||
type="checkbox"
|
||||
v-model="transactionData.payment_for_certificate"
|
||||
/>
|
||||
<FormControl
|
||||
:label="__('Member Consent')"
|
||||
type="checkbox"
|
||||
v-model="transactionData.member_consent"
|
||||
:disabled="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-5 mt-5">
|
||||
@@ -28,22 +55,27 @@
|
||||
:label="__('Member')"
|
||||
doctype="User"
|
||||
v-model="transactionData.member"
|
||||
:required="true"
|
||||
/>
|
||||
<FormControl
|
||||
:label="__('Billing Name')"
|
||||
v-model="transactionData.billing_name"
|
||||
:required="true"
|
||||
/>
|
||||
<Link
|
||||
:label="__('Source')"
|
||||
v-model="transactionData.source"
|
||||
doctype="LMS Source"
|
||||
/>
|
||||
<Link
|
||||
<FormControl
|
||||
type="select"
|
||||
:options="documentTypeOptions"
|
||||
:label="__('Payment For Document Type')"
|
||||
v-model="transactionData.payment_for_document_type"
|
||||
doctype="DocType"
|
||||
/>
|
||||
<Link
|
||||
v-if="transactionData.payment_for_document_type"
|
||||
:label="__('Payment For Document')"
|
||||
v-model="transactionData.payment_for_document"
|
||||
:doctype="transactionData.payment_for_document_type"
|
||||
@@ -58,8 +90,13 @@
|
||||
:label="__('Currency')"
|
||||
v-model="transactionData.currency"
|
||||
doctype="Currency"
|
||||
:required="true"
|
||||
/>
|
||||
<FormControl
|
||||
:label="__('Amount')"
|
||||
v-model="transactionData.amount"
|
||||
:required="true"
|
||||
/>
|
||||
<FormControl :label="__('Amount')" v-model="transactionData.amount" />
|
||||
<FormControl
|
||||
v-if="transactionData.amount_with_gst"
|
||||
:label="__('Amount with GST')"
|
||||
@@ -103,6 +140,7 @@
|
||||
:label="__('Address')"
|
||||
v-model="transactionData.address"
|
||||
doctype="Address"
|
||||
:required="true"
|
||||
/>
|
||||
<FormControl :label="__('GSTIN')" v-model="transactionData.gstin" />
|
||||
<FormControl :label="__('PAN')" v-model="transactionData.pan" />
|
||||
@@ -116,25 +154,12 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-x-2 mt-auto ml-auto">
|
||||
<Button @click="openDetails()">
|
||||
{{ __('Open the ') }}
|
||||
{{
|
||||
data.payment_for_document_type == 'LMS Course'
|
||||
? __('Course')
|
||||
: __('Batch')
|
||||
}}
|
||||
</Button>
|
||||
<Button variant="solid" @click="saveTransaction()">
|
||||
{{ __('Save') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { Button, FormControl } from 'frappe-ui'
|
||||
import { Button, FormControl, toast } from 'frappe-ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ref, watch } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { ChevronLeft } from 'lucide-vue-next'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
|
||||
@@ -148,21 +173,40 @@ const props = defineProps<{
|
||||
data: any
|
||||
}>()
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(newVal) => {
|
||||
transactionData.value = newVal ? { ...newVal } : null
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
const saveTransaction = () => {
|
||||
if (props.data?.name) {
|
||||
updateTransaction()
|
||||
} else {
|
||||
createTransaction()
|
||||
}
|
||||
}
|
||||
|
||||
const saveTransaction = (close: () => void) => {
|
||||
props.transactions.value.setValue
|
||||
const createTransaction = () => {
|
||||
console.log(props.transactions)
|
||||
props.transactions.insert
|
||||
.submit({
|
||||
...transactionData.value,
|
||||
})
|
||||
.then(() => {
|
||||
close()
|
||||
toast.success(__('Transaction created successfully'))
|
||||
})
|
||||
.catch((err: any) => {
|
||||
toast.error(__(err.messages?.[0] || err))
|
||||
console.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
const updateTransaction = () => {
|
||||
props.transactions.setValue
|
||||
.submit({
|
||||
...transactionData.value,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(__('Transaction updated successfully'))
|
||||
})
|
||||
.catch((err: any) => {
|
||||
toast.error(__(err.messages?.[0] || err))
|
||||
console.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -181,4 +225,48 @@ const openDetails = () => {
|
||||
show.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const emptyTransactionData = {
|
||||
payment_received: false,
|
||||
payment_for_certificate: false,
|
||||
member: null,
|
||||
billing_name: null,
|
||||
source: null,
|
||||
payment_for_document_type: null,
|
||||
payment_for_document: null,
|
||||
member_consent: false,
|
||||
currency: null,
|
||||
amount: null,
|
||||
amount_with_gst: null,
|
||||
coupon: null,
|
||||
coupon_code: null,
|
||||
discount_amount: null,
|
||||
original_amount: null,
|
||||
order_id: null,
|
||||
payment_id: null,
|
||||
gstin: null,
|
||||
pan: null,
|
||||
address: null,
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(newVal) => {
|
||||
transactionData.value = newVal ? { ...newVal } : emptyTransactionData
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const documentTypeOptions = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: __('Course'),
|
||||
value: 'LMS Course',
|
||||
},
|
||||
{
|
||||
label: __('Batch'),
|
||||
value: 'LMS Batch',
|
||||
},
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
<template>
|
||||
<div class="flex min-h-0 flex-col text-base">
|
||||
<div class="mb-5">
|
||||
<div class="text-xl font-semibold mb-1 text-ink-gray-9">
|
||||
{{ __(label) }}
|
||||
</div>
|
||||
<div class="text-ink-gray-6 leading-5">
|
||||
{{ __(description) }}
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<div>
|
||||
<div class="text-xl font-semibold mb-1 text-ink-gray-9">
|
||||
{{ __(label) }}
|
||||
</div>
|
||||
<div class="text-ink-gray-6 leading-5">
|
||||
{{ __(description) }}
|
||||
</div>
|
||||
</div>
|
||||
<Button @click="emit('updateStep', 'new', null)">
|
||||
<template #prefix>
|
||||
<FeatherIcon name="plus" class="h-4 w-4 stroke-1.5" />
|
||||
</template>
|
||||
{{ __('Add Transaction') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-5 mb-4">
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
<template>
|
||||
<TransactionDetails
|
||||
v-if="step == 'new'"
|
||||
:transactions="transactions"
|
||||
:data="data"
|
||||
v-model:show="show"
|
||||
@updateStep="updateStep"
|
||||
/>
|
||||
<TransactionList
|
||||
v-if="step === 'list'"
|
||||
v-else-if="step === 'list'"
|
||||
:label="props.label"
|
||||
:description="props.description"
|
||||
:transactions="transactions"
|
||||
@@ -33,6 +40,8 @@ const updateStep = (newStep: 'list' | 'new' | 'edit', newData: any) => {
|
||||
step.value = newStep
|
||||
if (newData) {
|
||||
data.value = newData
|
||||
} else {
|
||||
data.value = null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -269,12 +269,13 @@ const iconProps = {
|
||||
onMounted(() => {
|
||||
setUpOnboarding()
|
||||
addKeyboardShortcut()
|
||||
updateSidebarLinks()
|
||||
socket.on('publish_lms_notifications', (data) => {
|
||||
unreadNotifications.reload()
|
||||
})
|
||||
})
|
||||
|
||||
const setSidebarLinks = () => {
|
||||
const updateSidebarLinksVisibility = () => {
|
||||
sidebarSettings.reload(
|
||||
{},
|
||||
{
|
||||
@@ -405,9 +406,13 @@ const steps = reactive([
|
||||
minimize.value = true
|
||||
let course = await getFirstCourse()
|
||||
if (course) {
|
||||
router.push({ name: 'CourseForm', params: { courseName: course } })
|
||||
router.push({
|
||||
name: 'CourseDetail',
|
||||
params: { courseName: course },
|
||||
hash: '#settings',
|
||||
})
|
||||
} else {
|
||||
router.push({ name: 'CourseForm' })
|
||||
router.push({ name: 'Courses', query: { newCourse: '1' } })
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -422,11 +427,12 @@ const steps = reactive([
|
||||
let course = await getFirstCourse()
|
||||
if (course) {
|
||||
router.push({
|
||||
name: 'CourseForm',
|
||||
name: 'CourseDetail',
|
||||
params: { courseName: course },
|
||||
hash: '#settings',
|
||||
})
|
||||
} else {
|
||||
router.push({ name: 'Courses' })
|
||||
router.push({ name: 'Courses', query: { newCourse: '1' } })
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -591,10 +597,18 @@ watch(userResource, async () => {
|
||||
await programs.reload()
|
||||
setUpOnboarding()
|
||||
}
|
||||
sidebarLinks.value = getSidebarLinks()
|
||||
setSidebarLinks()
|
||||
updateSidebarLinks()
|
||||
})
|
||||
|
||||
watch(settingsStore.settings, () => {
|
||||
updateSidebarLinks()
|
||||
})
|
||||
|
||||
const updateSidebarLinks = () => {
|
||||
sidebarLinks.value = getSidebarLinks()
|
||||
updateSidebarLinksVisibility()
|
||||
}
|
||||
|
||||
const redirectToWebsite = () => {
|
||||
window.open('https://frappe.io/learning', '_blank')
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ const evaluationCourses = computed(() => {
|
||||
|
||||
const canScheduleEvals = computed(() => {
|
||||
return (
|
||||
upcoming_evals.data?.length != evaluationCourses.length &&
|
||||
upcoming_evals.data?.length != evaluationCourses.value?.length &&
|
||||
!props.forHome &&
|
||||
!endDateHasPassed.value
|
||||
)
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
:size="size"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<template v-if="user.open_to === 'Opportunities'" #indicator>
|
||||
<Tooltip :text="__('Open to Opportunities')" placement="right">
|
||||
<template v-if="user.open_to === 'Work'" #indicator>
|
||||
<Tooltip :text="__('Open to Work')" placement="right">
|
||||
<div class="rounded-full bg-surface-green-3 w-fit">
|
||||
<BadgeCheckIcon :class="'text-ink-white ' + checkSize" />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user