feat: evaluator unavailability

This commit is contained in:
Jannat Patel
2024-04-16 11:08:30 +05:30
parent 39bc141133
commit 719e471678
12 changed files with 371 additions and 25 deletions

View File

@@ -25,7 +25,7 @@
<div class="mb-1.5 text-sm text-gray-600">
{{ __('Date') }}
</div>
<DatePicker v-model="evaluation.date" />
<FormControl type="date" v-model="evaluation.date" />
</div>
<div v-if="slots.data?.length">
<div class="mb-1.5 text-sm text-gray-600">
@@ -57,7 +57,7 @@
</Dialog>
</template>
<script setup>
import { Dialog, createResource, Select, DatePicker } from 'frappe-ui'
import { Dialog, createResource, Select, FormControl } from 'frappe-ui'
import { defineModel, reactive, watch, inject } from 'vue'
import { createToast, formatTime } from '@/utils/'
@@ -168,7 +168,7 @@ watch(
() => evaluation.date,
(date) => {
evaluation.start_time = ''
if (date) {
if (date && evaluation.course) {
slots.submit(evaluation)
}
}

View File

@@ -116,7 +116,7 @@ const profile = createResource({
const setActiveTab = () => {
let fragments = route.path.split('/')
let sections = ['certificates', 'settings', 'evaluator']
let sections = ['certificates', 'roles', 'evaluations']
sections.forEach((section) => {
if (fragments.includes(section)) {
activeTab.value = convertToTitleCase(section)
@@ -130,8 +130,8 @@ watchEffect(() => {
let route = {
About: { name: 'ProfileAbout' },
Certificates: { name: 'ProfileCertificates' },
Settings: { name: 'ProfileSettings' },
Evaluato: { name: 'ProfileEvaluator' },
Roles: { name: 'ProfileRoles' },
Evaluations: { name: 'ProfileEvaluator' },
}[activeTab.value]
router.push(route)
}
@@ -147,9 +147,9 @@ const isSessionUser = () => {
const getTabButtons = () => {
let buttons = [{ label: 'About' }, { label: 'Certificates' }]
if ($user.data?.is_moderator) buttons.push({ label: 'Settings' })
if ($user.data?.is_moderator) buttons.push({ label: 'Roles' })
if (isSessionUser() && $user.data?.is_evaluator)
buttons.push({ label: 'Evaluation Slots' })
buttons.push({ label: 'Evaluations' })
return buttons
}

View File

@@ -9,7 +9,7 @@
:key="certificate.name"
class="bg-white shadow rounded-lg p-3 cursor-pointer"
>
<div class="font-medium">
<div class="font-medium leading-5">
{{ certificate.course_title }}
</div>
<div class="mt-2">

View File

@@ -1,2 +1,278 @@
<template>Evaluator</template>
<script setup></script>
<template>
<div class="mt-7 mb-20">
<h2 class="mb-4 text-lg font-semibold text-gray-900">
{{ __('My availability') }}
</h2>
<div class="w-3/4">
<div class="grid grid-cols-4 gap-4 text-sm text-gray-700 mb-4">
<div>
{{ __('Day') }}
</div>
<div>
{{ __('Start Time') }}
</div>
<div>
{{ __('End Time') }}
</div>
</div>
<div
v-if="slots.data"
v-for="slot in slots.data.schedule"
class="grid grid-cols-4 gap-4 mb-4 group"
>
<FormControl
type="select"
:options="days"
v-model="slot.day"
@focusout.stop="update(slot.name, 'day', slot.day)"
/>
<FormControl
type="time"
v-model="slot.start_time"
@focusout.stop="update(slot.name, 'start_time', slot.start_time)"
/>
<FormControl
type="time"
v-model="slot.end_time"
@focusout.stop="update(slot.name, 'end_time', slot.end_time)"
/>
<X
@click="deleteRow(slot.name)"
class="w-6 h-auto stroke-1.5 text-red-900 rounded-md cursor-pointer p-1 bg-red-100 hidden group-hover:block"
/>
</div>
<div class="grid grid-cols-4 gap-4 mb-4" v-show="showSlotsTemplate">
<FormControl
type="select"
:options="days"
v-model="newSlot.day"
@focusout.stop="add()"
/>
<FormControl
type="time"
v-model="newSlot.start_time"
@focusout.stop="add()"
/>
<FormControl
type="time"
v-model="newSlot.end_time"
@focusout.stop="add()"
/>
</div>
<Button @click="showSlotsTemplate = 1">
<template #prefix>
<Plus class="w-4 h-4 stroke-1.5 text-gray-700" />
</template>
{{ __('Add Slot') }}
</Button>
</div>
<div class="mt-10 w-3/4">
<h2 class="mb-4 text-lg font-semibold text-gray-900">
{{ __('I am unavailable') }}
</h2>
<div class="grid grid-cols-4 gap-4">
<FormControl
type="date"
:label="__('From')"
v-model="from"
@change.stop="
() => {
updateUnavailability.submit({
field: 'unavailable_from',
value: from,
})
}
"
/>
<FormControl
type="date"
:label="__('To')"
v-model="to"
@change.stop="
() => {
updateUnavailability.submit({
field: 'unavailable_to',
value: to,
})
}
"
/>
</div>
</div>
</div>
</template>
<script setup>
import { createResource, FormControl, Button } from 'frappe-ui'
import { computed, reactive, ref } from 'vue'
import { showToast, convertToTitleCase } from '@/utils'
import { Plus, X } from 'lucide-vue-next'
const props = defineProps({
profile: {
type: Object,
required: true,
},
})
const showSlotsTemplate = ref(0)
const from = ref(null)
const to = ref(null)
const newSlot = reactive({
day: '',
start_time: '',
end_time: '',
})
const slots = createResource({
url: 'lms.lms.api.get_evaluator_details',
params: {
evaluator: props.profile.data?.name,
},
auto: true,
})
const createSlot = createResource({
url: 'frappe.client.insert',
makeParams(values) {
return {
doc: {
doctype: 'Evaluator Schedule',
parent: slots.data?.name,
parentfield: 'schedule',
parenttype: 'Course Evaluator',
...newSlot,
},
}
},
onSuccess() {
showToast('Success', 'Slot added successfully', 'check')
slots.reload()
showSlotsTemplate.value = 0
newSlot.day = ''
newSlot.start_time = ''
newSlot.end_time = ''
},
onError(err) {
showToast('Error', err.messages?.[0] || err, 'x')
},
})
const updateSlot = createResource({
url: 'frappe.client.set_value',
makeParams(values) {
return {
doctype: 'Evaluator Schedule',
name: values.name,
fieldname: values.field,
value: values.value,
}
},
onSuccess() {
showToast('Success', 'Availability updated successfully', 'check')
},
onError(err) {
showToast('Error', err.messages?.[0] || err, 'x')
},
})
const deleteSlot = createResource({
url: 'frappe.client.delete',
makeParams(values) {
return {
doctype: 'Evaluator Schedule',
name: values.name,
}
},
onSuccess() {
showToast('Success', 'Slot deleted successfully', 'check')
slots.reload()
},
onError(err) {
showToast('Error', err.messages?.[0] || err, 'x')
},
})
const updateUnavailability = createResource({
url: 'frappe.client.set_value',
makeParams(values) {
return {
doctype: 'Course Evaluator',
name: slots.data?.name,
fieldname: values.field,
value: values.value,
}
},
onSuccess() {
showToast('Success', 'Unavailability updated successfully', 'check')
},
onError(err) {
showToast('Error', err.messages?.[0] || err, 'x')
},
})
const update = (name, field, value) => {
updateSlot.submit(
{
name,
field,
value,
},
{
validate() {
if (!value) {
return `Please enter a value for ${convertToTitleCase(field)}`
}
},
}
)
}
const add = () => {
if (!newSlot.day || !newSlot.start_time || !newSlot.end_time) {
return
}
createSlot.submit()
}
const deleteRow = (name) => {
deleteSlot.submit({ name })
}
const days = computed(() => {
return [
{
label: 'Monday',
value: 'Monday',
},
{
label: 'Tuesday',
value: 'Tuesday',
},
{
label: 'Wednesday',
value: 'Wednesday',
},
{
label: 'Thursday',
value: 'Thursday',
},
{
label: 'Friday',
value: 'Friday',
},
{
label: 'Saturday',
value: 'Saturday',
},
{
label: 'Sunday',
value: 'Sunday',
},
]
})
</script>

View File

@@ -73,13 +73,13 @@ const routes = [
component: () => import('@/pages/ProfileCertificates.vue'),
},
{
name: 'ProfileSettings',
path: 'settings',
component: () => import('@/pages/ProfileSettings.vue'),
name: 'ProfileRoles',
path: 'roles',
component: () => import('@/pages/ProfileRoles.vue'),
},
{
name: 'ProfileEvaluator',
path: 'evaluator',
path: 'evaluations',
component: () => import('@/pages/ProfileEvaluator.vue'),
},
],

View File

@@ -298,3 +298,16 @@ def get_unsplash_photos(keyword=None):
return get_by_keyword(keyword)
return frappe.cache().get_value("unsplash_photos", generator=get_list)
@frappe.whitelist()
def get_evaluator_details(evaluator):
frappe.only_for("Batch Evaluator")
if frappe.db.exists("Course Evaluator", {"evaluator": evaluator}):
return frappe.get_doc("Course Evaluator", evaluator, as_dict=1)
else:
doc = frappe.new_doc("Course Evaluator")
doc.evaluator = evaluator
doc.insert()
return doc.as_dict()

View File

@@ -8,7 +8,11 @@
"engine": "InnoDB",
"field_order": [
"evaluator",
"schedule"
"schedule",
"unavailability_section",
"unavailable_from",
"column_break_ahzi",
"unavailable_to"
],
"fields": [
{
@@ -23,11 +27,30 @@
"fieldtype": "Table",
"label": "Schedule",
"options": "Evaluator Schedule"
},
{
"fieldname": "unavailability_section",
"fieldtype": "Section Break",
"label": "Unavailability"
},
{
"fieldname": "column_break_ahzi",
"fieldtype": "Column Break"
},
{
"fieldname": "unavailable_from",
"fieldtype": "Date",
"label": "From"
},
{
"fieldname": "unavailable_to",
"fieldtype": "Date",
"label": "To"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-04-15 11:21:52.182338",
"modified": "2024-04-15 18:45:08.614466",
"modified_by": "Administrator",
"module": "LMS",
"name": "Course Evaluator",

View File

@@ -6,6 +6,7 @@ from frappe import _
from frappe.model.document import Document
from lms.lms.utils import get_evaluator
from datetime import datetime
from frappe.utils import get_time
class CourseEvaluator(Document):
@@ -14,7 +15,7 @@ class CourseEvaluator(Document):
def validate_time_slots(self):
for schedule in self.schedule:
if schedule.start_time >= schedule.end_time:
if get_time(schedule.start_time) >= get_time(schedule.end_time):
frappe.throw(_("Start Time cannot be greater than End Time"))
self.validate_overlaps(schedule)
@@ -26,11 +27,21 @@ class CourseEvaluator(Document):
overlap = False
for slot in same_day_slots:
if schedule.start_time <= slot.start_time < schedule.end_time:
if (
get_time(schedule.start_time)
<= get_time(slot.start_time)
< get_time(schedule.end_time)
):
overlap = True
if schedule.start_time < slot.end_time <= schedule.end_time:
if (
get_time(schedule.start_time)
< get_time(slot.end_time)
<= get_time(schedule.end_time)
):
overlap = True
if slot.start_time < schedule.start_time and schedule.end_time < slot.end_time:
if get_time(slot.start_time) < get_time(schedule.start_time) and get_time(
schedule.end_time
) < get_time(slot.end_time):
overlap = True
if overlap:

View File

@@ -39,8 +39,6 @@
"reqd": 1
},
{
"fetch_from": "course.evaluator",
"fetch_if_empty": 1,
"fieldname": "evaluator",
"fieldtype": "Link",
"label": "Evaluator",
@@ -109,7 +107,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-04-15 11:23:03.933035",
"modified": "2024-04-16 11:01:28.336807",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Certificate Request",

View File

@@ -11,10 +11,35 @@ from lms.lms.utils import get_evaluator
class LMSCertificateRequest(Document):
def validate(self):
self.set_evaluator()
self.validate_unavailability()
self.validate_slot()
self.validate_if_existing_requests()
self.validate_evaluation_end_date()
def set_evaluator(self):
if not self.evaluator:
self.evaluator = get_evaluator(self.course, self.batch_name)
def validate_unavailability(self):
unavailable = frappe.db.get_value(
"Course Evaluator", self.evaluator, ["unavailable_from", "unavailable_to"], as_dict=1
)
if (
unavailable.unavailable_from
and unavailable.unavailable_to
and getdate(self.date) >= unavailable.unavailable_from
and getdate(self.date) <= unavailable.unavailable_to
):
frappe.throw(
_(
"Evaluator is unavailable from {0} to {1}. Please select a date after {1}"
).format(
format_date(unavailable.unavailable_from, "medium"),
format_date(unavailable.unavailable_to, "medium"),
)
)
def validate_slot(self):
if frappe.db.exists(
"LMS Certificate Request",

View File

@@ -1798,6 +1798,6 @@ def get_roles(name):
return {
"moderator": has_course_moderator_role(name),
"course_creator": has_course_instructor_role(name),
"class_evaluator": has_course_evaluator_role(name),
"batch_evaluator": has_course_evaluator_role(name),
"lms_student": has_student_role(name),
}