From 3308d036a56bea231706c677899e0cbf0eb52a4e Mon Sep 17 00:00:00 2001
From: Adams Pierre David <57180807+adamspd@users.noreply.github.com>
Date: Sat, 16 Dec 2023 22:00:10 +0100
Subject: [PATCH 1/7] Updated documentation's version
---
appointment/__init__.py | 2 +-
docs/history/readme_v2_1_1.md | 2 +-
docs/migration_guides/latest.md | 14 +++++++-------
docs/release_notes/latest.md | 10 +++++-----
4 files changed, 14 insertions(+), 14 deletions(-)
diff --git a/appointment/__init__.py b/appointment/__init__.py
index 2a6fce2..0d1cec8 100644
--- a/appointment/__init__.py
+++ b/appointment/__init__.py
@@ -4,5 +4,5 @@
__description__ = "Managing appointment scheduling with customizable slots, staff features, and conflict handling."
__package_name__ = "django-appointment"
__url__ = "https://github.com/adamspd/django-appointment"
-__version__ = "2.1.2"
+__version__ = "2.1.5"
__test_version__ = True
diff --git a/docs/history/readme_v2_1_1.md b/docs/history/readme_v2_1_1.md
index 2d10089..0f52f84 100644
--- a/docs/history/readme_v2_1_1.md
+++ b/docs/history/readme_v2_1_1.md
@@ -24,7 +24,7 @@ notes](https://github.com/adamspd/django-appointment/tree/main/docs/release_note
6. Staff members can now define their own configuration settings for the appointment system, such as slot duration,
working hours, and buffer time between appointments. However, only admins have the privilege to add/remove services.
-### Breaking Changes in version 2.1.2:
+### Breaking Changes in version 2.1.1:
- None
diff --git a/docs/migration_guides/latest.md b/docs/migration_guides/latest.md
index 153c506..f243ff2 100644
--- a/docs/migration_guides/latest.md
+++ b/docs/migration_guides/latest.md
@@ -1,10 +1,10 @@
-## Migration Guide for Version 2.1.2 🚀
+## Migration Guide for Version 2.1.5 🚀
-Version 2.1.2 of django-appointment is focused on enhancing functionality, documentation, and internationalization, with
+Version 2.1.5 of django-appointment is focused on enhancing functionality, documentation, and internationalization, with
no significant database schema changes introduced. This guide provides the steps to ensure a smooth upgrade from version
2.1.1 or any earlier versions post 2.0.0.
-### Steps for Upgrading to Version 2.1.2:
+### Steps for Upgrading to Version 2.1.5:
1. **Backup Your Database**:
- As a best practice, always back up your current database before performing an upgrade. This precaution ensures you
@@ -13,7 +13,7 @@ no significant database schema changes introduced. This guide provides the steps
2. **Update Package**:
- Upgrade to the latest version by running:
```bash
- pip install django-appointment==2.1.2
+ pip install django-appointment==2.1.5
```
3. **Run Migrations** (if any):
@@ -27,17 +27,17 @@ no significant database schema changes introduced. This guide provides the steps
4. **Review and Test**:
- After upgrading, thoroughly test your application to ensure all functionalities are working as expected with the
new version.
- - Pay special attention to features affected by the updates in version 2.1.2, as detailed in the release notes.
+ - Pay special attention to features affected by the updates in version 2.1.5, as detailed in the release notes.
### Troubleshooting:
- **Issues Post Migration**:
- - If you encounter issues after migration, consult the release notes for version 2.1.2 for specific updates that
+ - If you encounter issues after migration, consult the release notes for version 2.1.5 for specific updates that
might affect your setup.
- Check the Django logs for any error messages that can provide insights into issues.
### Important Notes 📝:
-- No database schema changes were introduced in version 2.1.2, so the migration process should be straightforward.
+- No database schema changes were introduced in version 2.1.5, so the migration process should be straightforward.
- As with any upgrade, testing in a development or staging environment before applying changes to your production
environment is highly recommended.
diff --git a/docs/release_notes/latest.md b/docs/release_notes/latest.md
index dd920ca..b11429a 100644
--- a/docs/release_notes/latest.md
+++ b/docs/release_notes/latest.md
@@ -1,12 +1,12 @@
# django-appointment 📦
-**v2.1.2 🆕**
+**v2.1.5 🆕**
-## ___Release Notes for Version 2.1.2___
+## ___Release Notes for Version 2.1.5___
## Introduction 📜
-Version 2.1.2 of django-appointment introduces a series of refinements and updates, enhancing both the functionality and
+Version 2.1.5 of django-appointment introduces a series of refinements and updates, enhancing both the functionality and
the user experience. This release focuses on improving documentation, workflow, community engagement, and
internationalization, alongside some crucial library updates and new dynamic features.
@@ -115,7 +115,7 @@ If you're upgrading from a previous version or installing for the first time, fo
### Installation 📥:
```bash
-pip install django-appointment==2.1.0
+pip install django-appointment==2.1.5
```
### Database Migration 🔧:
@@ -132,5 +132,5 @@ please refer to the provided resources.
## Conclusion 🎉
-Version 2.1.2 continues our commitment to providing a robust and user-friendly appointment management solution. With
+Version 2.1.5 continues our commitment to providing a robust and user-friendly appointment management solution. With
these updates, Django Appointment becomes more adaptable, secure, and community-focused.
From 895bf6b8347364cc2bf4bca39310074f31e93383 Mon Sep 17 00:00:00 2001
From: Adams Pierre David <57180807+adamspd@users.noreply.github.com>
Date: Mon, 1 Jan 2024 01:06:25 +0100
Subject: [PATCH 2/7] Feature and UI Enhancements for v2.1.5
- Implemented dynamic AJAX-based appointment management functionalities.
- Revamped user interface with updated staff_index.js for enhanced interactivity and mobile responsiveness.
- Improved security measures and data handling in appointment processing.
- Added new methods for appointment data representation in dictionary format.
- Introduced dynamic labeling feature for appointment pages with app_offered_by_label.
- Updated documentation and workflow processes for better clarity and efficiency.
- Strengthened community engagement with new community guidelines and issue templates.
- Performed crucial library updates and applied security patches.
- Refined translation inconsistencies for improved internationalization.
- Added endpoint for deleting appointments.
- Fixed access issue for staff members without a staff profile in the appointment page.
- Various CSS and JavaScript optimizations for a more responsive and user-friendly interface.
- Starting to update translations.
---
.gitignore | 1 +
appointment/models.py | 16 +
appointment/services.py | 13 +-
appointment/static/css/app_admin/admin.css | 53 ++
appointment/static/css/appointments.css | 455 +----------
appointment/static/css/appt-common.css | 452 +++++++++++
.../static/js/app_admin/staff_index.js | 732 ++++++++++++------
appointment/static/js/appointments.js | 4 +-
appointment/static/js/modal/confirm_modal.js | 15 -
appointment/static/js/modal/error_modal.js | 2 +-
appointment/static/js/modal/show_modal.js | 25 +
.../administration/display_appointment.html | 4 +-
.../administration/service_list.html | 4 +-
.../templates/administration/staff_index.html | 213 ++++-
.../administration/user_profile.html | 10 +-
.../templates/appointment/appointments.html | 7 +-
.../templates/modal/confirm_modal.html | 4 +-
.../templates/modal/event_details_modal.html | 5 +
appointment/utils/date_time.py | 2 +-
appointment/utils/db_helpers.py | 4 +-
appointment/utils/error_codes.py | 2 +
appointment/utils/json_context.py | 12 +-
appointment/views_admin.py | 145 +++-
appointments/settings.py | 15 +-
appointments/urls.py | 6 +-
docs/release_notes/latest.md | 24 +-
26 files changed, 1448 insertions(+), 777 deletions(-)
create mode 100644 appointment/static/css/app_admin/admin.css
create mode 100644 appointment/static/css/appt-common.css
delete mode 100644 appointment/static/js/modal/confirm_modal.js
create mode 100644 appointment/static/js/modal/show_modal.js
diff --git a/.gitignore b/.gitignore
index b5fd1f6..11323c0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -177,3 +177,4 @@ cython_debug/
**/.DS_Store/**
**/migrations/**
/services/
+locale/
diff --git a/appointment/models.py b/appointment/models.py
index 892020a..8e95bfa 100644
--- a/appointment/models.py
+++ b/appointment/models.py
@@ -526,6 +526,22 @@ def is_valid_date(appt_date, start_time, staff_member, current_appointment_id, w
def is_owner(self, staff_user_id):
return self.appointment_request.staff_member.user.id == staff_user_id
+ def to_dict(self):
+ return {
+ "id": self.id,
+ "client_name": self.client.get_full_name(),
+ "client_email": self.client.email,
+ "start_time": self.appointment_request.start_time.strftime('%Y-%m-%d %H:%M'),
+ "end_time": self.appointment_request.end_time.strftime('%Y-%m-%d %H:%M'),
+ "service_name": self.appointment_request.service.name,
+ "address": self.address,
+ "want_reminder": self.want_reminder,
+ "additional_info": self.additional_info,
+ "paid": self.paid,
+ "amount_to_pay": self.amount_to_pay,
+ "id_request": self.id_request,
+ }
+
class Config(models.Model):
"""
diff --git a/appointment/services.py b/appointment/services.py
index 6151cf6..9b9986a 100644
--- a/appointment/services.py
+++ b/appointment/services.py
@@ -300,16 +300,9 @@ def get_working_hours_and_days_off_context(request, btn_txt, form_name, form, us
return context
-def save_appointment(appt, client_name, client_email, start_time, phone_number, client_address, service_id):
+def save_appointment(appt, client_name, client_email, start_time, phone_number, client_address, service_id,
+ want_reminder=False, additional_info=None):
"""Save an appointment's details.
-
- :param appt: The appointment to modify.
- :param client_name: The name of the client.
- :param client_email: The email of the client.
- :param start_time: The start time of the appointment.
- :param phone_number: The phone number of the client.
- :param client_address: The address of the client.
- :param service_id: The ID of the service.
:return: The modified appointment.
"""
# Modify and save client details
@@ -335,6 +328,8 @@ def save_appointment(appt, client_name, client_email, start_time, phone_number,
# Modify and save appointment details
appt.phone = phone_number
appt.address = client_address
+ appt.want_reminder = want_reminder
+ appt.additional_info = additional_info
appt.save()
return appt
diff --git a/appointment/static/css/app_admin/admin.css b/appointment/static/css/app_admin/admin.css
new file mode 100644
index 0000000..ea5b6db
--- /dev/null
+++ b/appointment/static/css/app_admin/admin.css
@@ -0,0 +1,53 @@
+/* Hide scrollbar for day grid month view on small screens */
+
+@media (max-width: 500px) {
+ .djangoAppt_no-events {
+ margin-bottom: 10px;
+ }
+
+ .djangoAppt_btn-new-event {
+ padding: 5px 8px !important;
+ font-size: 13px !important;
+ }
+
+ .fc-dayGridMonth-view .fc-scroller {
+ overflow: hidden !important;
+ }
+
+ .modal-content {
+ margin: 0 auto !important;
+ }
+
+ .modal-footer {
+ text-align: left !important;
+ flex-wrap: inherit !important;
+ justify-content: center !important;
+ align-content: flex-start;
+ }
+
+ #eventDetailsModal .btn {
+ margin-right: 2px !important;
+ font-size: 13px !important;
+ }
+
+ #eventModalBody, #serviceSelect {
+ font-size: 13px !important;
+ }
+
+ #eventModalLabel {
+ font-size: 15px !important;
+ }
+}
+
+/* Hide scrollbar for time grid day view on larger screens */
+@media (min-width: 450px) {
+ .fc-timeGridDay-view .fc-scroller {
+ overflow: hidden !important;
+ }
+}
+
+@media (min-width: 600px) {
+ .fc-scroller {
+ overflow: hidden !important;
+ }
+}
\ No newline at end of file
diff --git a/appointment/static/css/appointments.css b/appointment/static/css/appointments.css
index 45ae99b..aa75c54 100644
--- a/appointment/static/css/appointments.css
+++ b/appointment/static/css/appointments.css
@@ -1,447 +1,8 @@
-.djangoAppt_main-container {
- margin: 20px auto;
- max-width: 1200px;
- padding: 20px;
- background-color: rgba(248, 249, 250, 0.4);
- border-radius: 5px;
- box-shadow: 0 0 2px rgba(0, 0, 0, 0.1);
-}
-
-.djangoAppt_body-container {
- margin: 0 auto;
- max-width: 1120px;
- padding: 0 15px;
-}
-
-.djangoAppt_page-body {
- display: flex;
- flex-direction: row;
- margin-top: 50px;
-}
-
-.djangoAppt_appointment-calendar {
- flex: 3;
- padding: 20px;
- background-color: #fff;
- border-radius: 5px;
- box-shadow: 0 0 2px rgba(0, 0, 0, 0.1);
-}
-
-.djangoAppt_service-description {
- flex: 1;
- margin-left: 20px;
- padding: 20px;
- background-color: #fff;
- border-radius: 5px;
- box-shadow: 0 0 2px rgba(0, 0, 0, 0.1);
-}
-
-.djangoAppt_second-part {
- margin-top: 10px;
-}
-
-.djangoAppt_calendar-and-slot {
- margin-top: 20px;
- display: flex;
-}
-
-.djangoAppt_service-description-content {
- margin-top: 20px;
- color: black;
-}
-
-.djangoAppt_item-name {
- color: black;
- font-weight: bold;
- font-size: 20px;
- margin-bottom: 10px;
-}
-
-.djangoAppt_calendar {
- flex: 3;
-}
-
-.djangoAppt_slot {
- flex: 2;
-}
-
-.djangoAppt_appointment-calendar-title-timezone {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 10px;
-}
-
-.djangoAppt_title {
- flex: 1;
- font-weight: bold;
- font-size: 20px;
-}
-
-.djangoAppt_timezone-details {
- flex: 1;
- text-align: right;
- font-size: 16px;
- color: #333;
-}
-
-.fc-day {
- font-size: 12px;
-}
-
-.fc-daygrid-day-frame {
- height: 20px;
-}
-
-a {
- color: #0c042c;
-}
-
-.djangoAppt_slot {
- margin-left: 20px;
-}
-
-.djangoAppt_slot-list {
- columns: 2;
- -webkit-columns: 2;
- -moz-columns: 2;
- margin-top: 10px;
- padding-left: 10px !important;
-}
-
-#slot-list li {
- list-style-type: none;
- text-align: center;
-}
-
-.djangoAppt_appointment-slot {
- border: 1px solid #ccc;
- background-color: rgba(0, 48, 124, 0.95);
- color: #fff;
- padding: 7px;
- margin-bottom: 6px;
- cursor: pointer;
- border-radius: 4px;
-}
-
-.djangoAppt_appointment-slot:hover {
- background-color: #fff;
- color: rgba(42, 42, 42, 0.82);
-}
-
-.selected {
- background-color: #fff;
- color: rgba(42, 42, 42, 0.82);
-}
-
-.djangoAppt_next-available-date {
- font-size: 16px;
- font-weight: bold;
- color: #333;
- margin-top: 10px;
- margin-left: 10px;
- padding: 8px;
- background-color: #f8f9fa;
- border: 1px solid #dee2e6;
- border-radius: 5px;
- width: fit-content;
-}
-
.fc-scroller {
overflow: hidden !important;
}
-/* Change the color of the buttons for the calendar */
-.fc-button {
- background-color: rgba(0, 48, 124, 0.95) !important;
- border-color: rgba(2, 76, 157, 0.85) !important;
- color: #fff !important;
-}
-
-/* Change the color of the buttons when hovered */
-.fc-button:hover {
- background-color: #025bbb !important;
- border-color: #0056b3;
-}
-
-/* Change the color of the buttons when active or focused */
-.fc-button:active, .fc-button:focus {
- background-color: #0759b2 !important;
- border-color: #145294;
-}
-
-.djangoAppt_date_chosen {
- margin-left: 5px;
- padding-left: 5px;
- font-size: 18px;
- color: #333;
- font-weight: bold;
-}
-
-.djangoAppt_btn-request-next-slot {
- margin-left: 10px;
- padding: 8px !important;
- margin-top: -30px;
-}
-
-.djangoAppt_no-availability-text {
- margin-left: 5px;
- padding-left: 5px;
- color: #f00;
- font-weight: bold;
-}
-
-.disabled-day {
- background-color: #ECECEC; /* light gray */
- opacity: 0.5;
- pointer-events: none; /* makes it unclickable */
-}
-
-/* responsive */
-
-/* CSS for screens larger than 1200px */
-@media (min-width: 1200px) {
- .djangoAppt_page-body {
- flex-direction: row;
- }
-
- .djangoAppt_appointment-calendar {
- flex: 3;
- padding: 20px;
- }
-
- .djangoAppt_service-description {
- flex: 1;
- margin-left: 20px;
- }
-
- .djangoAppt_calendar {
- flex: 3;
- }
-
- .djangoAppt_slot {
- flex: 2;
- }
-
- .djangoAppt_slot {
- margin-left: 20px;
- }
-}
-
-/* Select design */
-
-#staff_id {
- width: 100%;
- padding: 2px 4px;
- border: 1px solid rgba(255, 227, 227, 0.89);
- border-radius: 5px;
- background-color: #d4eaf5;
- color: #333;
- appearance: none; /* Remove default arrow icon in some browsers */
- -webkit-appearance: none; /* For Webkit browsers */
- -moz-appearance: none; /* For Firefox */
- cursor: pointer;
- outline: none;
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
- transition: background-color 0.3s;
- position: relative;
- font-size: 16px;
-}
-
-#staff_id:hover {
- background-color: #f6c6c6;
-}
-
-#staff_id:focus {
- background-color: #d1dbff;
-}
-
-/* Add a custom arrow icon using pseudo-elements */
-#staff_id::-ms-expand {
- display: none;
-}
-
-#staff_id::after {
- content: '\25BC'; /* Unicode arrow character */
- position: absolute;
- top: 50%;
- right: 10px;
- transform: translateY(-50%);
- pointer-events: none; /* Make sure clicks pass through */
- color: #333;
-}
-
-/* Styling the options when hovered */
-#staff_id option:hover {
- background-color: #f0f0f0;
- color: #333;
-}
-
-/* Styling the options when they are active (clicked or selected with keyboard) */
-#staff_id option:active, #staff_id option:checked {
- background-color: #e0e0e0;
- color: #333;
-}
-
-/* This changes the background color of the option elements when the select is open */
-#staff_id option {
- background-color: #f8f8f8;
- color: #333;
-}
-
-/* For Firefox - to change the background color of the dropdown */
-#staff_id:-moz-focusring {
- color: transparent;
- text-shadow: 0 0 0 #333;
-}
-
-
-/* CSS for screens smaller than 1200px */
-@media (max-width: 1199px) {
- .djangoAppt_page-body {
- flex-direction: column;
- }
-
- .djangoAppt_appointment-calendar {
- flex: 1;
- padding: 10px;
- }
-
- .djangoAppt_service-description {
- flex: 1;
- margin-left: 0;
- margin-top: 20px;
- }
-
- .djangoAppt_calendar {
- flex: 1;
- }
-
- .djangoAppt_slot {
- flex: 1;
- }
-
- .djangoAppt_slot {
- margin-left: 10px;
- }
-}
-
-/* CSS for screens smaller or equal to 768px */
-@media (max-width: 768px) {
- .djangoAppt_main-container {
- padding: 8px;
- }
-
- .djangoAppt_body-container {
- padding: 8px;
- }
-
- .djangoAppt_appointment-calendar {
- flex: 1;
- padding: 10px;
- }
-
- .djangoAppt_service-description {
- flex: 1;
- margin-left: 0;
- margin-top: 20px;
- }
-
- .djangoAppt_calendar-and-slot {
- display: grid;
- }
-
- .djangoAppt_slot {
- margin-top: 40px;
- }
-
- .djangoAppt_slot-list, .djangoAppt_date_chosen, .djangoAppt_no-availability-text {
- margin-left: 0;
- padding-left: 0;
- }
-
- .djangoAppt_btn-request-next-slot {
- margin-left: 0;
- }
-
- /* Reduce font size for smaller screens */
- .djangoAppt_title, .djangoAppt_item-name, .djangoAppt_date_chosen, .djangoAppt_next-available-date {
- font-size: 16px;
- }
-
- .djangoAppt_timezone-details {
- font-size: 14px;
- }
-
- .fc-day {
- font-size: 10px;
- }
-}
-
-/* CSS for screens smaller or equal to 768px */
@media (max-width: 450px) {
- .djangoAppt_main-container {
- padding: 3px;
- }
-
- .djangoAppt_body-container {
- padding: 3px;
- }
-
- .djangoAppt_appointment-calendar {
- flex: 1;
- padding: 5px;
- }
-
- .djangoAppt_service-description {
- flex: 1;
- margin-left: 0;
- margin-top: 20px;
- }
-
- .djangoAppt_calendar-and-slot {
- display: grid;
- }
-
- .djangoAppt_slot {
- margin-top: 40px;
- }
-
- .djangoAppt_slot-list, .djangoAppt_date_chosen, .djangoAppt_no-availability-text {
- margin-left: 0 !important;
- padding-left: 0 !important;
- }
-
- .djangoAppt_btn-request-next-slot {
- margin-left: 0;
- }
-
- /* Reduce font size for smaller screens */
- .djangoAppt_title, .djangoAppt_item-name, .djangoAppt_date_chosen, .djangoAppt_next-available-date {
- font-size: 13px;
- }
-
- .djangoAppt_timezone-details, .error-message {
- font-size: 13px;
- }
-
- .fc-day {
- font-size: 11px;
- }
-
- .fc-toolbar-title {
- font-size: 14px !important;
- }
-
- .fc {
- font-size: 13px !important;
- }
-
- .fc, .fc-button {
- padding: .3em .45em !important;
- vertical-align: center !important;
- }
-
.fc-daygrid-day-events {
display: none !important;
margin: 0 !important;
@@ -450,17 +11,7 @@ a {
height: 0 !important;
}
- .djangoAppt_appointment-slot {
- padding: 5px;
- font-size: 13px;
- }
-
- .djangoAppt_service-description {
- font-size: 13px !important;
+ .fc, .fc-button {
+ padding: .3em .45em !important;
}
-}
-
-.selected-cell {
- background-color: #aaddff; /* or any color you prefer */
-}
-
+}
\ No newline at end of file
diff --git a/appointment/static/css/appt-common.css b/appointment/static/css/appt-common.css
new file mode 100644
index 0000000..d2056b3
--- /dev/null
+++ b/appointment/static/css/appt-common.css
@@ -0,0 +1,452 @@
+.djangoAppt_main-container {
+ margin: 20px auto;
+ max-width: 1200px;
+ padding: 20px;
+ background-color: rgba(248, 249, 250, 0.4);
+ border-radius: 5px;
+ box-shadow: 0 0 2px rgba(0, 0, 0, 0.1);
+}
+
+.djangoAppt_body-container {
+ margin: 0 auto;
+ max-width: 1120px;
+ padding: 0 15px;
+}
+
+.djangoAppt_page-body {
+ display: flex;
+ flex-direction: row;
+ margin-top: 50px;
+}
+
+.djangoAppt_appointment-calendar {
+ flex: 3;
+ padding: 20px;
+ background-color: #fff;
+ border-radius: 5px;
+ box-shadow: 0 0 2px rgba(0, 0, 0, 0.1);
+}
+
+.djangoAppt_service-description {
+ flex: 1;
+ margin-left: 20px;
+ padding: 20px;
+ background-color: #fff;
+ border-radius: 5px;
+ box-shadow: 0 0 2px rgba(0, 0, 0, 0.1);
+}
+
+.djangoAppt_second-part {
+ margin-top: 10px;
+}
+
+.djangoAppt_calendar-and-slot {
+ margin-top: 20px;
+ display: flex;
+}
+
+.djangoAppt_service-description-content {
+ margin-top: 20px;
+ color: black;
+}
+
+.djangoAppt_item-name {
+ color: black;
+ font-weight: bold;
+ font-size: 20px;
+ margin-bottom: 10px;
+}
+
+.djangoAppt_calendar {
+ flex: 3;
+}
+
+.djangoAppt_slot {
+ flex: 2;
+}
+
+.djangoAppt_appointment-calendar-title-timezone {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 10px;
+}
+
+.djangoAppt_title {
+ flex: 1;
+ font-weight: bold;
+ font-size: 20px;
+}
+
+.djangoAppt_timezone-details {
+ flex: 1;
+ text-align: right;
+ font-size: 16px;
+ color: #333;
+}
+
+.fc-day {
+ font-size: 12px;
+}
+
+.fc-daygrid-day-frame {
+ height: 20px;
+}
+
+a {
+ color: #0c042c;
+}
+
+.djangoAppt_slot {
+ margin-left: 20px;
+}
+
+.djangoAppt_slot-list {
+ columns: 2;
+ -webkit-columns: 2;
+ -moz-columns: 2;
+ margin-top: 10px;
+ padding-left: 10px !important;
+}
+
+#slot-list li {
+ list-style-type: none;
+ text-align: center;
+}
+
+.djangoAppt_appointment-slot {
+ border: 1px solid #ccc;
+ background-color: rgba(0, 48, 124, 0.95);
+ color: #fff;
+ padding: 7px;
+ margin-bottom: 6px;
+ cursor: pointer;
+ border-radius: 4px;
+}
+
+.djangoAppt_appointment-slot:hover {
+ background-color: #fff;
+ color: rgba(42, 42, 42, 0.82);
+}
+
+.selected {
+ background-color: #fff;
+ color: rgba(42, 42, 42, 0.82);
+}
+
+.djangoAppt_next-available-date {
+ font-size: 16px;
+ font-weight: bold;
+ color: #333;
+ margin-top: 10px;
+ margin-left: 10px;
+ padding: 8px;
+ background-color: #f8f9fa;
+ border: 1px solid #dee2e6;
+ border-radius: 5px;
+ width: fit-content;
+}
+
+/* Change the color of the buttons for the calendar */
+.fc-button {
+ background-color: rgba(0, 48, 124, 0.95) !important;
+ border-color: rgba(2, 76, 157, 0.85) !important;
+ color: #fff !important;
+}
+
+/* Change the color of the buttons when hovered */
+.fc-button:hover {
+ background-color: #025bbb !important;
+ border-color: #0056b3;
+}
+
+/* Change the color of the buttons when active or focused */
+.fc-button:active, .fc-button:focus {
+ background-color: #0759b2 !important;
+ border-color: #145294;
+}
+
+.djangoAppt_date_chosen {
+ margin-left: 5px;
+ padding-left: 5px;
+ font-size: 18px;
+ color: #333;
+ font-weight: bold;
+}
+
+.djangoAppt_btn-request-next-slot {
+ margin-left: 10px;
+ padding: 8px !important;
+ margin-top: -30px;
+}
+
+.djangoAppt_no-availability-text {
+ margin-left: 5px;
+ padding-left: 5px;
+ color: #f00;
+ font-weight: bold;
+}
+
+.disabled-day {
+ background-color: #ECECEC; /* light gray */
+ opacity: 0.5;
+ pointer-events: none; /* makes it unclickable */
+}
+
+/* responsive */
+
+/* CSS for screens larger than 1200px */
+@media (min-width: 1200px) {
+ .djangoAppt_page-body {
+ flex-direction: row;
+ }
+
+ .djangoAppt_appointment-calendar {
+ flex: 3;
+ padding: 20px;
+ }
+
+ .djangoAppt_service-description {
+ flex: 1;
+ margin-left: 20px;
+ }
+
+ .djangoAppt_calendar {
+ flex: 3;
+ }
+
+ .djangoAppt_slot {
+ flex: 2;
+ }
+
+ .djangoAppt_slot {
+ margin-left: 20px;
+ }
+}
+
+/* Select design */
+
+#staff_id {
+ width: 100%;
+ padding: 2px 4px;
+ border: 1px solid rgba(255, 227, 227, 0.89);
+ border-radius: 5px;
+ background-color: #d4eaf5;
+ color: #333;
+ appearance: none; /* Remove default arrow icon in some browsers */
+ -webkit-appearance: none; /* For Webkit browsers */
+ -moz-appearance: none; /* For Firefox */
+ cursor: pointer;
+ outline: none;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+ transition: background-color 0.3s;
+ position: relative;
+ font-size: 16px;
+}
+
+#staff_id:hover {
+ background-color: #f6c6c6;
+}
+
+#staff_id:focus {
+ background-color: #d1dbff;
+}
+
+/* Add a custom arrow icon using pseudo-elements */
+#staff_id::-ms-expand {
+ display: none;
+}
+
+#staff_id::after {
+ content: '\25BC'; /* Unicode arrow character */
+ position: absolute;
+ top: 50%;
+ right: 10px;
+ transform: translateY(-50%);
+ pointer-events: none; /* Make sure clicks pass through */
+ color: #333;
+}
+
+/* Styling the options when hovered */
+#staff_id option:hover {
+ background-color: #f0f0f0;
+ color: #333;
+}
+
+/* Styling the options when they are active (clicked or selected with keyboard) */
+#staff_id option:active, #staff_id option:checked {
+ background-color: #e0e0e0;
+ color: #333;
+}
+
+/* This changes the background color of the option elements when the select is open */
+#staff_id option {
+ background-color: #f8f8f8;
+ color: #333;
+}
+
+/* For Firefox - to change the background color of the dropdown */
+#staff_id:-moz-focusring {
+ color: transparent;
+ text-shadow: 0 0 0 #333;
+}
+
+
+/* CSS for screens smaller than 1200px */
+@media (max-width: 1199px) {
+ .djangoAppt_page-body {
+ flex-direction: column;
+ }
+
+ .djangoAppt_appointment-calendar {
+ flex: 1;
+ padding: 10px;
+ }
+
+ .djangoAppt_service-description {
+ flex: 1;
+ margin-left: 0;
+ margin-top: 20px;
+ }
+
+ .djangoAppt_calendar {
+ flex: 1;
+ }
+
+ .djangoAppt_slot {
+ flex: 1;
+ }
+
+ .djangoAppt_slot {
+ margin-left: 10px;
+ }
+}
+
+/* CSS for screens smaller or equal to 768px */
+@media (max-width: 768px) {
+ .djangoAppt_main-container {
+ padding: 8px;
+ }
+
+ .djangoAppt_body-container {
+ padding: 8px;
+ }
+
+ .djangoAppt_appointment-calendar {
+ flex: 1;
+ padding: 10px;
+ }
+
+ .djangoAppt_service-description {
+ flex: 1;
+ margin-left: 0;
+ margin-top: 20px;
+ }
+
+ .djangoAppt_calendar-and-slot {
+ display: grid;
+ }
+
+ .djangoAppt_slot {
+ margin-top: 40px;
+ }
+
+ .djangoAppt_slot-list, .djangoAppt_date_chosen, .djangoAppt_no-availability-text {
+ margin-left: 0;
+ padding-left: 0;
+ }
+
+ .djangoAppt_btn-request-next-slot {
+ margin-left: 0;
+ }
+
+ /* Reduce font size for smaller screens */
+ .djangoAppt_title, .djangoAppt_item-name, .djangoAppt_date_chosen, .djangoAppt_next-available-date {
+ font-size: 16px;
+ }
+
+ .djangoAppt_timezone-details {
+ font-size: 14px;
+ }
+
+ .fc-day {
+ font-size: 10px;
+ }
+}
+
+/* CSS for screens smaller or equal to 768px */
+@media (max-width: 450px) {
+ .djangoAppt_main-container {
+ padding: 3px;
+ }
+
+ .djangoAppt_body-container {
+ padding: 3px;
+ }
+
+ .djangoAppt_appointment-calendar {
+ flex: 1;
+ padding: 5px;
+ }
+
+ .djangoAppt_service-description {
+ flex: 1;
+ margin-left: 0;
+ margin-top: 20px;
+ }
+
+ .djangoAppt_calendar-and-slot {
+ display: grid;
+ }
+
+ .djangoAppt_slot {
+ margin-top: 40px;
+ }
+
+ .djangoAppt_slot-list, .djangoAppt_date_chosen, .djangoAppt_no-availability-text {
+ margin-left: 0 !important;
+ padding-left: 0 !important;
+ }
+
+ .djangoAppt_btn-request-next-slot {
+ margin-left: 0;
+ }
+
+ /* Reduce font size for smaller screens */
+ .djangoAppt_title, .djangoAppt_item-name, .djangoAppt_date_chosen, .djangoAppt_next-available-date {
+ font-size: 13px;
+ }
+
+ .djangoAppt_timezone-details, .error-message {
+ font-size: 13px;
+ }
+
+ .fc-day {
+ font-size: 11px;
+ }
+
+ .fc-toolbar-title {
+ font-size: 14px !important;
+ }
+
+ .fc {
+ font-size: 13px !important;
+ }
+
+ .fc, .fc-button {
+ vertical-align: center !important;
+ }
+
+ .djangoAppt_appointment-slot {
+ padding: 5px;
+ font-size: 13px;
+ }
+
+ .djangoAppt_service-description {
+ font-size: 13px !important;
+ }
+}
+
+.selected-cell {
+ background-color: #aaddff; /* or any color you prefer */
+}
\ No newline at end of file
diff --git a/appointment/static/js/app_admin/staff_index.js b/appointment/static/js/app_admin/staff_index.js
index 182c564..a5d70a5 100644
--- a/appointment/static/js/app_admin/staff_index.js
+++ b/appointment/static/js/app_admin/staff_index.js
@@ -1,23 +1,50 @@
// Constants
-const MOBILE_WIDTH = 450;
-const SMALL_TABLET_WIDTH = 650;
-const TABLET_WIDTH = 767;
-const MEDIUM_WIDTH = 991;
-
-// Global variables
-let eventIdSelected = null;
-let calendar = null;
-let isEditing = false;
+const Constants = {
+ MOBILE_WIDTH_SMALL: 350,
+ MOBILE_WIDTH: 450,
+ SMALL_TABLET_WIDTH: 650,
+ TABLET_WIDTH: 767,
+ MEDIUM_WIDTH: 991,
+ DEFAULT_START_TIME: '09:00'
+};
+
+// Application State
+const AppState = {
+ eventIdSelected: null, calendar: null, isEditingAppointment: false, isCreating: false,
+};
document.addEventListener("DOMContentLoaded", initializeCalendar);
window.addEventListener('resize', updateCalendarConfig);
+document.getElementById('eventDetailsModal').addEventListener('keypress', function (event) {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ document.getElementById('eventSubmitBtn').click();
+ }
+});
+
+// Throttle resize events
+let resizeTimeout;
+window.addEventListener('resize', function () {
+ if (resizeTimeout) {
+ clearTimeout(resizeTimeout);
+ }
+ resizeTimeout = setTimeout(function () {
+ initializeCalendar()
+ }, 500); // Only rerender at most, every 100ms
+});
+
+document.addEventListener("DOMContentLoaded", function () {
+ // Wait for a 50ms after the DOM is ready before initializing the calendar
+ setTimeout(initializeCalendar, 50);
+});
+
function initializeCalendar() {
const formattedAppointments = formatAppointmentsForCalendar(appointments);
const calendarEl = document.getElementById('calendar');
- calendar = new FullCalendar.Calendar(calendarEl, getCalendarConfig(formattedAppointments));
- calendar.setOption('locale', locale);
- calendar.render();
+ AppState.calendar = new FullCalendar.Calendar(calendarEl, getCalendarConfig(formattedAppointments));
+ AppState.calendar.setOption('locale', locale);
+ AppState.calendar.render();
}
function formatAppointmentsForCalendar(appointments) {
@@ -32,8 +59,23 @@ function formatAppointmentsForCalendar(appointments) {
}
function updateCalendarConfig() {
- calendar.setOption('headerToolbar', getHeaderToolbarConfig());
- calendar.setOption('height', getCalendarHeight());
+ AppState.calendar.setOption('headerToolbar', getHeaderToolbarConfig());
+ AppState.calendar.setOption('height', getCalendarHeight());
+}
+
+function mobileCheck() {
+ return window.innerWidth < Constants.MOBILE_WIDTH;
+}
+
+function tabletCheck() {
+ return window.innerWidth < Constants.TABLET_WIDTH;
+}
+
+function getEventDisplayedStyle() {
+ if (mobileCheck()) {
+ return "list-item";
+ }
+ return "block";
}
function getCalendarConfig(events) {
@@ -54,42 +96,110 @@ function getCalendarConfig(events) {
prevYear: 'fa-angle-double-left',
nextYear: 'fa-angle-double-right'
},
+ defaultView: mobileCheck() ? "basicDay" : "dayGridMonth",
selectable: true,
events: events,
- eventDisplay: 'block',
+ eventDisplay: getEventDisplayedStyle(),
timeZone: timezone,
eventClick: async function (info) {
- eventIdSelected = info.event.id;
- await showEventModal(info.event);
+ AppState.eventIdSelected = info.event.id;
+ await showEventModal(info.event.id, false, false);
},
dateClick: function (info) {
- // ... your code for dateClick ...
+ // Retrieve events for the clicked date
+ const dateEvents = appointments
+ .filter(event => moment(info.date).isSame(event.start_time, 'day'))
+ .sort((a, b) => new Date(a.start_time) - new Date(b.start_time));
+
+ // Display events in a list below the calendar
+ displayEventList(dateEvents, info.date);
},
+
selectAllow: function (info) {
- // ... your code for selectAllow ...
},
dayCellClassNames: function (info) {
- // ... your code for dayCellClassNames ...
+ const day = info.date.getDay();
+ if (day === 0 || day === 6) { // 0 = Sunday, 6 = Saturday
+ return 'highlight-weekend';
+ }
+ return ''; // Return empty string for regular days
},
eventDrop: async function (info) {
await validateAndUpdateAppointmentDate(info.event, info.revert);
},
+ eventDidMount: function (info) {
+ // If it is a mobile view, we change the event to a dot
+ if (mobileCheck()) {
+ // Find the fc-daygrid-event-dot class within the event element
+ // and change its style to display as a dot
+ const dotEl = info.el.querySelector('.fc-daygrid-event-dot') || document.createElement('span');
+ dotEl.classList.add('fc-daygrid-event-dot');
+ dotEl.style.borderRadius = '50%';
+ dotEl.style.backgroundColor = info.event.backgroundColor;
+
+ // Clear the inner HTML of the event element and append the dot
+ info.el.innerHTML = '';
+ info.el.appendChild(dotEl);
+ }
+ },
+ dayCellDidMount: function (dayCell) {
+ // Check if the day is in the past
+ const currentDate = new Date();
+ currentDate.setHours(0, 0, 0, 0); // Reset time part to compare only dates
+
+ if (dayCell.date >= currentDate && !tabletCheck()) {
+ // Attach right-click event listener only if the day is not in the past
+ dayCell.el.addEventListener('contextmenu', function (event) {
+ event.preventDefault();
+ handleCalendarRightClick(event, dayCell.date);
+ });
+ }
+ },
};
}
+function displayEventList(events, date) {
+ let eventListHtml = '
' + eventsOnTxt + ' ' + moment(date).format('MMMM Do, YYYY') + '
';
+ eventListHtml += '
';
+
+ events.forEach(function (event) {
+ eventListHtml += `${event.service_name}
`;
+ eventListHtml += ` ${moment(event.start_time).format('h:mm a')} - ${moment(event.end_time).format('h:mm a')}
`;
+ eventListHtml += '
';
+ });
+
+ const date_obj = new Date(date.toISOString())
+
+ if (events.length === 0) {
+ eventListHtml += `` + noEventTxt +`
`;
+ }
+
+ eventListHtml += ``;
+
+ const eventListContainer = document.getElementById('event-list-container');
+ eventListContainer.innerHTML = eventListHtml;
+
+ // Add click event listeners to each event item
+ const eventItems = eventListContainer.getElementsByClassName('event-list-item-appt');
+ for (let item of eventItems) {
+ item.addEventListener('click', function () {
+ const eventId = this.getAttribute('data-event-id');
+ showEventModal(eventId, false, false).then(r => r);
+ });
+ }
+}
+
+
function getHeaderToolbarConfig() {
if (window.matchMedia('(max-width: 767px)').matches) {
// Mobile configuration
return {
- left: 'title',
- right: 'prev,next,dayGridMonth,timeGridDay'
+ left: 'title', right: 'prev,next,dayGridMonth,timeGridDay'
};
} else if (window.matchMedia('(max-width: 991px)').matches) {
// Tablet configuration
return {
- left: 'prev,today,next',
- center: 'title',
- right: 'dayGridMonth,timeGridWeek,timeGridDay'
+ left: 'prev,today,next', center: 'title', right: 'dayGridMonth,timeGridWeek,timeGridDay'
};
} else {
// Desktop configuration
@@ -104,57 +214,30 @@ function getHeaderToolbarConfig() {
}
function getCalendarHeight() {
- if (window.innerWidth <= MOBILE_WIDTH) return '500px';
- if (window.innerWidth <= SMALL_TABLET_WIDTH) return '600px';
- if (window.innerWidth <= TABLET_WIDTH) return '650px';
- if (window.innerWidth <= MEDIUM_WIDTH) return '767px';
+ if (window.innerWidth <= Constants.MOBILE_WIDTH_SMALL) return '400px';
+ if (window.innerWidth <= Constants.MOBILE_WIDTH) return '450px';
+ if (window.innerWidth <= Constants.SMALL_TABLET_WIDTH) return '600px';
+ if (window.innerWidth <= Constants.TABLET_WIDTH) return '650px';
+ if (window.innerWidth <= Constants.MEDIUM_WIDTH) return '767px';
return '850px';
}
-function toggleEditMode() {
- const modal = document.getElementById("eventDetailsModal");
- const inputs = modal.querySelectorAll("input");
- const servicesDropdown = document.getElementById("serviceSelect");
+function handleCalendarRightClick(event, date) {
+ const contextMenu = document.getElementById("customContextMenu");
+ contextMenu.style.top = `${event.pageY}px`;
+ contextMenu.style.left = `${event.pageX}px`;
+ contextMenu.style.display = 'block';
- // Retrieve the appointment that matches the eventIdSelected
- const appointment = appointments.find(app => Number(app.id) === Number(eventIdSelected));
- if (!appointment) {
- return;
- }
+ const newAppointmentOption = document.getElementById("newAppointmentOption");
+ newAppointmentOption.onclick = () => createNewAppointment(date);
- const endTimeLabel = modal.querySelector("label[for='endTime']"); // Assuming you have a label with `for` attribute set to `endTime`
- const endTimeInput = modal.querySelector("input[name='endTime']"); // Assuming you have an input with `name` attribute set to `endTime`
- const editButton = document.getElementById("eventEditBtn");
- const submitButton = document.getElementById("eventSubmitBtn");
- const closeButton = modal.querySelector(".btn-secondary[data-dismiss='modal']");
- const cancelButton = document.getElementById("eventCancelBtn");
-
- if (isEditing) {
- inputs.forEach(input => input.disabled = true);
- servicesDropdown.disabled = true;
- endTimeLabel.style.display = ""; // Show the end time label
- endTimeInput.style.display = ""; // Show the end time input
- editButton.style.display = "";
- closeButton.style.display = "";
- submitButton.style.display = "none";
- cancelButton.style.display = "none";
- } else {
- inputs.forEach(input => input.disabled = false);
- servicesDropdown.disabled = false;
- endTimeLabel.style.display = "none"; // Hide the end time label
- endTimeInput.style.display = "none"; // Hide the end time input
- editButton.style.display = "none";
- closeButton.style.display = "none";
- submitButton.style.display = "";
- cancelButton.style.display = "";
- }
-
- isEditing = !isEditing;
+ // Hide context menu on any click
+ document.addEventListener('click', () => contextMenu.style.display = 'none', {once: true});
}
function goToEvent() {
// Get the event URL
- const event = appointments.find(app => Number(app.id) === Number(eventIdSelected));
+ const event = appointments.find(app => Number(app.id) === Number(AppState.eventIdSelected));
if (event && event.url) {
closeModal()
window.location.href = event.url;
@@ -177,100 +260,21 @@ function closeModal() {
cancelButton.style.display = "none";
// Reset the editing flag
- isEditing = false;
+ AppState.isEditingAppointment = false;
// Close the modal
$('#eventDetailsModal').modal('hide');
}
-function checkEmail(email) {
- const emailInput = document.querySelector('input[name="clientEmail"]');
- const emailError = document.getElementById("emailError");
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
- if (!emailRegex.test(email)) {
- emailInput.style.border = "1px solid red";
- emailError.textContent = "Invalid email address.";
- emailError.style.color = "red";
- emailError.style.display = "inline"; // Make the error message visible
- return false;
- } else {
- emailInput.style.border = ""; // Reset the border to its original style
- emailError.textContent = ""; // Clear the error message
- emailError.style.display = "none"; // Hide the error message
- return true;
- }
-}
-
-async function submitChanges() {
- const modal = document.getElementById("eventDetailsModal");
- const inputs = modal.querySelectorAll("input");
- const serviceDropdown = modal.querySelector("#serviceSelect");
- const serviceId = serviceDropdown.value;
-
- const data = {};
-
- inputs.forEach(input => {
- let key = input.previousElementSibling.innerText.trim().replace(/\s+/g, '_').replace(":", "").toLowerCase();
- data[key] = input.value;
- });
-
- // Check if the email is valid
- if (!checkEmail(data["client_email"])) {
- return;
- }
-
- data["appointment_id"] = eventIdSelected; // Adding the appointment ID
- data["service_id"] = serviceId;
-
- try {
- const response = await fetch(updateApptMinInfoURL, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-Requested-With': 'XMLHttpRequest', // to ensure Django treats this as an AJAX request
- 'X-CSRFToken': getCSRFToken(),
- },
- body: JSON.stringify(data)
- });
- if (response.ok) {
- const responseData = await response.json();
-
- if (responseData.appt) {
- const index = appointments.findIndex(app => Number(app.id) === Number(eventIdSelected));
- if (index !== -1) {
- appointments[index] = responseData.appt;
- }
- }
- let eventToUpdate = calendar.getEventById(eventIdSelected);
- if (eventToUpdate) {
- eventToUpdate.setProp('title', responseData.appt.service_name);
- eventToUpdate.setStart(moment(responseData.appt.start_time).format('YYYY-MM-DDTHH:mm:ss'));
- eventToUpdate.setEnd(responseData.appt.end_time);
- eventToUpdate.setExtendedProp('client_name', responseData.appt.client_name);
- eventToUpdate.setProp('backgroundColor', responseData.appt.background_color);
- calendar.render();
- }
- calendar.refetchEvents();
- document.querySelector('input[name="startTime"]').value = responseData.appt.start_time;
- document.querySelector('input[name="endTime"]').value = responseData.appt.end_time;
-
-
- } else {
- console.error('Failed to update appointment. Server responded with:', response.status);
- // TODO: Handle error, e.g., show an error message to the user
- }
- } catch (error) {
- console.error('Failed to send data:', error);
- // TODO: Handle error, e.g., show an error message to the user
- }
+// ################################################################ //
+// Generic //
+// ################################################################ //
- closeModal();
-}
async function cancelEdit() {
// Retrieve the appointment that matches the eventIdSelected
- const appointment = appointments.find(app => Number(app.id) === Number(eventIdSelected));
+ const appointment = appointments.find(app => Number(app.id) === Number(AppState.eventIdSelected));
if (!appointment) {
return;
}
@@ -289,74 +293,54 @@ async function cancelEdit() {
endTimeInput.style.display = "";
// Re-show the event modal with the original data
- const event = {id: eventIdSelected};
- await showEventModal(event);
+ await showEventModal(appointment.id, false, false);
toggleEditMode(); // Turn off edit mode
}
-async function showEventModal(event) {
- const appointment = appointments.find(app => Number(app.id) === Number(event.id));
- if (!appointment) {
- return;
- }
-
- // Extract only the time using Moment.js
- const startTime = moment(appointment.start_time).format('HH:mm:ss');
- const endTime = moment(appointment.end_time).format('HH:mm:ss');
-
- // Fetch and populate services for dropdown
- const servicesDropdown = await populateServices(appointment.service_id);
- servicesDropdown.id = "serviceSelect";
- servicesDropdown.value = appointment.service_id; // Assuming you have a service_id in the appointment
- servicesDropdown.disabled = true; // Initially disable the dropdown
-
- // Convert the services dropdown to a string for the template
- const div = document.createElement('div');
- div.appendChild(servicesDropdown);
- const servicesDropdownString = div.innerHTML;
-
- // Set the content of the modal as input fields
- const modalBodyContent = `
-
- ${servicesDropdownString}
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
-
- document.getElementById('eventModalBody').innerHTML = modalBodyContent;
-
- // Display the modal
- $('#eventDetailsModal').modal('show');
-}
-
-function fetchServices() {
- let ajax_data_get_data = {
- 'appointmentId': eventIdSelected,
- }
- const finalUrl = `${fetchServiceListForStaffURL}?${$.param(ajax_data_get_data)}`;
- return fetch(finalUrl)
- .then(response => response.json())
+function confirmDeleteAppointment(appointmentId) {
+ const deleteURL = deleteAppointmentURLTemplate
+ const data = {appointment_id: appointmentId};
+
+ fetch(deleteURL, {
+ method: 'POST', headers: {
+ 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'X-CSRFToken': getCSRFToken(),
+ }, body: JSON.stringify(data)
+ })
+ .then(response => {
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+ return response.json();
+ })
.then(data => {
- return data.services_offered; // This should be a list of dictionaries
+ $('#eventDetailsModal').modal('hide');
+ let event = AppState.calendar.getEventById(appointmentId);
+ if (event) {
+ event.remove();
+ }
+ showErrorModal(data.message, successTxt);
+ closeConfirmModal(); // Close the confirmation modal
})
.catch(error => {
- console.error("There was an error fetching the services.", error);
+ console.error('Error:', error);
+ showErrorModal(updateApptErrorTitleTxt);
});
}
-async function populateServices(selectedServiceId) {
- const services = await fetchServices();
+function deleteAppointment() {
+ showModal(confirmDeletionTxt, confirmDeletionTxt, deleteBtnTxt, null, () => confirmDeleteAppointment(AppState.eventIdSelected));
+}
+
+function fetchServices(isEditMode = false) {
+ let url = isEditMode && AppState.eventIdSelected ? `${fetchServiceListForStaffURL}?appointmentId=${AppState.eventIdSelected}` : fetchServiceListForStaffURL;
+ return fetch(url)
+ .then(response => response.json())
+ .then(data => data.services_offered)
+ .catch(error => console.error("Error fetching services: ", error));
+}
+
+async function populateServices(selectedServiceId, isEditMode = false) {
+ const services = await fetchServices(isEditMode);
const selectElement = document.createElement('select');
services.forEach(service => {
const option = document.createElement('option');
@@ -380,26 +364,19 @@ function getCSRFToken() {
}
}
-
async function validateAndUpdateAppointmentDate(event, revertFunction) {
const updatedStartTime = event.start.toISOString();
const updatedEndTime = event.end ? event.end.toISOString() : null;
const data = {
- appointment_id: event.id,
- start_time: updatedStartTime,
- date: event.start.toISOString().split('T')[0]
+ appointment_id: event.id, start_time: updatedStartTime, date: event.start.toISOString().split('T')[0]
};
try {
const validationResponse = await fetch(validateApptDateURL, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-Requested-With': 'XMLHttpRequest',
- 'X-CSRFToken': getCSRFToken(),
- },
- body: JSON.stringify(data)
+ method: 'POST', headers: {
+ 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'X-CSRFToken': getCSRFToken(),
+ }, body: JSON.stringify(data)
});
if (validationResponse.ok) {
@@ -421,28 +398,23 @@ async function updateAppointmentDate(event, revertFunction) {
const updatedDate = event.start.toISOString().split('T')[0];
const data = {
- appointment_id: event.id,
- start_time: updatedStartTime,
- date: updatedDate,
+ appointment_id: event.id, start_time: updatedStartTime, date: updatedDate,
};
try {
const response = await fetch(updateApptDateURL, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-Requested-With': 'XMLHttpRequest',
- 'X-CSRFToken': getCSRFToken(),
- },
- body: JSON.stringify(data)
+ method: 'POST', headers: {
+ 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'X-CSRFToken': getCSRFToken(),
+ }, body: JSON.stringify(data)
});
+ const responseData = await response.json();
if (response.ok) {
- const responseData = await response.json();
- showErrorModal(responseData.message, 'Success')
- console.log('Appointment date updated:', responseData.message);
+ console.log("Updated message: " + responseData.message)
+ showErrorModal(responseData.message, successTxt)
} else {
console.error('Failed to update appointment date. Server responded with:', response.statusText);
+ showErrorModal(responseData.message, updateApptErrorTitleTxt);
revertFunction();
}
} catch (error) {
@@ -450,3 +422,295 @@ async function updateAppointmentDate(event, revertFunction) {
revertFunction();
}
}
+
+// ################################################################ //
+// Create new Appt //
+// ################################################################ //
+function createNewAppointment(dateInput) {
+ let date;
+ if (typeof dateInput === 'string' || dateInput instanceof String) {
+ date = new Date(dateInput);
+ } else {
+ date = dateInput;
+ }
+
+ const day = date.getDate().toString().padStart(2, '0');
+ const month = (date.getMonth() + 1).toString().padStart(2, '0'); // getMonth() returns 0-11
+ const year = date.getFullYear();
+ const formattedDate = `${year}-${month}-${day}`;
+ const defaultStartTime = `${formattedDate}T09:00:00`;
+
+ showCreateAppointmentModal(defaultStartTime, formattedDate).then(() => {
+ });
+}
+
+
+async function showCreateAppointmentModal(defaultStartTime, formattedDate) {
+ const servicesDropdown = await populateServices(null, false);
+ servicesDropdown.id = "serviceSelect";
+ servicesDropdown.disabled = false; // Enable dropdown
+
+ document.getElementById('eventModalBody').innerHTML = prepareCreateAppointmentModalContent(servicesDropdown, defaultStartTime, formattedDate);
+
+ adjustCreateAppointmentModalButtons();
+ AppState.isCreating = true;
+ $('#eventDetailsModal').modal('show');
+}
+
+function adjustCreateAppointmentModalButtons() {
+ document.getElementById("eventSubmitBtn").style.display = "";
+ document.getElementById("eventCancelBtn").style.display = "none";
+ document.getElementById("eventEditBtn").style.display = "none";
+ document.getElementById("eventDeleteBtn").style.display = "none";
+ document.getElementById("eventGoBtn").style.display = "none";
+}
+
+
+// ################################################################ //
+// Show Event Modal //
+// ################################################################ //
+
+// Extract Appointment Data
+async function getAppointmentData(eventId, isCreatingMode, defaultStartTime) {
+ if (eventId && !isCreatingMode) {
+ const appointment = appointments.find(app => Number(app.id) === Number(eventId));
+ if (!appointment) {
+ showErrorModal(apptNotFoundTxt, errorTxt);
+ return null;
+ }
+ return appointment;
+ }
+ return {
+ id: null,
+ service_name: '',
+ start_time: defaultStartTime,
+ end_time: '',
+ client_name: '',
+ client_email: '',
+ client_phone: '',
+ client_address: '',
+ additional_info: '',
+ want_reminder: false,
+ background_color: '',
+ timezone: '',
+ };
+}
+
+// Populate Services Dropdown
+async function getServiceDropdown(serviceId, isEditMode) {
+ const servicesDropdown = await populateServices(serviceId, !isEditMode);
+ servicesDropdown.id = "serviceSelect";
+ servicesDropdown.disabled = !isEditMode;
+ return servicesDropdown;
+}
+
+// Show Event Modal
+async function showEventModal(eventId = null, isEditMode, isCreatingMode = false, defaultStartTime = '') {
+ const appointment = await getAppointmentData(eventId, isCreatingMode, defaultStartTime);
+ if (!appointment) return;
+
+ const servicesDropdown = await getServiceDropdown(appointment.service_id, isEditMode);
+ document.getElementById('eventModalBody').innerHTML = generateModalContent(appointment, servicesDropdown, isEditMode);
+ adjustModalButtonsVisibility(isEditMode, isCreatingMode);
+ $('#eventDetailsModal').modal('show');
+}
+
+// Adjust Modal Buttons Visibility
+function adjustModalButtonsVisibility(isEditMode, isCreatingMode) {
+ const editButton = document.getElementById("eventEditBtn");
+ const submitButton = document.getElementById("eventSubmitBtn");
+ const deleteButton = document.getElementById("eventDeleteBtn");
+ const goButton = document.getElementById("eventGoBtn");
+
+ editButton.style.display = !isEditMode && !isCreatingMode ? "" : "none";
+ submitButton.style.display = isCreatingMode || isEditMode ? "" : "none";
+ deleteButton.style.display = !isEditMode && !isCreatingMode ? "" : "none";
+ goButton.style.display = isCreatingMode ? "none" : "";
+}
+
+// ################################################################ //
+// Edit Logic //
+// ################################################################ //
+
+function toggleEditMode() {
+ const modal = document.getElementById("eventDetailsModal");
+ const appointment = appointments.find(app => Number(app.id) === Number(AppState.eventIdSelected));
+ AppState.isCreating = false; // Turn off creating mode
+
+ // Proceed only if an appointment is found
+ if (appointment) {
+ AppState.isEditingAppointment = !AppState.isEditingAppointment; // Toggle the editing state
+ updateModalUIForEditMode(modal, AppState.isEditingAppointment);
+ }
+}
+
+function updateModalUIForEditMode(modal, isEditingAppointment) {
+ const inputs = modal.querySelectorAll("input");
+ const servicesDropdown = document.getElementById("serviceSelect");
+ const editButton = document.getElementById("eventEditBtn");
+ const submitButton = document.getElementById("eventSubmitBtn");
+ const closeButton = modal.querySelector(".btn-secondary[data-dismiss='modal']");
+ const cancelButton = document.getElementById("eventCancelBtn");
+ const deleteButton = document.getElementById("eventDeleteBtn");
+ const goButton = document.getElementById("eventGoBtn");
+ const endTimeLabel = modal.querySelector("label[for='endTime']");
+ const endTimeInput = modal.querySelector("input[name='endTime']");
+
+ // Toggle input and dropdown enable/disable state
+ inputs.forEach(input => input.disabled = !isEditingAppointment);
+ servicesDropdown.disabled = !isEditingAppointment;
+
+ // Toggle visibility of UI elements
+ toggleElementVisibility(editButton, !isEditingAppointment);
+ toggleElementVisibility(submitButton, isEditingAppointment);
+ toggleElementVisibility(cancelButton, isEditingAppointment);
+ toggleElementVisibility(deleteButton, !isEditingAppointment);
+ toggleElementVisibility(closeButton, !isEditingAppointment);
+ toggleElementVisibility(endTimeLabel, !isEditingAppointment); // Show end time in view mode
+ toggleElementVisibility(endTimeInput, !isEditingAppointment); // Show end time in view mode
+ toggleElementVisibility(goButton, !isEditingAppointment);
+}
+
+function toggleElementVisibility(element, isVisible) {
+ if (element) {
+ element.style.display = isVisible ? "" : "none";
+ }
+}
+
+
+// ################################################################ //
+// Submit Logic //
+// ################################################################ //
+
+async function submitChanges() {
+ const modal = document.getElementById("eventDetailsModal");
+ const formData = collectFormDataFromModal(modal);
+
+ if (!validateFormData(formData)) return;
+
+ const response = await sendAppointmentData(formData);
+ if (response.ok) {
+ const responseData = await response.json();
+ if (AppState.isCreating) {
+ addNewAppointmentToCalendar(responseData.appt[0]);
+ } else {
+ updateExistingAppointmentInCalendar(responseData.appt);
+ }
+
+ AppState.calendar.render();
+ } else {
+ const responseData = await response.json();
+ showErrorModal(responseData.message);
+ }
+ closeModal();
+
+}
+
+// Collect form data from modal
+function collectFormDataFromModal(modal) {
+ const inputs = modal.querySelectorAll("input");
+ const serviceId = modal.querySelector("#serviceSelect").value;
+ const data = {isCreating: AppState.isCreating, service_id: serviceId, appointment_id: AppState.eventIdSelected};
+
+ inputs.forEach(input => {
+ if (input.name !== "date") {
+ let key = input.previousElementSibling.innerText.trim().replace(/\s+/g, '_').replace(":", "").toLowerCase();
+ data[key] = input.value;
+ }
+ });
+
+ if (AppState.isCreating) {
+ data["date"] = modal.querySelector('input[name="date"]').value;
+ }
+
+ // Special handling for checkbox
+ const wantReminderCheckbox = modal.querySelector('input[name="want_reminder"]');
+ if (!wantReminderCheckbox.checked) {
+ data['want_reminder'] = 'false';
+ } else {
+ data['want_reminder'] = 'true';
+ }
+
+ return data;
+}
+
+// Validate form data
+function validateFormData(data) {
+ return validateEmail(data["client_email"]);
+}
+
+// Validate email
+function validateEmail(email) {
+ const emailInput = document.querySelector('input[name="clientEmail"]');
+ const emailError = document.getElementById("emailError");
+
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (!emailRegex.test(email)) {
+ emailInput.style.border = "1px solid red";
+ emailError.textContent = "Invalid email address.";
+ emailError.style.color = "red";
+ emailError.style.display = "inline";
+ return false;
+ } else {
+ emailInput.style.border = "";
+ emailError.textContent = "";
+ emailError.style.display = "none";
+ return true;
+ }
+}
+
+// Send appointment data to server
+async function sendAppointmentData(data) {
+ const headers = {
+ 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'X-CSRFToken': getCSRFToken(),
+ };
+
+ return fetch(updateApptMinInfoURL, {
+ method: 'POST', headers: headers, body: JSON.stringify(data)
+ });
+}
+
+// Handle response from server
+async function handleAppointmentResponse(response) {
+ if (!response.ok) {
+ throw new Error(response.message);
+ }
+
+ const responseData = await response.json();
+ if (AppState.isCreating) {
+ addNewAppointmentToCalendar(responseData.appt[0]);
+ } else {
+ updateExistingAppointmentInCalendar(responseData.appt);
+ }
+
+ AppState.calendar.render();
+}
+
+// Add new appointment to calendar
+function addNewAppointmentToCalendar(newAppointment) {
+ const newEvent = formatAppointmentsForCalendar([newAppointment])[0];
+ appointments.push(newAppointment);
+ AppState.calendar.addEvent(newEvent);
+}
+
+// Update existing appointment in calendar
+function updateExistingAppointmentInCalendar(appointment) {
+ let eventToUpdate = AppState.calendar.getEventById(AppState.eventIdSelected);
+ if (eventToUpdate) {
+ updateEventProperties(eventToUpdate, appointment);
+ }
+ // update appointment in appointments array
+ const index = appointments.findIndex(app => Number(app.id) === Number(AppState.eventIdSelected));
+ if (index !== -1) {
+ appointments[index] = appointment;
+ }
+}
+
+// Update event properties
+function updateEventProperties(event, appointment) {
+ event.setProp('title', appointment.service_name);
+ event.setStart(moment(appointment.start_time).format('YYYY-MM-DDTHH:mm:ss'));
+ event.setEnd(appointment.end_time);
+ event.setExtendedProp('client_name', appointment.client_name);
+ event.setProp('backgroundColor', appointment.background_color);
+}
diff --git a/appointment/static/js/appointments.js b/appointment/static/js/appointments.js
index 26a7332..145479e 100644
--- a/appointment/static/js/appointments.js
+++ b/appointment/static/js/appointments.js
@@ -225,7 +225,7 @@ function getAvailableSlots(selectedDate, staffId = null) {
// Show an error message
errorMessageContainer.append('Date is in the past.
');
if (slotContainer.find('.djangoAppt_btn-request-next-slot').length === 0) {
- slotContainer.append(``);
+ slotContainer.append(``);
}
// Disable the 'submit' button
$('.btn-submit-appointment').attr('disabled', 'disabled');
@@ -237,7 +237,7 @@ function getAvailableSlots(selectedDate, staffId = null) {
// Check if the returned message is 'No availability'
if (data.message.toLowerCase() === 'no availability') {
if (slotContainer.find('.djangoAppt_btn-request-next-slot').length === 0) {
- slotContainer.append(``);
+ slotContainer.append(``);
}
} else {
$('.djangoAppt_btn-request-next-slot').remove();
diff --git a/appointment/static/js/modal/confirm_modal.js b/appointment/static/js/modal/confirm_modal.js
deleted file mode 100644
index c178161..0000000
--- a/appointment/static/js/modal/confirm_modal.js
+++ /dev/null
@@ -1,15 +0,0 @@
-function showModal(title, body, actionText, actionUrl) {
- // Set the content of the modal
- document.getElementById('modalLabel').innerText = title;
- document.getElementById('modalBody').innerText = body;
- const actionBtn = document.getElementById('modalActionBtn');
- actionBtn.innerText = actionText;
- actionBtn.setAttribute('href', actionUrl);
-
- // Display the modal
- $('#confirmModal').modal('show');
-}
-
-function closeModal() {
- $('#confirmModal').modal('hide');
-}
\ No newline at end of file
diff --git a/appointment/static/js/modal/error_modal.js b/appointment/static/js/modal/error_modal.js
index faea1b5..a64887f 100644
--- a/appointment/static/js/modal/error_modal.js
+++ b/appointment/static/js/modal/error_modal.js
@@ -1,6 +1,6 @@
let errorModalInstance = null;
-function showErrorModal(message, title = 'Error') {
+function showErrorModal(message, title = errorTxt) {
// Insert the error message into the modal
document.getElementById('errorModalLabel').textContent = title;
document.getElementById('errorModalMessage').textContent = message;
diff --git a/appointment/static/js/modal/show_modal.js b/appointment/static/js/modal/show_modal.js
new file mode 100644
index 0000000..7dac8b7
--- /dev/null
+++ b/appointment/static/js/modal/show_modal.js
@@ -0,0 +1,25 @@
+function showModal(title, body, actionText, actionUrl, actionCallback) {
+ // Set the content of the modal
+ document.getElementById('modalLabel').innerText = title;
+ document.getElementById('modalBody').innerText = body;
+ const actionBtn = document.getElementById('modalActionBtn');
+ actionBtn.innerText = actionText;
+
+ // Determine the type of action: callback function or URL
+ if (actionCallback) {
+ actionBtn.onclick = () => {
+ actionCallback();
+ closeModal(); // Close the modal after action
+ };
+ } else if (actionUrl) {
+ actionBtn.href = actionUrl;
+ }
+
+ // Display the modal
+ $('#confirmModal').modal('show');
+}
+
+
+function closeConfirmModal() {
+ $('#confirmModal').modal('hide');
+}
\ No newline at end of file
diff --git a/appointment/templates/administration/display_appointment.html b/appointment/templates/administration/display_appointment.html
index efd8b1c..3e3c10c 100644
--- a/appointment/templates/administration/display_appointment.html
+++ b/appointment/templates/administration/display_appointment.html
@@ -48,7 +48,7 @@ {{ page_title }}
- {% trans 'Want reminder' %}: {{ appointment.want_reminder }}
+ {% trans 'Wants reminder' %}: {{ appointment.want_reminder }}
@@ -56,7 +56,7 @@
{{ page_title }}
- {% trans 'Additional client info' %}: {{ appointment.additional_info }}
+ {% trans 'Additional Information' %}: {{ appointment.additional_info }}
diff --git a/appointment/templates/administration/service_list.html b/appointment/templates/administration/service_list.html
index 20fe2c5..49c6d25 100644
--- a/appointment/templates/administration/service_list.html
+++ b/appointment/templates/administration/service_list.html
@@ -43,7 +43,7 @@
@@ -51,7 +51,7 @@
- {% empty %}
+ {% empty %}
{% trans 'No service found' %}. |
diff --git a/appointment/templates/administration/staff_index.html b/appointment/templates/administration/staff_index.html
index 0bc27aa..d382f6b 100644
--- a/appointment/templates/administration/staff_index.html
+++ b/appointment/templates/administration/staff_index.html
@@ -5,7 +5,8 @@
{% endblock %}
{% block customCSS %}
-
+
+
{% endblock %}
{% block title %}
@@ -140,6 +265,7 @@
{% include 'modal/event_details_modal.html' %}
@@ -155,6 +281,13 @@
+
+
+ {% include 'modal/confirm_modal.html' %}
{% endblock %}
@@ -169,6 +302,7 @@
const locale = "{{ locale }}";
const availableSlotsAjaxURL = "{% url 'appointment:available_slots_ajax' %}";
const requestNextAvailableSlotURLTemplate = "{% url 'appointment:request_next_available_slot' service_id=0 %}";
+ const deleteAppointmentURLTemplate = "{% url 'appointment:delete_appointment_ajax' %}";
const getNonWorkingDaysURL = "{% url 'appointment:get_non_working_days_ajax' %}";
const serviceId = "{{ service.id }}";
const serviceDuration = parseInt("{{ service.duration.total_seconds }}") / 60;
@@ -178,7 +312,78 @@
const updateApptDateURL = "{% url 'appointment:update_appt_date_time' %}";
const validateApptDateURL = "{% url 'appointment:validate_appointment_date' %}";
+
+
+
+
+
{% endblock %}
diff --git a/appointment/templates/administration/user_profile.html b/appointment/templates/administration/user_profile.html
index 22cd141..5885a11 100644
--- a/appointment/templates/administration/user_profile.html
+++ b/appointment/templates/administration/user_profile.html
@@ -122,13 +122,13 @@ {% trans 'Days Off' %}
{% translate "Are you sure you want to delete this working hours?" as d_modal_message %}
{% if superuser %}
{% else %}
@@ -188,13 +188,13 @@ {% trans 'Working Hours' %}
{% translate "Are you sure you want to delete this working hours?" as w_modal_message %}
{% if superuser %}
{% else %}
@@ -263,7 +263,7 @@ {% trans 'Service Offered' %}
-
+
{% endblock %}
diff --git a/appointment/templates/appointment/appointments.html b/appointment/templates/appointment/appointments.html
index 5970d6d..c99236a 100644
--- a/appointment/templates/appointment/appointments.html
+++ b/appointment/templates/appointment/appointments.html
@@ -2,6 +2,7 @@
{% load i18n %}
{% load static %}
{% block customCSS %}
+
{% endblock %}
{% block title %}
@@ -54,7 +55,8 @@ {{ service.name }}