feat: evaluation feedback record
This commit is contained in:
@@ -21,7 +21,7 @@
|
||||
"chart.js": "^4.4.1",
|
||||
"dayjs": "^1.11.6",
|
||||
"feather-icons": "^4.28.0",
|
||||
"frappe-ui": "^0.1.56",
|
||||
"frappe-ui": "^0.1.67",
|
||||
"lucide-vue-next": "^0.383.0",
|
||||
"markdown-it": "^14.0.0",
|
||||
"pinia": "^2.0.33",
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
<template>
|
||||
<div class="flex text-center">
|
||||
<div v-for="index in 5">
|
||||
<Star
|
||||
:class="index <= rating ? 'fill-orange-500' : ''"
|
||||
class="h-6 w-6 fill-gray-400 text-gray-50 mr-1 cursor-pointer"
|
||||
@click="markRating(index)"
|
||||
/>
|
||||
<div class="space-y-1">
|
||||
<label class="block text-xs text-gray-600" v-if="props.label">
|
||||
{{ props.label }}
|
||||
</label>
|
||||
<div class="flex text-center">
|
||||
<div v-for="index in 5">
|
||||
{{ rating }}
|
||||
<Star
|
||||
:class="index <= rating ? 'fill-orange-500' : ''"
|
||||
class="h-6 w-6 fill-gray-400 text-gray-50 mr-1 cursor-pointer"
|
||||
@click="markRating(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -23,11 +29,15 @@ const props = defineProps({
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
let rating = ref(props.modelValue)
|
||||
|
||||
console.log(props.modelValue)
|
||||
let emitChange = (value) => {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
156
frontend/src/components/Modals/Event.vue
Normal file
156
frontend/src/components/Modals/Event.vue
Normal file
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<Dialog
|
||||
v-model="show"
|
||||
:options="{
|
||||
size: '2xl',
|
||||
}">
|
||||
<template #body>
|
||||
<div class="flex text-base">
|
||||
<div class="flex flex-col w-1/2 p-5">
|
||||
<div class="text-lg font-semibold mb-4">
|
||||
{{ event.title }}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col space-y-3">
|
||||
<div class="flex items-center space-x-1">
|
||||
<BookOpen class="h-4 w-4 stroke-1.5" />
|
||||
<span>
|
||||
{{ event.course_title }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-1">
|
||||
<Calendar class="h-4 w-4 stroke-1.5" />
|
||||
<span>
|
||||
{{ dayjs(event.date).format("DD MMM YYYY") }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-1">
|
||||
<Clock class="h-4 w-4 stroke-1.5" />
|
||||
<span>
|
||||
{{ formatTime(event.start_time) }} - {{ formatTime(event.end_time) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-1">
|
||||
<User class="h-4 w-4 stroke-1.5" />
|
||||
<span>
|
||||
{{ event.member }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button @click="openCallLink(event.venue)" class="mt-auto">
|
||||
<template #prefix>
|
||||
<Video class="h-4 w-4 stroke-1.5" />
|
||||
</template>
|
||||
<span>
|
||||
{{ __("Join Meeting") }}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-4 border-l w-1/2 p-5">
|
||||
{{ evaluation.rating }}
|
||||
<Rating v-model="evaluation.rating" :label="__('Rating')"/>
|
||||
<FormControl type="select" :options='[{
|
||||
value: "Pending",
|
||||
label: __("Pending")
|
||||
}, {
|
||||
value: "In Progress",
|
||||
label: __("In Progress")
|
||||
}, {
|
||||
value: "Pass",
|
||||
label: __("Pass")
|
||||
}, {
|
||||
value: "Fail",
|
||||
label: __("Fail")
|
||||
}]'
|
||||
v-model="evaluation.status" :label="__('Status')" />
|
||||
<FormControl type="textarea" v-model="evaluation.summary" :label="__('Summary')" />
|
||||
<Button variant="solid" @click="saveEvaluation()">
|
||||
{{ __("Save") }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Dialog, Button, FormControl, createResource } from 'frappe-ui';
|
||||
import { User, Calendar, Clock, Video, BookOpen } from "lucide-vue-next"
|
||||
import { inject, reactive, watch } from "vue"
|
||||
import { formatTime, showToast } from "@/utils"
|
||||
import Rating from "@/components/Controls/Rating.vue"
|
||||
|
||||
const show = defineModel()
|
||||
const dayjs = inject("$dayjs")
|
||||
|
||||
const props = defineProps({
|
||||
event: {
|
||||
type: [Object, null],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const evaluation = reactive({
|
||||
rating: 0,
|
||||
status: "Pending",
|
||||
summary: "",
|
||||
})
|
||||
|
||||
const openCallLink = (link) => {
|
||||
window.open(link, "_blank")
|
||||
}
|
||||
|
||||
const evaluationResource = createResource({
|
||||
url: "lms.lms.api.save_evaluation_details",
|
||||
makeParams(values) {
|
||||
return {
|
||||
member: props.event.member,
|
||||
course: props.event.course,
|
||||
date: props.event.date,
|
||||
start_time: props.event.start_time,
|
||||
end_time: props.event.end_time,
|
||||
status: evaluation.status,
|
||||
rating: evaluation.rating,
|
||||
summary: evaluation.summary,
|
||||
}
|
||||
},
|
||||
auto: false
|
||||
})
|
||||
|
||||
const evaluationDetails = createResource({
|
||||
url: "frappe.client.get",
|
||||
makeParams(values) {
|
||||
return {
|
||||
doctype: "LMS Certificate Evaluation",
|
||||
filters: {
|
||||
member: props.event.member,
|
||||
course: props.event.course,
|
||||
}
|
||||
}
|
||||
},
|
||||
onSuccess(data) {
|
||||
for (const key in data) {
|
||||
if (key in evaluation)
|
||||
evaluation[key] = data[key]
|
||||
}
|
||||
},
|
||||
auto: false
|
||||
})
|
||||
|
||||
const saveEvaluation = () => {
|
||||
evaluationResource.submit({}, {
|
||||
onSuccess: () => {
|
||||
show.value = false
|
||||
showToast( __("Success"), __("Evaluation saved successfully"), "check")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
watch(show, () => {
|
||||
if (show.value) {
|
||||
evaluation.rating = 0
|
||||
evaluation.status = "Pending"
|
||||
evaluation.summary = ""
|
||||
evaluationDetails.reload()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -24,7 +24,7 @@
|
||||
<div>
|
||||
{{ __('Please login to access this page.') }}
|
||||
</div>
|
||||
<Button variant="solid" @click="redirectToLogin()" class="mt-2">
|
||||
<Button @click="redirectToLogin()" class="mt-4">
|
||||
{{ __('Login') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -146,7 +146,7 @@ const coverImage = createResource({
|
||||
|
||||
const setActiveTab = () => {
|
||||
let fragments = route.path.split('/')
|
||||
let sections = ['certificates', 'roles', 'evaluations']
|
||||
let sections = ['certificates', 'roles', 'slots', 'schedule']
|
||||
sections.forEach((section) => {
|
||||
if (fragments.includes(section)) {
|
||||
activeTab.value = convertToTitleCase(section)
|
||||
@@ -158,10 +158,11 @@ const setActiveTab = () => {
|
||||
watchEffect(() => {
|
||||
if (activeTab.value) {
|
||||
let route = {
|
||||
About: { name: 'ProfileAbout' },
|
||||
Certificates: { name: 'ProfileCertificates' },
|
||||
Roles: { name: 'ProfileRoles' },
|
||||
Evaluations: { name: 'ProfileEvaluator' },
|
||||
"About": { name: 'ProfileAbout' },
|
||||
"Certificates": { name: 'ProfileCertificates' },
|
||||
"Roles": { name: 'ProfileRoles' },
|
||||
"Slots": { name: 'ProfileEvaluator' },
|
||||
"Schedule": { name: 'ProfileEvaluationSchedule' },
|
||||
}[activeTab.value]
|
||||
router.push(route)
|
||||
}
|
||||
@@ -185,8 +186,10 @@ const isSessionUser = () => {
|
||||
const getTabButtons = () => {
|
||||
let buttons = [{ label: 'About' }, { label: 'Certificates' }]
|
||||
if ($user.data?.is_moderator) buttons.push({ label: 'Roles' })
|
||||
if (isSessionUser() && $user.data?.is_evaluator)
|
||||
buttons.push({ label: 'Evaluations' })
|
||||
if (isSessionUser() && ($user.data?.is_evaluator || $user.data?.is_moderator)) {
|
||||
buttons.push({ label: 'Slots' })
|
||||
buttons.push({ label: 'Schedule' })
|
||||
}
|
||||
|
||||
return buttons
|
||||
}
|
||||
|
||||
92
frontend/src/pages/ProfileEvaluationSchedule.vue
Normal file
92
frontend/src/pages/ProfileEvaluationSchedule.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div class="mt-7 mb-20">
|
||||
<div class="flex h-screen flex-col overflow-hidden">
|
||||
<Calendar
|
||||
v-if="evaluations.data?.length"
|
||||
:config="{
|
||||
defaultMode: 'Month',
|
||||
disableModes: ['Day', 'Week'],
|
||||
redundantCellHeight: 100,
|
||||
enableShortcuts: true,
|
||||
}"
|
||||
:events="evaluations.data"
|
||||
@click="(event) => openEvent(event)"
|
||||
>
|
||||
<template #header="{currentMonthYear,
|
||||
decrement,
|
||||
increment,
|
||||
}">
|
||||
<div class="mb-2 flex justify-between">
|
||||
<span class="text-lg font-semibold">
|
||||
{{ currentMonthYear }}
|
||||
</span>
|
||||
<div class="flex gap-x-1">
|
||||
<Button
|
||||
@click="decrement()"
|
||||
variant="ghost"
|
||||
class="h-4 w-4"
|
||||
icon="chevron-left"
|
||||
/>
|
||||
<Button
|
||||
@click="increment()"
|
||||
variant="ghost"
|
||||
class="h-4 w-4"
|
||||
icon="chevron-right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Calendar>
|
||||
</div>
|
||||
</div>
|
||||
<Event v-model="showEvent" :event="currentEvent"/>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Calendar, createListResource, Button } from "frappe-ui"
|
||||
import { inject, ref } from "vue"
|
||||
import Event from "@/components/Modals/Event.vue"
|
||||
|
||||
const user = inject("$user")
|
||||
const currentEvent = ref(null)
|
||||
const showEvent = ref(false)
|
||||
|
||||
const props = defineProps({
|
||||
profile: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const evaluations = createListResource({
|
||||
doctype: "LMS Certificate Request",
|
||||
filters: {
|
||||
"evaluator": user.data?.name
|
||||
},
|
||||
fields: ["name", "member_name", "member", "course", "course_title", "date", "start_time", "end_time", "google_meet_link"],
|
||||
auto: true,
|
||||
cache: ["schedule", user.data?.name],
|
||||
transform(data) {
|
||||
return data.map((d) => {
|
||||
return {
|
||||
title: `${d.member_name}'s Evaluation`,
|
||||
participant: d.member_name,
|
||||
id: d.name,
|
||||
venue: d.google_meet_link,
|
||||
fromDate: `${d.date} ${d.start_time}`,
|
||||
toDate: `${d.date} ${d.end_time}`,
|
||||
color: "green",
|
||||
start_time: d.start_time,
|
||||
end_time: d.end_time,
|
||||
course: d.course,
|
||||
course_title: d.course_title,
|
||||
member: d.member,
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const openEvent = (event) => {
|
||||
currentEvent.value = event.calendarEvent
|
||||
showEvent.value = true
|
||||
}
|
||||
</script>
|
||||
@@ -79,9 +79,14 @@ const routes = [
|
||||
},
|
||||
{
|
||||
name: 'ProfileEvaluator',
|
||||
path: 'evaluations',
|
||||
path: 'slots',
|
||||
component: () => import('@/pages/ProfileEvaluator.vue'),
|
||||
},
|
||||
{
|
||||
name: 'ProfileEvaluationSchedule',
|
||||
path: 'schedule',
|
||||
component: () => import('@/pages/ProfileEvaluationSchedule.vue'),
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
1489
frontend/yarn.lock
1489
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -610,3 +610,42 @@ def check_app_permission():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_evaluation_details(
|
||||
member: str,
|
||||
course: str,
|
||||
date: str,
|
||||
start_time: str,
|
||||
end_time: str,
|
||||
status: str,
|
||||
rating: int,
|
||||
summary: str) -> None:
|
||||
"""
|
||||
Save evaluation details for a member against a course.
|
||||
"""
|
||||
evaluation = frappe.db.exists("LMS Certificate Evaluation", {
|
||||
"member": member,
|
||||
"course": course
|
||||
})
|
||||
|
||||
details = {
|
||||
"date": date,
|
||||
"start_time": start_time,
|
||||
"end_time": end_time,
|
||||
"status": status,
|
||||
"rating": rating,
|
||||
"summary": summary
|
||||
}
|
||||
|
||||
if evaluation:
|
||||
doc = frappe.db.set_value("LMS Certificate Evaluation", evaluation, details)
|
||||
else:
|
||||
doc = frappe.new_doc("LMS Certificate Evaluation")
|
||||
details.update({
|
||||
"member": member,
|
||||
"course": course
|
||||
})
|
||||
doc.update(details)
|
||||
doc.insert()
|
||||
@@ -38,7 +38,6 @@
|
||||
{
|
||||
"fieldname": "member",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Member",
|
||||
"options": "User",
|
||||
"reqd": 1
|
||||
@@ -46,6 +45,7 @@
|
||||
{
|
||||
"fieldname": "evaluator",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Evaluator",
|
||||
"options": "User",
|
||||
"read_only": 1
|
||||
@@ -141,7 +141,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-09-05 16:28:54.043488",
|
||||
"modified": "2024-09-06 18:39:53.551920",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Certificate Request",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "frappe_lms",
|
||||
"version": "1.0.0",
|
||||
"description": "Easy to use, open-source, Learning Management System",
|
||||
"workspaces1": [
|
||||
"workspaces": [
|
||||
"frappe-ui",
|
||||
"frontend"
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user