feat: evaluation feedback record

This commit is contained in:
Jannat Patel
2024-09-09 20:05:08 +05:30
parent bd94890da7
commit 60f2e86b42
12 changed files with 4756 additions and 1028 deletions

View File

@@ -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",

View File

@@ -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)
}

View 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>

View File

@@ -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>

View File

@@ -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
}

View 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>

View File

@@ -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'),
}
],
},
{

File diff suppressed because it is too large Load Diff

View File

@@ -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()

View File

@@ -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",

View File

@@ -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"
],

3948
yarn.lock

File diff suppressed because it is too large Load Diff