feat: see all upcoming slots when scheduling evaluation

This commit is contained in:
Jannat Patel
2025-12-18 14:40:41 +05:30
parent 776730447a
commit 9da6cff8a5
4 changed files with 196 additions and 78 deletions

View File

@@ -1,7 +1,7 @@
<template> <template>
<div v-if="user.data?.is_student"> <div v-if="user.data?.is_student">
<div> <div>
<div class="leading-5 mb-4"> <div class="leading-5 mb-4 text-ink-gray-7">
<div v-if="readOnly"> <div v-if="readOnly">
{{ __('Thank you for providing your feedback.') }} {{ __('Thank you for providing your feedback.') }}
<span <span

View File

@@ -2,7 +2,7 @@
<Dialog <Dialog
v-model="show" v-model="show"
:options="{ :options="{
title: __('Schedule Evaluation'), title: __('Schedule your evaluation'),
size: 'xl', size: 'xl',
actions: [ actions: [
{ {
@@ -14,52 +14,49 @@
}" }"
> >
<template #body-content> <template #body-content>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4 text-base max-h-[60vh]">
<div> <FormControl
<div class="mb-1.5 text-sm text-ink-gray-5"> v-model="evaluation.course"
{{ __('Course') }} type="select"
:label="__('Course')"
:options="getCourses()"
/>
<div v-if="slots.data?.length" class="space-y-4 overflow-y-auto mt-4">
<div class="text-ink-gray-9 font-medium">
{{ __('Available Slots') }}
</div> </div>
<Select v-model="evaluation.course" :options="getCourses()" /> <div class="space-y-5">
</div> <div v-for="row in slots.data" class="space-y-2">
<div> <div class="flex items-center text-ink-gray-7 space-x-2">
<div class="mb-1.5 text-sm text-ink-gray-5"> <Calendar class="size-3" />
{{ __('Date') }} <div class="">
</div> {{ dayjs(row.date).format('DD MMMM YYYY') }}
<FormControl </div>
type="date" <div>&middot;</div>
v-model="evaluation.date" <div class="text-ink-gray-5">
:min=" {{ row.day }}
dayjs() </div>
.add(dayjs.duration({ days: 1 })) </div>
.format('YYYY-MM-DD') <div class="grid grid-cols-3 gap-2">
" <div
/> v-for="slot in row.slots"
</div> class="text-base text-center border rounded-md text-ink-gray-8 p-2 cursor-pointer text-ink-gray-7 hover:bg-surface-gray-2 hover:border-outline-gray-3"
<div v-if="slots.data?.length"> @click="saveSlot(slot, row)"
<div class="mb-1.5 text-sm text-ink-gray-5"> :class="{
{{ __('Select a slot') }} 'border-outline-gray-4 text-ink-gray-9':
</div> evaluation.date == row.date &&
<div class="grid grid-cols-2 gap-2"> evaluation.start_time == slot.start_time,
<div v-for="slot in slots.data"> }"
<div >
class="text-base text-center border rounded-md text-ink-gray-8 bg-surface-gray-3 p-2 cursor-pointer" {{ formatTime(slot.start_time) }} -
@click="saveSlot(slot)" {{ formatTime(slot.end_time) }}
:class="{ </div>
'border-outline-gray-4':
evaluation.start_time == slot.start_time,
}"
>
{{ formatTime(slot.start_time) }} -
{{ formatTime(slot.end_time) }}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div <div v-else class="text-ink-red-3">
v-else-if="evaluation.course && evaluation.date" {{ __('No slots available for the selected course.') }}
class="text-sm italic text-ink-red-4"
>
{{ __('No slots available for this date.') }}
</div> </div>
</div> </div>
</template> </template>
@@ -67,14 +64,15 @@
</template> </template>
<script setup> <script setup>
import { import {
call,
createResource,
dayjs, dayjs,
Dialog, Dialog,
createResource,
Select,
FormControl, FormControl,
toast, toast,
} from 'frappe-ui' } from 'frappe-ui'
import { reactive, watch, inject } from 'vue' import { ref, watch, inject } from 'vue'
import { Calendar } from 'lucide-vue-next'
import { formatTime } from '@/utils/' import { formatTime } from '@/utils/'
const user = inject('$user') const user = inject('$user')
@@ -96,7 +94,7 @@ const props = defineProps({
}, },
}) })
const evaluation = reactive({ const evaluation = ref({
course: '', course: '',
date: '', date: '',
start_time: '', start_time: '',
@@ -120,21 +118,36 @@ const createEvaluation = createResource({
}) })
function submitEvaluation(close) { function submitEvaluation(close) {
createEvaluation.submit(evaluation, { call('frappe.client.insert', {
doc: {
doctype: 'LMS Certificate Request',
batch_name: evaluation.value.batch,
...evaluation.value,
},
})
.then(() => {
evaluations.value.reload()
close()
})
.catch((err) => {
console.log(err.messages?.[0] || err)
toast.warning(__(err.messages?.[0] || err))
})
/* createEvaluation.submit(evaluation.value, {
validate() { validate() {
if (!evaluation.course) { if (!evaluation.value.course) {
return 'Please select a course.' return 'Please select a course.'
} }
if (!evaluation.date) { if (!evaluation.value.date) {
return 'Please select a date.' return 'Please select a date.'
} }
if (!evaluation.start_time) { if (!evaluation.value.start_time) {
return 'Please select a slot.' return 'Please select a slot.'
} }
if (dayjs(evaluation.date).isBefore(dayjs(), 'day')) { if (dayjs(evaluation.value.date).isBefore(dayjs(), 'day')) {
return 'Please select a future date.' return 'Please select a future date.'
} }
if (dayjs(evaluation.date).isAfter(dayjs(props.endDate), 'day')) { if (dayjs(evaluation.value.date).isAfter(dayjs(props.endDate), 'day')) {
return `Please select a date before the end date ${dayjs( return `Please select a date before the end date ${dayjs(
props.endDate props.endDate
).format('DD MMMM YYYY')}.` ).format('DD MMMM YYYY')}.`
@@ -148,7 +161,7 @@ function submitEvaluation(close) {
console.log(err.messages?.[0] || err) console.log(err.messages?.[0] || err)
toast.warning(__(err.messages?.[0] || err), { duration: 10000 }) toast.warning(__(err.messages?.[0] || err), { duration: 10000 })
}, },
}) }) */
} }
const getCourses = () => { const getCourses = () => {
@@ -163,7 +176,7 @@ const getCourses = () => {
} }
if (courses.length === 1) { if (courses.length === 1) {
evaluation.course = courses[0].value evaluation.value.course = courses[0].value
} }
return courses return courses
@@ -174,13 +187,12 @@ const slots = createResource({
makeParams(values) { makeParams(values) {
return { return {
course: values.course, course: values.course,
date: values.date,
batch: props.batch, batch: props.batch,
} }
}, },
}) })
watch( /* watch(
() => evaluation.date, () => evaluation.date,
(date) => { (date) => {
evaluation.start_time = '' evaluation.start_time = ''
@@ -189,19 +201,18 @@ watch(
} }
} }
) )
*/
watch( watch(
() => evaluation.course, () => evaluation.value.course,
(course) => { (course) => {
evaluation.date = '' slots.reload(evaluation.value)
evaluation.start_time = ''
slots.reset()
} }
) )
const saveSlot = (slot) => { const saveSlot = (slot, row) => {
evaluation.start_time = slot.start_time evaluation.value.start_time = slot.start_time
evaluation.end_time = slot.end_time evaluation.value.end_time = slot.end_time
evaluation.day = slot.day evaluation.value.date = row.date
evaluation.value.day = row.day
} }
</script> </script>

View File

@@ -144,6 +144,20 @@
</span> </span>
</div> </div>
</div> </div>
<div
v-if="batch.data.evaluation_end_date && isStudent"
class="text-sm leading-5 bg-surface-amber-1 text-ink-amber-3 p-2 rounded-md mb-10"
>
{{ __('The last day to schedule your evaluations is ') }}
<span class="font-medium">
{{
dayjs(batch.data.evaluation_end_date).format('DD MMMM YYYY')
}} </span
>.
{{
__('Please make sure to schedule your evaluation before this date.')
}}
</div>
<div v-if="dayjs().isSameOrAfter(dayjs(batch.data.start_date))"> <div v-if="dayjs().isSameOrAfter(dayjs(batch.data.start_date))">
<div class="text-ink-gray-7 font-semibold mb-2"> <div class="text-ink-gray-7 font-semibold mb-2">
{{ __('Feedback') }} {{ __('Feedback') }}

View File

@@ -6,7 +6,7 @@ from datetime import datetime
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import get_time, getdate from frappe.utils import add_days, get_time, getdate, nowdate
from lms.lms.utils import get_evaluator from lms.lms.utils import get_evaluator
@@ -58,33 +58,126 @@ class CourseEvaluator(Document):
@frappe.whitelist() @frappe.whitelist()
def get_schedule(course, date, batch=None): def get_schedule(course, batch=None):
evaluator = get_evaluator(course, batch) evaluator = get_evaluator(course, batch)
day = datetime.strptime(date, "%Y-%m-%d").strftime("%A") start_date = nowdate()
end_date = get_end_date(start_date, batch)
print(start_date, end_date)
all_slots = get_all_slots(evaluator, start_date, end_date)
booked_slots = get_booked_slots(evaluator, start_date, end_date)
all_slots = remove_booked_slots(all_slots, booked_slots)
return all_slots
all_slots = frappe.get_all(
def get_all_slots(evaluator, start_date, end_date):
schedule = get_evaluator_schedule(evaluator)
unavailable_dates = get_unavailable_dates(evaluator)
all_slots = []
current_date = getdate(start_date)
end_date = getdate(end_date)
while current_date <= end_date:
if current_date in unavailable_dates:
current_date = add_days(current_date, 1)
continue
day_of_week = current_date.strftime("%A")
slots_for_day = [x for x in schedule if x.day == day_of_week]
for slot in slots_for_day:
all_slots.append(
frappe._dict(
{
"day": day_of_week,
"date": current_date,
"start_time": slot.start_time,
"end_time": slot.end_time,
}
)
)
current_date = add_days(current_date, 1)
return all_slots
def get_evaluator_schedule(evaluator):
return frappe.get_all(
"Evaluator Schedule", "Evaluator Schedule",
filters={ filters={
"parent": evaluator, "parent": evaluator,
"day": day,
}, },
fields=["day", "start_time", "end_time"], fields=["day", "start_time", "end_time"],
order_by="start_time", order_by="start_time",
) )
booked_slots = frappe.get_all(
def get_booked_slots(evaluator, start_date, end_date):
date = ["between", [start_date, end_date]]
return frappe.get_all(
"LMS Certificate Request", "LMS Certificate Request",
filters={ filters={
"evaluator": evaluator, "evaluator": evaluator,
"date": date, "date": date,
"status": ["!=", "Cancelled"], "status": ["!=", "Cancelled"],
}, },
fields=["start_time", "day"], fields=["start_time", "day", "date"],
) )
for slot in booked_slots:
same_slot = [x for x in all_slots if x.start_time == slot.start_time and x.day == slot.day]
if len(same_slot):
all_slots.remove(same_slot[0])
return all_slots def remove_booked_slots(all_slots, booked_slots):
slots_to_remove = []
for slot in all_slots:
for booked in booked_slots:
if slot.date == booked.date and slot.start_time == booked.start_time:
slots_to_remove.append(slot)
for slot in slots_to_remove:
all_slots.remove(slot)
return group_slots_by_date(all_slots)
def group_slots_by_date(all_slots):
slots_by_date = []
dates_included = set()
for slot in all_slots:
date_str = slot.get("date").strftime("%Y-%m-%d")
if date_str not in dates_included:
slots_by_date.append({"date": date_str, "day": slot.day, "slots": []})
dates_included.add(date_str)
for date_slot in slots_by_date:
if date_slot.get("date") == date_str:
date_slot.get("slots").append(
{
"start_time": slot.get("start_time"),
"end_time": slot.get("end_time"),
}
)
return slots_by_date
def get_evaluator_availability(evaluator):
return frappe.db.get_value(
"Course Evaluator", evaluator, ["unavailable_from", "unavailable_to"], as_dict=1
)
def get_unavailable_dates(evaluator):
availability = get_evaluator_availability(evaluator)
unavailable_dates = []
if availability.unavailable_from and availability.unavailable_to:
current_date = getdate(availability.unavailable_from)
end_date = getdate(availability.unavailable_to)
while current_date <= end_date:
unavailable_dates.append(current_date)
current_date = add_days(current_date, 1)
return unavailable_dates
def get_end_date(start_date, batch=None):
end_date = add_days(start_date, 30)
if batch:
batch_end_date = frappe.db.get_value("LMS Batch", batch, "evaluation_end_date")
if batch_end_date:
end_date = getdate(batch_end_date)
return end_date