fix: improved evaluators list

This commit is contained in:
Jannat Patel
2025-07-04 12:26:57 +05:30
parent 3d1a23576a
commit e057d3ed9a
7 changed files with 179 additions and 92 deletions

View File

@@ -23,8 +23,8 @@ describe("Batch Creation", () => {
const randomEmail = `testuser_${dateNow}@example.com`; const randomEmail = `testuser_${dateNow}@example.com`;
const randomName = `Test User ${dateNow}`; const randomName = `Test User ${dateNow}`;
cy.get("input[placeholder='Email']").type(randomEmail); cy.get("input[placeholder='jane@doe.com']").type(randomEmail);
cy.get("input[placeholder='First Name']").type(randomName); cy.get("input[placeholder='Jane']").type(randomName);
cy.get("button").contains("Add").click(); cy.get("button").contains("Add").click();
// Add evaluator // Add evaluator
@@ -39,7 +39,7 @@ describe("Batch Creation", () => {
.click(); .click();
const randomEvaluator = `evaluator${dateNow}@example.com`; const randomEvaluator = `evaluator${dateNow}@example.com`;
cy.get("input[placeholder='Email']").type(randomEvaluator); cy.get("input[placeholder='jane@doe.com']").type(randomEvaluator);
cy.get("button").contains("Add").click(); cy.get("button").contains("Add").click();
cy.get("div").contains(randomEvaluator).should("be.visible").click(); cy.get("div").contains(randomEvaluator).should("be.visible").click();

View File

@@ -1,60 +1,54 @@
<template> <template>
<div class="flex min-h-0 flex-col text-base"> <div class="flex min-h-0 flex-col text-base">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between">
<div> <div>
<div class="text-xl font-semibold mb-1 text-ink-gray-9"> <div class="text-xl font-semibold mb-1 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 item-center space-x-2"> <div class="flex item-center space-x-2">
<Button variant="solid" @click="() => (showForm = !showForm)">
<template #prefix>
<Plus class="size-4 stroke-1.5" />
</template>
{{ __('New') }}
</Button>
</div>
</div>
<div class="mt-8 pb-5">
<FormControl <FormControl
v-model="search" v-model="search"
:placeholder="__('Search')" :placeholder="__('Search')"
type="text" type="text"
:debounce="300" :debounce="300"
/> class="w-1/4 mb-4"
<Button @click="() => (showForm = !showForm)"> >
<template #prefix> <template #prefix>
<Plus v-if="!showForm" class="size-4 stroke-1.5" /> <Search class="size-4 stroke-1.5 text-ink-gray-5" />
<X v-else class="size-4 stroke-1.5" />
</template> </template>
{{ showForm ? __('Close') : __('New') }} </FormControl>
</Button> <div class="overflow-auto h-[60vh]">
</div>
</div>
<!-- Form to add new member -->
<div v-if="showForm" class="flex items-center space-x-2 my-4">
<FormControl
v-model="email"
:placeholder="__('Email')"
type="email"
class="w-full"
@keydown.enter="addEvaluator"
/>
<Button @click="addEvaluator()" variant="subtle">
{{ __('Add') }}
</Button>
</div>
<div class="overflow-y-scroll">
<div class="divide-y"> <div class="divide-y">
<div <div
v-for="evaluator in evaluators.data" v-for="evaluator in evaluators.data"
@click="openProfile(evaluator.username)" :key="evaluator.evaluator"
class="cursor-pointer" class="cursor-pointer"
> >
<div class="flex items-center justify-between py-3"> <div class="flex items-center justify-between group py-3">
<div class="flex items-center space-x-3"> <div
class="flex items-center space-x-3"
@click="openProfile(evaluator.username)"
>
<Avatar <Avatar
:image="evaluator.user_image" :image="evaluator.user_image"
:label="evaluator.full_name" :label="evaluator.full_name"
size="lg" size="xl"
/> />
<div> <div class="space-y-1">
<div class="text-base font-semibold text-ink-gray-9"> <div class="text-base font-semibold text-ink-gray-9">
{{ evaluator.full_name }} {{ evaluator.full_name }}
</div> </div>
@@ -63,16 +57,73 @@
</div> </div>
</div> </div>
</div> </div>
<div class="invisible group-hover:visible">
<Button
variant="ghost"
@click="deleteEvaluator(evaluator.evaluator)"
>
<template #icon>
<Trash2 class="size-4 stroke-1.5 text-ink-red-3" />
</template>
</Button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div
v-if="evaluators.length && hasNextPage"
class="flex justify-center mt-4"
>
<Button @click="evaluators.reload()">
<template #prefix>
<RefreshCw class="h-3 w-3 stroke-1.5" />
</template>
{{ __('Load More') }}
</Button>
</div> </div>
</div>
</div>
</div>
<Dialog
v-model="showForm"
:options="{
size: 'xl',
title: __('Add Evaluator'),
actions: [{
label: __('Add'),
variant: 'solid',
onClick({ close }: any) {
addEvaluator(close)
},
}]
}"
>
<template #body-content>
<div v-if="showForm" class="flex items-center">
<FormControl
v-model="email"
:label="__('Email')"
placeholder="jane@doe.com"
type="email"
class="w-full"
@keydown.enter="addEvaluator"
/>
</div>
</template>
</Dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { createResource, Button, FormControl, call, Avatar } from 'frappe-ui' import {
Avatar,
Button,
call,
createListResource,
Dialog,
FormControl,
toast,
} from 'frappe-ui'
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import { Plus, X } from 'lucide-vue-next' import { Plus, Search, Trash2, RefreshCw } from 'lucide-vue-next'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const show = defineModel('show') const show = defineModel('show')
@@ -95,33 +146,39 @@ const props = defineProps({
}, },
}) })
const evaluators = createResource({ const evaluators = createListResource({
url: 'frappe.client.get_list',
makeParams: () => {
return {
doctype: 'Course Evaluator', doctype: 'Course Evaluator',
fields: ['evaluator', 'full_name', 'user_image', 'username'], fields: ['evaluator', 'username', 'full_name', 'user_image'],
filters: search.value ? { evaluator: ['like', `%${search.value}%`] } : {},
}
},
auto: true, auto: true,
orderBy: 'creation desc',
}) })
const addEvaluator = () => { const addEvaluator = (close: () => void) => {
call('lms.lms.api.add_an_evaluator', { call('lms.lms.api.add_an_evaluator', {
email: email.value, email: email.value,
}).then((data) => { })
showForm.value = false .then(() => {
email.value = '' email.value = ''
evaluators.reload() evaluators.reload()
toast.success(__('Evaluator added successfully'))
close()
})
.catch((error: any) => {
toast.error(__(error.messages[0] || error.messages))
console.error('Error adding evaluator:', error)
}) })
} }
watch(search, () => { watch(search, () => {
evaluators.update({
filters: {
full_name: ['like', `%${search.value}%`],
},
})
evaluators.reload() evaluators.reload()
}) })
const openProfile = (username) => { const openProfile = (username: string) => {
show.value = false show.value = false
router.push({ router.push({
name: 'Profile', name: 'Profile',
@@ -130,4 +187,18 @@ const openProfile = (username) => {
}, },
}) })
} }
const deleteEvaluator = (evaluator: string) => {
call('lms.lms.api.delete_evaluator', {
evaluator: evaluator,
})
.then(() => {
toast.success(__('Evaluator deleted successfully'))
evaluators.reload()
})
.catch((error: any) => {
toast.error(__(error.messages[0] || error.messages))
console.error('Error deleting evaluator:', error)
})
}
</script> </script>

View File

@@ -19,7 +19,7 @@
</div> </div>
</div> </div>
<div class="mt-8 pb-10 overflow-auto"> <div class="mt-8 pb-10">
<FormControl <FormControl
v-model="search" v-model="search"
:placeholder="__('Search')" :placeholder="__('Search')"
@@ -31,8 +31,7 @@
<Search class="size-4 stroke-1.5 text-ink-gray-5" /> <Search class="size-4 stroke-1.5 text-ink-gray-5" />
</template> </template>
</FormControl> </FormControl>
<!-- Member list --> <div class="overflow-y-scroll h-[60vh]">
<div class="overflow-y-scroll">
<ul class="divide-y"> <ul class="divide-y">
<li <li
v-for="member in memberList" v-for="member in memberList"
@@ -69,7 +68,6 @@
</div> </div>
</li> </li>
</ul> </ul>
</div>
<div <div
v-if="memberList.length && hasNextPage" v-if="memberList.length && hasNextPage"
class="flex justify-center mt-4" class="flex justify-center mt-4"
@@ -83,6 +81,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<Dialog <Dialog
v-model="showForm" v-model="showForm"
:options="{ :options="{
@@ -210,7 +209,6 @@ const newMember = createResource({
auto: false, auto: false,
onSuccess(data: Member) { onSuccess(data: Member) {
show.value = false show.value = false
if (user?.data?.is_system_manager) updateOnboardingStep('invite_students') if (user?.data?.is_system_manager) updateOnboardingStep('invite_students')
router.push({ router.push({

View File

@@ -248,8 +248,10 @@ const tabsStructure = computed(() => {
}, },
{ {
label: 'Evaluators', label: 'Evaluators',
description: 'Manage the evaluators of your learning system', description: '',
icon: 'UserCheck', icon: 'UserCheck',
description:
'Add new evaluators or check the slots existing evaluators',
}, },
{ {
label: 'Categories', label: 'Categories',

View File

@@ -1392,6 +1392,7 @@ def save_role(user, role, value):
@frappe.whitelist() @frappe.whitelist()
def add_an_evaluator(email): def add_an_evaluator(email):
frappe.only_for("Moderator")
if not frappe.db.exists("User", email): if not frappe.db.exists("User", email):
user = frappe.new_doc("User") user = frappe.new_doc("User")
user.update( user.update(
@@ -1411,6 +1412,16 @@ def add_an_evaluator(email):
return evaluator return evaluator
@frappe.whitelist()
def delete_evaluator(evaluator):
frappe.only_for("Moderator")
if not frappe.db.exists("Course Evaluator", evaluator):
frappe.throw(_("Evaluator does not exist."))
frappe.db.delete("Has Role", {"parent": evaluator, "role": "Batch Evaluator"})
frappe.db.delete("Course Evaluator", evaluator)
@frappe.whitelist() @frappe.whitelist()
def capture_user_persona(responses): def capture_user_persona(responses):
frappe.only_for("System Manager") frappe.only_for("System Manager")

View File

@@ -58,8 +58,7 @@
"fetch_from": "evaluator.full_name", "fetch_from": "evaluator.full_name",
"fieldname": "full_name", "fieldname": "full_name",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Full Name", "label": "Full Name"
"read_only": 1
}, },
{ {
"fieldname": "column_break_casg", "fieldname": "column_break_casg",
@@ -73,21 +72,19 @@
"fetch_from": "evaluator.user_image", "fetch_from": "evaluator.user_image",
"fieldname": "user_image", "fieldname": "user_image",
"fieldtype": "Attach Image", "fieldtype": "Attach Image",
"label": "User Image", "label": "User Image"
"read_only": 1
}, },
{ {
"fetch_from": "evaluator.username", "fetch_from": "evaluator.username",
"fieldname": "username", "fieldname": "username",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Username", "label": "Username"
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2025-06-05 11:04:32.475711", "modified": "2025-07-04 12:04:11.007945",
"modified_by": "sayali@frappe.io", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Course Evaluator", "name": "Course Evaluator",
"naming_rule": "By fieldname", "naming_rule": "By fieldname",

View File

@@ -1,6 +1,7 @@
{ {
"attach_print": 0, "attach_print": 0,
"channel": "Email", "channel": "Email",
"condition": "doc.status == \"Upcoming\"",
"creation": "2022-06-03 11:51:02.681803", "creation": "2022-06-03 11:51:02.681803",
"date_changed": "date", "date_changed": "date",
"days_in_advance": 1, "days_in_advance": 1,
@@ -13,7 +14,8 @@
"is_standard": 1, "is_standard": 1,
"message": "<p> {{ _(\"Hey {0}\").format(doc.member_name) }} </p>\n<br>\n<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2} {3}.').format(doc.course_title, frappe.utils.format_date(doc.date, \"medium\"), frappe.utils.format_time(doc.start_time, \"short\"), doc.timezone) }}</p>\n<br>\n<p> {{ _(\"{0} is your evaluator\").format(doc.evaluator_name) }} </p>\n<br>\n<p> {{ _(\"Please prepare well and be on time for the evaluations.\") }} </p>\n", "message": "<p> {{ _(\"Hey {0}\").format(doc.member_name) }} </p>\n<br>\n<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2} {3}.').format(doc.course_title, frappe.utils.format_date(doc.date, \"medium\"), frappe.utils.format_time(doc.start_time, \"short\"), doc.timezone) }}</p>\n<br>\n<p> {{ _(\"{0} is your evaluator\").format(doc.evaluator_name) }} </p>\n<br>\n<p> {{ _(\"Please prepare well and be on time for the evaluations.\") }} </p>\n",
"message_type": "HTML", "message_type": "HTML",
"modified": "2024-09-05 16:33:42.212842", "minutes_offset": 0,
"modified": "2025-07-04 10:47:58.448814",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Certificate Request Reminder", "name": "Certificate Request Reminder",
@@ -22,6 +24,12 @@
{ {
"receiver_by_document_field": "member" "receiver_by_document_field": "member"
}, },
{
"receiver_by_document_field": "member"
},
{
"receiver_by_document_field": "evaluator"
},
{ {
"receiver_by_document_field": "evaluator" "receiver_by_document_field": "evaluator"
} }