feat: batch feedback for moderators
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div v-if="user.data?.is_student">
|
||||||
<div
|
<div
|
||||||
v-if="feedbackList.data?.length"
|
v-if="feedbackList.data?.length"
|
||||||
class="bg-blue-100 text-blue-700 p-2 rounded-md mb-5"
|
class="bg-blue-100 text-blue-700 p-2 rounded-md mb-5"
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-8">
|
<div class="space-y-8">
|
||||||
<div class="grid grid-cols-4 gap-5">
|
<div class="flex items-center justify-between">
|
||||||
<Rating
|
<Rating
|
||||||
v-model="feedback.content"
|
v-model="feedback.content"
|
||||||
:label="__('Content')"
|
:label="__('Content')"
|
||||||
@@ -46,21 +46,103 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="feedbackList.data?.length">
|
||||||
|
<div class="text-lg font-semibold mb-5">
|
||||||
|
{{ __('Average of Feedback Received') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mb-10">
|
||||||
|
<Rating
|
||||||
|
v-model="average.content"
|
||||||
|
:label="__('Content')"
|
||||||
|
:readonly="true"
|
||||||
|
/>
|
||||||
|
<Rating
|
||||||
|
v-model="average.delivery"
|
||||||
|
:label="__('Delivery')"
|
||||||
|
:readonly="true"
|
||||||
|
/>
|
||||||
|
<Rating
|
||||||
|
v-model="average.instructors"
|
||||||
|
:label="__('Instructors')"
|
||||||
|
:readonly="true"
|
||||||
|
/>
|
||||||
|
<Rating v-model="average.value" :label="__('Value')" :readonly="true" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-lg font-semibold mb-5">
|
||||||
|
{{ __('All Feedback') }}
|
||||||
|
</div>
|
||||||
|
<ListView
|
||||||
|
:columns="feedbackColumns"
|
||||||
|
:rows="feedbackList.data"
|
||||||
|
row-key="name"
|
||||||
|
:options="{
|
||||||
|
showTooltip: false,
|
||||||
|
rowHeight: 'h-16',
|
||||||
|
selectable: false,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<ListHeader
|
||||||
|
class="mb-2 grid items-center space-x-4 rounded bg-gray-100 p-2"
|
||||||
|
></ListHeader>
|
||||||
|
<ListRows>
|
||||||
|
<ListRow
|
||||||
|
:row="row"
|
||||||
|
v-for="row in feedbackList.data"
|
||||||
|
class="group cursor-pointer"
|
||||||
|
>
|
||||||
|
<template #default="{ column, item }">
|
||||||
|
<ListRowItem
|
||||||
|
:item="row[column.key]"
|
||||||
|
:align="column.align"
|
||||||
|
class="text-sm"
|
||||||
|
>
|
||||||
|
<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="ratingKeys.includes(column.key)">
|
||||||
|
<Rating v-model="row[column.key]" :readonly="true" />
|
||||||
|
</div>
|
||||||
|
<div v-else class="leading-5">
|
||||||
|
{{ row[column.key] }}
|
||||||
|
</div>
|
||||||
|
</ListRowItem>
|
||||||
|
</template>
|
||||||
|
</ListRow>
|
||||||
|
</ListRows>
|
||||||
|
</ListView>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { inject, reactive, ref } from 'vue'
|
import { computed, inject, onMounted, reactive, ref, watch } from 'vue'
|
||||||
import { Button, createListResource, FormControl, Rating } from 'frappe-ui'
|
import {
|
||||||
|
Avatar,
|
||||||
|
Button,
|
||||||
|
createListResource,
|
||||||
|
FormControl,
|
||||||
|
ListView,
|
||||||
|
ListHeader,
|
||||||
|
ListHeaderItem,
|
||||||
|
ListRows,
|
||||||
|
ListRow,
|
||||||
|
ListRowItem,
|
||||||
|
Rating,
|
||||||
|
} from 'frappe-ui'
|
||||||
|
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
const ratingKeys = ['content', 'delivery', 'instructors', 'value']
|
const ratingKeys = ['content', 'delivery', 'instructors', 'value']
|
||||||
const readOnly = ref(false)
|
const readOnly = ref(false)
|
||||||
const feedback = reactive({
|
const average = reactive({})
|
||||||
content: 0,
|
const feedback = reactive({})
|
||||||
delivery: 0,
|
|
||||||
instructors: 0,
|
|
||||||
value: 0,
|
|
||||||
feedback: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
batch: {
|
batch: {
|
||||||
@@ -69,25 +151,64 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
let filters = {
|
||||||
|
batch: props.batch,
|
||||||
|
}
|
||||||
|
if (user.data?.is_student) {
|
||||||
|
filters['member'] = user.data?.name
|
||||||
|
}
|
||||||
|
feedbackList.update({
|
||||||
|
filters: filters,
|
||||||
|
})
|
||||||
|
feedbackList.reload()
|
||||||
|
})
|
||||||
|
|
||||||
const feedbackList = createListResource({
|
const feedbackList = createListResource({
|
||||||
doctype: 'LMS Batch Feedback',
|
doctype: 'LMS Batch Feedback',
|
||||||
filters: {
|
filters: {
|
||||||
batch: props.batch,
|
batch: props.batch,
|
||||||
member: user.data?.name,
|
|
||||||
},
|
},
|
||||||
fields: ['content', 'delivery', 'instructors', 'value', 'feedback', 'name'],
|
fields: [
|
||||||
|
'content',
|
||||||
|
'delivery',
|
||||||
|
'instructors',
|
||||||
|
'value',
|
||||||
|
'feedback',
|
||||||
|
'name',
|
||||||
|
'member',
|
||||||
|
'member_name',
|
||||||
|
'member_image',
|
||||||
|
],
|
||||||
cache: ['feedbackList', props.batch, user.data?.name],
|
cache: ['feedbackList', props.batch, user.data?.name],
|
||||||
auto: true,
|
})
|
||||||
onSuccess(data) {
|
|
||||||
if (data.length) {
|
watch(
|
||||||
|
() => feedbackList.data,
|
||||||
|
() => {
|
||||||
|
if (feedbackList.data.length) {
|
||||||
|
let data = feedbackList.data
|
||||||
readOnly.value = true
|
readOnly.value = true
|
||||||
Object.keys(feedback).forEach((key) => {
|
|
||||||
if (ratingKeys.includes(key)) feedback[key] = data[0][key] * 5
|
ratingKeys.forEach((key) => {
|
||||||
else feedback[key] = data[0][key]
|
average[key] = 0
|
||||||
|
})
|
||||||
|
|
||||||
|
data.forEach((row) => {
|
||||||
|
Object.keys(row).forEach((key) => {
|
||||||
|
if (ratingKeys.includes(key)) row[key] = row[key] * 5
|
||||||
|
feedback[key] = row[key]
|
||||||
|
})
|
||||||
|
ratingKeys.forEach((key) => {
|
||||||
|
average[key] += row[key]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
Object.keys(average).forEach((key) => {
|
||||||
|
average[key] = average[key] / data.length
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
const submitFeedback = () => {
|
const submitFeedback = () => {
|
||||||
ratingKeys.forEach((key) => {
|
ratingKeys.forEach((key) => {
|
||||||
@@ -106,4 +227,39 @@ const submitFeedback = () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const feedbackColumns = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: 'Member',
|
||||||
|
key: 'member_name',
|
||||||
|
width: '10rem',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Feedback',
|
||||||
|
key: 'feedback',
|
||||||
|
width: '15rem',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Content',
|
||||||
|
key: 'content',
|
||||||
|
width: '10rem',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Delivery',
|
||||||
|
key: 'delivery',
|
||||||
|
width: '10rem',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Instructors',
|
||||||
|
key: 'instructors',
|
||||||
|
width: '10rem',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Value',
|
||||||
|
key: 'value',
|
||||||
|
width: '10rem',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div v-if="batch.data" class="grid grid-cols-[70%,30%] h-screen">
|
<div v-if="batch.data" class="grid grid-cols-[75%,25%] h-screen">
|
||||||
<div class="border-r">
|
<div class="border-r">
|
||||||
<Tabs
|
<Tabs
|
||||||
v-model="tabIndex"
|
v-model="tabIndex"
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
<div v-else-if="tab.label == 'Dashboard'">
|
<div v-else-if="tab.label == 'Dashboard'">
|
||||||
<BatchStudents :batch="batch.data" />
|
<BatchStudents :batch="batch.data" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="tab.label == 'Live Class'">
|
<div v-else-if="tab.label == 'Classes'">
|
||||||
<LiveClass :batch="batch.data.name" />
|
<LiveClass :batch="batch.data.name" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="tab.label == 'Assessments'">
|
<div v-else-if="tab.label == 'Assessments'">
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
:title="__('Discussions')"
|
:title="__('Discussions')"
|
||||||
:key="batch.data.name"
|
:key="batch.data.name"
|
||||||
:singleThread="true"
|
:singleThread="true"
|
||||||
:scrollToBottom="true"
|
:scrollToBottom="false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="tab.label == 'Feedback'">
|
<div v-else-if="tab.label == 'Feedback'">
|
||||||
@@ -274,7 +274,7 @@ const tabs = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
batchTabs.push({
|
batchTabs.push({
|
||||||
label: 'Live Class',
|
label: 'Classes',
|
||||||
icon: Laptop,
|
icon: Laptop,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -295,12 +295,10 @@ const tabs = computed(() => {
|
|||||||
icon: MessageCircle,
|
icon: MessageCircle,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (isStudent.value) {
|
batchTabs.push({
|
||||||
batchTabs.push({
|
label: 'Feedback',
|
||||||
label: 'Feedback',
|
icon: ClipboardPen,
|
||||||
icon: ClipboardPen,
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
return batchTabs
|
return batchTabs
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,15 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"member",
|
"member",
|
||||||
|
"member_name",
|
||||||
|
"member_image",
|
||||||
"batch",
|
"batch",
|
||||||
"feedback",
|
|
||||||
"column_break_swst",
|
"column_break_swst",
|
||||||
"content",
|
"content",
|
||||||
"delivery",
|
"delivery",
|
||||||
"instructors",
|
"instructors",
|
||||||
"value"
|
"value",
|
||||||
|
"feedback"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -61,11 +63,25 @@
|
|||||||
"fieldname": "value",
|
"fieldname": "value",
|
||||||
"fieldtype": "Rating",
|
"fieldtype": "Rating",
|
||||||
"label": "Value"
|
"label": "Value"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "member.full_name",
|
||||||
|
"fieldname": "member_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Member Name",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "member.user_image",
|
||||||
|
"fieldname": "member_image",
|
||||||
|
"fieldtype": "Attach Image",
|
||||||
|
"label": "Member Image",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-01-10 17:25:25.482724",
|
"modified": "2025-01-10 19:39:04.143783",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Batch Feedback",
|
"name": "LMS Batch Feedback",
|
||||||
|
|||||||
Reference in New Issue
Block a user