Merge pull request #1241 from pateljannat/batch-feedback
feat: batch feedback
This commit is contained in:
265
frontend/src/components/BatchFeedback.vue
Normal file
265
frontend/src/components/BatchFeedback.vue
Normal file
@@ -0,0 +1,265 @@
|
||||
<template>
|
||||
<div v-if="user.data?.is_student">
|
||||
<div
|
||||
v-if="feedbackList.data?.length"
|
||||
class="bg-blue-100 text-blue-700 p-2 rounded-md mb-5"
|
||||
>
|
||||
{{ __('Thank you for providing your feedback!') }}
|
||||
</div>
|
||||
<div v-else class="flex justify-between items-center mb-5">
|
||||
<div class="text-lg font-semibold">
|
||||
{{ __('Help Us Improve') }}
|
||||
</div>
|
||||
<Button @click="submitFeedback()">
|
||||
{{ __('Submit') }}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="space-y-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<Rating
|
||||
v-model="feedback.content"
|
||||
:label="__('Content')"
|
||||
:readonly="readOnly"
|
||||
/>
|
||||
<Rating
|
||||
v-model="feedback.delivery"
|
||||
:label="__('Delivery')"
|
||||
:readonly="readOnly"
|
||||
/>
|
||||
<Rating
|
||||
v-model="feedback.instructors"
|
||||
:label="__('Instructors')"
|
||||
:readonly="readOnly"
|
||||
/>
|
||||
<Rating
|
||||
v-model="feedback.value"
|
||||
:label="__('Value')"
|
||||
:readonly="readOnly"
|
||||
/>
|
||||
</div>
|
||||
<FormControl
|
||||
v-model="feedback.feedback"
|
||||
type="textarea"
|
||||
:label="__('Feedback')"
|
||||
:rows="7"
|
||||
:readonly="readOnly"
|
||||
/>
|
||||
</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>
|
||||
<script setup>
|
||||
import { computed, inject, onMounted, reactive, ref, watch } from 'vue'
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
createListResource,
|
||||
FormControl,
|
||||
ListView,
|
||||
ListHeader,
|
||||
ListHeaderItem,
|
||||
ListRows,
|
||||
ListRow,
|
||||
ListRowItem,
|
||||
Rating,
|
||||
} from 'frappe-ui'
|
||||
|
||||
const user = inject('$user')
|
||||
const ratingKeys = ['content', 'delivery', 'instructors', 'value']
|
||||
const readOnly = ref(false)
|
||||
const average = reactive({})
|
||||
const feedback = reactive({})
|
||||
|
||||
const props = defineProps({
|
||||
batch: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
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({
|
||||
doctype: 'LMS Batch Feedback',
|
||||
filters: {
|
||||
batch: props.batch,
|
||||
},
|
||||
fields: [
|
||||
'content',
|
||||
'delivery',
|
||||
'instructors',
|
||||
'value',
|
||||
'feedback',
|
||||
'name',
|
||||
'member',
|
||||
'member_name',
|
||||
'member_image',
|
||||
],
|
||||
cache: ['feedbackList', props.batch, user.data?.name],
|
||||
})
|
||||
|
||||
watch(
|
||||
() => feedbackList.data,
|
||||
() => {
|
||||
if (feedbackList.data.length) {
|
||||
let data = feedbackList.data
|
||||
readOnly.value = true
|
||||
|
||||
ratingKeys.forEach((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 = () => {
|
||||
ratingKeys.forEach((key) => {
|
||||
feedback[key] = feedback[key] / 5
|
||||
})
|
||||
feedbackList.insert.submit(
|
||||
{
|
||||
member: user.data?.name,
|
||||
batch: props.batch,
|
||||
...feedback,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
feedbackList.reload()
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -383,7 +383,7 @@ const getChartOptions = (categories) => {
|
||||
},
|
||||
rotate: 0,
|
||||
formatter: function (value) {
|
||||
return value.length > 30 ? `${value.substring(0, 30)}...` : value // Trim long labels
|
||||
return value.length > 30 ? `${value.substring(0, 30)}...` : value
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex rounded p-1 lg:px-2 lg:py-2.5 hover:bg-gray-100">
|
||||
<div class="flex rounded p-1 lg:px-2 lg:py-4 hover:bg-gray-100">
|
||||
<div class="flex w-3/5 md:w-2/5">
|
||||
<img
|
||||
:src="job.company_logo"
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</Button>
|
||||
</div>
|
||||
</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">
|
||||
<Tabs
|
||||
v-model="tabIndex"
|
||||
@@ -65,7 +65,7 @@
|
||||
<div v-else-if="tab.label == 'Dashboard'">
|
||||
<BatchStudents :batch="batch.data" />
|
||||
</div>
|
||||
<div v-else-if="tab.label == 'Live Class'">
|
||||
<div v-else-if="tab.label == 'Classes'">
|
||||
<LiveClass :batch="batch.data.name" />
|
||||
</div>
|
||||
<div v-else-if="tab.label == 'Assessments'">
|
||||
@@ -81,9 +81,12 @@
|
||||
:title="__('Discussions')"
|
||||
:key="batch.data.name"
|
||||
:singleThread="true"
|
||||
:scrollToBottom="true"
|
||||
:scrollToBottom="false"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="tab.label == 'Feedback'">
|
||||
<BatchFeedback :batch="batch.data.name" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Tabs>
|
||||
@@ -190,12 +193,11 @@ import {
|
||||
BookOpen,
|
||||
Laptop,
|
||||
BookOpenCheck,
|
||||
Contact2,
|
||||
Mail,
|
||||
SendIcon,
|
||||
MessageCircle,
|
||||
Globe,
|
||||
ShieldCheck,
|
||||
ClipboardPen,
|
||||
} from 'lucide-vue-next'
|
||||
import { formatTime, updateDocumentTitle } from '@/utils'
|
||||
import BatchDashboard from '@/components/BatchDashboard.vue'
|
||||
@@ -208,6 +210,7 @@ import AnnouncementModal from '@/components/Modals/AnnouncementModal.vue'
|
||||
import Discussions from '@/components/Discussions.vue'
|
||||
import DateRange from '@/components/Common/DateRange.vue'
|
||||
import BulkCertificates from '@/components/Modals/BulkCertificates.vue'
|
||||
import BatchFeedback from '@/components/BatchFeedback.vue'
|
||||
|
||||
const user = inject('$user')
|
||||
const showAnnouncementModal = ref(false)
|
||||
@@ -271,7 +274,7 @@ const tabs = computed(() => {
|
||||
})
|
||||
|
||||
batchTabs.push({
|
||||
label: 'Live Class',
|
||||
label: 'Classes',
|
||||
icon: Laptop,
|
||||
})
|
||||
|
||||
@@ -291,6 +294,11 @@ const tabs = computed(() => {
|
||||
label: 'Discussions',
|
||||
icon: MessageCircle,
|
||||
})
|
||||
|
||||
batchTabs.push({
|
||||
label: 'Feedback',
|
||||
icon: ClipboardPen,
|
||||
})
|
||||
return batchTabs
|
||||
})
|
||||
|
||||
|
||||
@@ -603,9 +603,13 @@ def get_categories(doctype, filters):
|
||||
@frappe.whitelist()
|
||||
def get_members(start=0, search=""):
|
||||
"""Get members for the given search term and start index.
|
||||
Args: start (int): Start index for the query.
|
||||
search (str): Search term to filter the results.
|
||||
Returns: List of members.
|
||||
Args: start (int): Start index for the query.
|
||||
<<<<<<< HEAD
|
||||
search (str): Search term to filter the results.
|
||||
=======
|
||||
search (str): Search term to filter the results.
|
||||
>>>>>>> 4869bba7bbb2fb38477d6fc29fb3b5838e075577
|
||||
Returns: List of members.
|
||||
"""
|
||||
|
||||
filters = {"enabled": 1, "name": ["not in", ["Administrator", "Guest"]]}
|
||||
|
||||
0
lms/lms/doctype/lms_batch_feedback/__init__.py
Normal file
0
lms/lms/doctype/lms_batch_feedback/__init__.py
Normal file
8
lms/lms/doctype/lms_batch_feedback/lms_batch_feedback.js
Normal file
8
lms/lms/doctype/lms_batch_feedback/lms_batch_feedback.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2025, Frappe and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("LMS Batch Feedback", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
118
lms/lms/doctype/lms_batch_feedback/lms_batch_feedback.json
Normal file
118
lms/lms/doctype/lms_batch_feedback/lms_batch_feedback.json
Normal file
@@ -0,0 +1,118 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-01-07 18:53:22.279844",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"member",
|
||||
"member_name",
|
||||
"member_image",
|
||||
"batch",
|
||||
"column_break_swst",
|
||||
"content",
|
||||
"delivery",
|
||||
"instructors",
|
||||
"value",
|
||||
"feedback"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "member",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Member",
|
||||
"options": "User",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "batch",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Batch",
|
||||
"options": "LMS Batch",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "feedback",
|
||||
"fieldtype": "Small Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Feedback",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_swst",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "content",
|
||||
"fieldtype": "Rating",
|
||||
"label": "Content"
|
||||
},
|
||||
{
|
||||
"fieldname": "delivery",
|
||||
"fieldtype": "Rating",
|
||||
"label": "Delivery"
|
||||
},
|
||||
{
|
||||
"fieldname": "instructors",
|
||||
"fieldtype": "Rating",
|
||||
"label": "Instructors"
|
||||
},
|
||||
{
|
||||
"fieldname": "value",
|
||||
"fieldtype": "Rating",
|
||||
"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,
|
||||
"links": [],
|
||||
"modified": "2025-01-10 19:39:04.143783",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Batch Feedback",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS Student",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
9
lms/lms/doctype/lms_batch_feedback/lms_batch_feedback.py
Normal file
9
lms/lms/doctype/lms_batch_feedback/lms_batch_feedback.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2025, Frappe and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class LMSBatchFeedback(Document):
|
||||
pass
|
||||
@@ -0,0 +1,30 @@
|
||||
# Copyright (c) 2025, Frappe and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
|
||||
# On IntegrationTestCase, the doctype test records and all
|
||||
# link-field test record dependencies are recursively loaded
|
||||
# Use these module variables to add/remove to/from that list
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
|
||||
|
||||
class UnitTestLMSBatchFeedback(UnitTestCase):
|
||||
"""
|
||||
Unit tests for LMSBatchFeedback.
|
||||
Use this class for testing individual functions and methods.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IntegrationTestLMSBatchFeedback(IntegrationTestCase):
|
||||
"""
|
||||
Integration tests for LMSBatchFeedback.
|
||||
Use this class for testing interactions between multiple components.
|
||||
"""
|
||||
|
||||
pass
|
||||
Reference in New Issue
Block a user