Skip to content

Commit a518dd4

Browse files
committed
Added feature for client to be able to reschedule
1 parent e98295a commit a518dd4

24 files changed

+1263
-78
lines changed

appointment/admin.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
from django import forms
1010
from django.contrib import admin
1111

12-
from .models import (
13-
Appointment, AppointmentRequest, Config, DayOff, EmailVerificationCode, Service, StaffMember, WorkingHours)
12+
from .models import (Appointment, AppointmentRequest, AppointmentRescheduleHistory, Config, DayOff,
13+
EmailVerificationCode, Service, StaffMember, WorkingHours)
1414

1515

1616
@admin.register(Service)
@@ -76,3 +76,18 @@ class WorkingHoursAdmin(admin.ModelAdmin):
7676
list_display = ('staff_member', 'day_of_week', 'start_time', 'end_time')
7777
search_fields = ('day_of_week',)
7878
list_filter = ('day_of_week', 'start_time', 'end_time')
79+
80+
81+
@admin.register(AppointmentRescheduleHistory)
82+
class AppointmentRescheduleHistoryAdmin(admin.ModelAdmin):
83+
list_display = (
84+
'appointment_request', 'date', 'start_time',
85+
'end_time', 'staff_member', 'reason_for_rescheduling', 'created_at'
86+
)
87+
search_fields = (
88+
'appointment_request__id_request', 'staff_member__user__first_name',
89+
'staff_member__user__last_name', 'reason_for_rescheduling'
90+
)
91+
list_filter = ('appointment_request__service', 'date', 'created_at')
92+
date_hierarchy = 'created_at'
93+
ordering = ('-created_at',)

appointment/forms.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
from phonenumber_field.formfields import PhoneNumberField
1212
from phonenumber_field.widgets import PhoneNumberPrefixWidget
1313

14-
from .models import Appointment, AppointmentRequest, DayOff, Service, StaffMember, WorkingHours
14+
from .models import (
15+
Appointment, AppointmentRequest, AppointmentRescheduleHistory, DayOff, Service, StaffMember,
16+
WorkingHours
17+
)
1518
from .utils.db_helpers import get_user_model
1619

1720

@@ -21,6 +24,15 @@ class Meta:
2124
fields = ('date', 'start_time', 'end_time', 'service', 'staff_member')
2225

2326

27+
class ReschedulingForm(forms.ModelForm):
28+
class Meta:
29+
model = AppointmentRescheduleHistory
30+
fields = ['reason_for_rescheduling']
31+
widgets = {
32+
'reason_for_rescheduling': forms.Textarea(attrs={'rows': 4, 'placeholder': 'Reason for rescheduling...'}),
33+
}
34+
35+
2436
class AppointmentForm(forms.ModelForm):
2537
phone = PhoneNumberField(widget=PhoneNumberPrefixWidget(initial='US'))
2638

appointment/services.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
from appointment.utils.db_helpers import (
2424
Appointment, AppointmentRequest, EmailVerificationCode, Service, StaffMember, WorkingHours, calculate_slots,
2525
calculate_staff_slots, check_day_off_for_staff, create_and_save_appointment, create_new_user,
26-
day_off_exists_for_date_range, exclude_booked_slots, get_all_appointments, get_all_staff_members,
26+
day_off_exists_for_date_range, exclude_booked_slots, exclude_pending_reschedules, get_all_appointments,
27+
get_all_staff_members,
2728
get_appointment_by_id, get_appointments_for_date_and_time, get_staff_member_appointment_list,
2829
get_staff_member_from_user_id_or_logged_in, get_times_from_config, get_user_by_email,
2930
get_working_hours_for_staff_and_day, parse_name, update_appointment_reminder, working_hours_exist)
@@ -304,7 +305,7 @@ def get_working_hours_and_days_off_context(request, btn_txt, form_name, form, us
304305
return context
305306

306307

307-
def save_appointment(appt, client_name, client_email, start_time, phone_number, client_address, service_id,
308+
def save_appointment(appt, client_name, client_email, start_time, phone_number, client_address, service_id, request,
308309
want_reminder=False, additional_info=None):
309310
"""Save an appointment's details.
310311
:return: The modified appointment.
@@ -329,7 +330,7 @@ def save_appointment(appt, client_name, client_email, start_time, phone_number,
329330

330331
# Update reminder here
331332
update_appointment_reminder(appointment=appt, new_date=appt_request.date, new_start_time=start_time,
332-
want_reminder=want_reminder)
333+
want_reminder=want_reminder, request=request)
333334

334335
appt_request.service = service
335336
appt_request.start_time = start_time
@@ -345,12 +346,13 @@ def save_appointment(appt, client_name, client_email, start_time, phone_number,
345346
return appt
346347

347348

348-
def save_appt_date_time(appt_start_time, appt_date, appt_id):
349+
def save_appt_date_time(appt_start_time, appt_date, appt_id, request):
349350
"""Save the date and time of an appointment request.
350351
351352
:param appt_start_time: The start time of the appointment request.
352353
:param appt_date: The date of the appointment request.
353354
:param appt_id: The ID of the appointment to modify.
355+
:param request: The request object.
354356
:return: The modified appointment.
355357
"""
356358
appt = Appointment.objects.get(id=appt_id)
@@ -373,7 +375,8 @@ def save_appt_date_time(appt_start_time, appt_date, appt_id):
373375
appt_date_obj = appt_date
374376

375377
# Update reminder here
376-
update_appointment_reminder(appointment=appt, new_date=appt_date_obj, new_start_time=appt_start_time_obj)
378+
update_appointment_reminder(appointment=appt, new_date=appt_date_obj, new_start_time=appt_start_time_obj,
379+
request=request)
377380

378381
# Modify and save appointment request details
379382
appt_request = appt.appointment_request
@@ -422,6 +425,7 @@ def get_available_slots_for_staff(date, staff_member):
422425

423426
slot_duration = datetime.timedelta(minutes=staff_member.get_slot_duration())
424427
slots = calculate_staff_slots(date, staff_member)
428+
slots = exclude_pending_reschedules(slots, staff_member, date)
425429
appointments = get_appointments_for_date_and_time(date, working_hours_dict['start_time'],
426430
working_hours_dict['end_time'], staff_member)
427431
slots = exclude_booked_slots(appointments, slots, slot_duration)
@@ -594,7 +598,7 @@ def create_new_appointment(data, request):
594598
'additional_info': data.get("additional_info", ""),
595599
'paid': False
596600
}
597-
appointment = create_and_save_appointment(appointment_request, client_data, appointment_data)
601+
appointment = create_and_save_appointment(appointment_request, client_data, appointment_data, request)
598602
appointment_list = convert_appointment_to_json(request, [appointment])
599603

600604
return json_response("Appointment created successfully.", custom_data={'appt': appointment_list})
@@ -613,7 +617,8 @@ def update_existing_appointment(data, request):
613617
client_address=data.get("client_address"),
614618
service_id=data.get("service_id"),
615619
want_reminder=want_reminder,
616-
additional_info=data.get("additional_info")
620+
additional_info=data.get("additional_info"),
621+
request=request,
617622
)
618623
appointments_json = convert_appointment_to_json(request, [appt])[0]
619624
return json_response(appt_updated_successfully, custom_data={'appt': appointments_json})

appointment/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,6 @@ def check_q_cluster():
6565
for warning in missing_conf:
6666
logger.warning(warning)
6767
return False
68-
print(f"Mising conf: {missing_conf}")
68+
print(f"Missing conf: {missing_conf}")
6969
# Both 'django_q' is installed and 'Q_CLUSTER' is configured
7070
return True
123 KB
Loading
4.57 KB
Loading
6.61 KB
Loading

appointment/static/js/appointments.js

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ const calendarEl = document.getElementById('calendar');
22
let nextAvailableDateSelector = $('.djangoAppt_next-available-date')
33
const body = $('body');
44
let nonWorkingDays = [];
5-
let selectedDate = null;
5+
let selectedDate = rescheduledDate || null;
66
let staffId = $('#staff_id').val() || null;
77
let previouslySelectedCell = null;
88

99
const calendar = new FullCalendar.Calendar(calendarEl, {
1010
initialView: 'dayGridMonth',
11+
initialDate: selectedDate,
1112
headerToolbar: {
1213
left: 'title',
1314
right: 'prev,today,next',
@@ -43,6 +44,9 @@ const calendar = new FullCalendar.Calendar(calendarEl, {
4344
selectedDate = info.dateStr;
4445
getAvailableSlots(info.dateStr, staffId);
4546
},
47+
datesSet: function (info) {
48+
highlightSelectedDate();
49+
},
4650
selectAllow: function (info) {
4751
const day = info.start.getDay(); // Get the day of the week (0 for Sunday, 6 for Saturday)
4852
if (nonWorkingDays.includes(day)) {
@@ -64,10 +68,20 @@ calendar.setOption('locale', locale);
6468
$(document).ready(function () {
6569
staffId = $('#staff_id').val() || null;
6670
calendar.render();
67-
const currentDate = moment.tz(timezone).format('YYYY-MM-DD');
71+
const currentDate = rescheduledDate || moment.tz(timezone).format('YYYY-MM-DD');
6872
getAvailableSlots(currentDate, staffId);
6973
});
7074

75+
function highlightSelectedDate() {
76+
setTimeout(function () {
77+
const dateCell = document.querySelector(`.fc-daygrid-day[data-date='${selectedDate}']`);
78+
if (dateCell) {
79+
dateCell.classList.add('selected-cell');
80+
previouslySelectedCell = dateCell;
81+
}
82+
}, 10);
83+
}
84+
7185
body.on('click', '.djangoAppt_btn-request-next-slot', function () {
7286
const serviceId = $(this).data('service-id');
7387
requestNextAvailableSlot(serviceId);
@@ -91,13 +105,22 @@ body.on('click', '.btn-submit-appointment', function () {
91105
const date = formattedDate.toISOString().slice(0, 10);
92106
const endTimeDate = new Date(formattedDate.getTime() + serviceDuration * 60000);
93107
const endTime = formatTime(endTimeDate);
94-
108+
const reasonForRescheduling = $('#reason_for_rescheduling').val();
95109
const form = $('.appointment-form');
96-
110+
let formAction = rescheduledDate ? appointmentRescheduleURL : appointmentRequestSubmitURL;
111+
form.attr('action', formAction);
112+
if (!form.find('input[name="appointment_request_id"]').length) {
113+
form.append($('<input>', {
114+
type: 'hidden',
115+
name: 'appointment_request_id',
116+
value: appointmentRequestId
117+
}));
118+
}
97119
form.append($('<input>', {type: 'hidden', name: 'date', value: date}));
98120
form.append($('<input>', {type: 'hidden', name: 'start_time', value: startTime}));
99121
form.append($('<input>', {type: 'hidden', name: 'end_time', value: endTime}));
100122
form.append($('<input>', {type: 'hidden', name: 'service', value: serviceId}));
123+
form.append($('<input>', {type: 'hidden', name: 'reason_for_rescheduling', value: reasonForRescheduling}));
101124
form.submit();
102125
} else {
103126
const warningContainer = $('.warning-message');
@@ -197,7 +220,7 @@ function getAvailableSlots(selectedDate, staffId = null) {
197220
// Check if 'staffId' is 'none', null, or undefined and display an error message
198221
if (staffId === 'none' || staffId === null || staffId === undefined) {
199222
console.log('No staff ID provided, displaying error message.');
200-
const errorMessage = $('<p class="djangoAppt_no-availability-text">'+ noStaffMemberSelectedTxt + '</p>');
223+
const errorMessage = $('<p class="djangoAppt_no-availability-text">' + noStaffMemberSelectedTxt + '</p>');
201224
errorMessageContainer.append(errorMessage);
202225
// Optionally disable the "submit" button here
203226
$('.btn-submit-appointment').attr('disabled', 'disabled');

appointment/tasks.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,13 @@
88
from django.utils.translation import gettext as _
99

1010
from appointment.email_sender import notify_admin, send_email
11-
from appointment.models import Appointment
1211
from appointment.logger_config import logger
12+
from appointment.models import Appointment
1313

1414

15-
def send_email_reminder(to_email, first_name, appointment_id):
15+
def send_email_reminder(to_email, first_name, reschedule_link, appointment_id):
1616
"""
1717
Send a reminder email to the client about the upcoming appointment.
18-
19-
:param to_email: The email address of the client.
20-
:param first_name: The first name of the client.
21-
:param appointment_id: The appointment ID.
22-
:return: None
2318
"""
2419

2520
# Fetch the appointment using appointment_id
@@ -28,6 +23,7 @@ def send_email_reminder(to_email, first_name, appointment_id):
2823
email_context = {
2924
'first_name': first_name,
3025
'appointment': appointment,
26+
'reschedule_link': reschedule_link,
3127
}
3228
send_email(
3329
recipient_list=[to_email], subject=_("Reminder: Upcoming Appointment"),

appointment/templates/appointment/appointments.html

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
<div class="container">
1616
<div class="djangoAppt_main-container">
1717
<div class="djangoAppt_body-container">
18-
<h1 class="page-title">{{ service.name }}</h1>
18+
<h1 class="page-title">
19+
{% if page_header %}{{ page_header }}{% else %}{{ service.name }}{% endif %} </h1>
1920
<small class="page-description">
2021
{% trans "Check out our availability and book the date and time that works for you" %}
2122
</small>
@@ -46,6 +47,13 @@ <h1 class="page-title">{{ service.name }}</h1>
4647

4748
</div>
4849
</div>
50+
{% if rescheduled_date %}
51+
<div class="form-group" style="margin-top: 10px">
52+
<label for="reason_for_rescheduling">{% trans "Reason for rescheduling" %}:</label>
53+
<textarea name="reason_for_rescheduling" id="reason_for_rescheduling"
54+
class="form-control" rows="1" required></textarea>
55+
</div>
56+
{% endif %}
4957
</div>
5058
<div class="djangoAppt_service-description">
5159
<form method="post" action="{% url 'appointment:appointment_request_submit' %}"
@@ -108,6 +116,10 @@ <h1 class="page-title">{{ service.name }}</h1>
108116
const getNonWorkingDaysURL = "{% url 'appointment:get_non_working_days_ajax' %}";
109117
const serviceId = "{{ service.id }}";
110118
const serviceDuration = parseInt("{{ service.duration.total_seconds }}") / 60;
119+
const rescheduledDate = "{{ rescheduled_date }}";
120+
const appointmentRequestId = "{{ ar_id_request }}";
121+
const appointmentRequestSubmitURL = "{% url 'appointment:appointment_request_submit' %}";
122+
const appointmentRescheduleURL = "{% url 'appointment:reschedule_appointment_submit' %}";
111123
</script>
112124
<script>
113125
const requestNonAvailableSlotBtnTxt = "{% trans 'Request next available slot' %}";

0 commit comments

Comments
 (0)