This commit is contained in:
Marwan Alwali 2025-01-27 19:24:42 +03:00
parent 679c5ee2ca
commit 1ed04ec706
48 changed files with 4649 additions and 127 deletions

View File

@ -2,17 +2,16 @@ from django import forms
from django.utils.translation import get_language
class AddClassMixin:
"""
Mixin for adding classes to form fields and wrapping them in a div with class 'form-floating'.
Mixin for adding classes to a model.
"""
def add_class_to_fields(self):
"""
Adds the class to the fields of the form and wraps them in a div with class 'form-floating'.
Adds the class to the fields of the model.
:return: class names form-control or form-select
"""
for field_name, field in self.fields.items():
# Add classes to the field
if isinstance(field.widget, forms.Select):
existing_classes = field.widget.attrs.get('class', '')
field.widget.attrs['class'] = f"{existing_classes} form-select form-select-sm".strip()
@ -20,23 +19,6 @@ class AddClassMixin:
existing_classes = field.widget.attrs.get('class', '')
field.widget.attrs['class'] = f"{existing_classes} form-control form-control-sm".strip()
# Wrap the field in a div with class 'form-floating'
field.widget.attrs['wrapper_class'] = 'form-floating'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.add_class_to_fields()
def __getitem__(self, name):
"""
Overrides the __getitem__ method to wrap the field in a div with class 'form-floating'.
"""
field = super().__getitem__(name)
wrapper_class = field.field.widget.attrs.pop('wrapper_class', None)
if wrapper_class:
field = forms.utils.safety.mark_safe(f'<div class="{wrapper_class}">{field}</div>')
return field
class LocalizedNameMixin:
"""

View File

@ -689,7 +689,8 @@ def create_item_model(sender, instance, created, **kwargs):
item_type=ItemModel.ITEM_TYPE_MATERIAL,
uom_model=uom,
coa_model=coa,
additional_info={}
# additional_info={}
)
product = entity.get_items_all().filter(name=instance.vin).first()

View File

@ -482,3 +482,5 @@ def to_dict(obj):
else:
obj_dict[key] = str(value)
return obj_dict

View File

@ -1,10 +1,10 @@
aiohappyeyeballs==2.4.4
aiohttp==3.11.11
aiohttp-retry==2.8.3
aiohttp-retry==2.9.1
aiosignal==1.3.2
alabaster==1.0.0
albucore==0.0.23
albumentations==2.0.0
albumentations==2.0.1
annotated-types==0.7.0
anyio==4.8.0
arabic-reshaper==3.0.0
@ -33,7 +33,7 @@ ctranslate2==4.5.0
cycler==0.12.1
Cython==3.0.11
decorator==5.1.1
desert==2020.11.18
desert==2022.9.22
dill==0.3.9
distro==1.9.0
dj-rest-auth==7.0.1
@ -53,13 +53,17 @@ django-filter==24.3
django-formtools==2.5.1
django-ledger==0.7.3
django-money==3.5.3
django-next-url-mixin==0.4.0
django-nine==0.2.7
django-nonefield==0.4
django-ordered-model==3.7.4
django-phonenumber-field==8.0.0
django-picklefield==3.2
django-plans==1.2.0
django-prometheus==2.3.1
django-q2==1.7.6
django-sekizai==4.1.0
django-sequences==3.0
django-silk==5.3.2
django-sms==0.7.0
django-sslserver==0.22
@ -72,12 +76,13 @@ djangorestframework_simplejwt==5.4.0
djangoviz==0.1.1
docutils==0.21.2
easy-thumbnails==2.10
emoji==2.14.1
et_xmlfile==2.0.0
Faker==33.3.1
filelock==3.16.1
Faker==35.0.0
filelock==3.17.0
fire==0.7.0
Flask==3.1.0
fonttools==4.55.3
fonttools==4.55.6
frozenlist==1.5.0
fsspec==2024.12.0
gprof2dot==2024.6.6
@ -85,11 +90,11 @@ graphqlclient==0.2.4
greenlet==3.1.1
h11==0.14.0
h2==4.1.0
hpack==4.0.0
hpack==4.1.0
hstspreload==2025.1.1
httpcore==1.0.7
httpx==0.28.1
hyperframe==6.0.1
hyperframe==6.1.0
idna==3.10
imageio==2.37.0
imagesize==1.4.1
@ -110,7 +115,7 @@ lxml==5.3.0
Markdown==3.7
markdown-it-py==3.0.0
MarkupSafe==3.0.2
marshmallow==3.25.1
marshmallow==3.26.0
matplotlib==3.10.0
mccabe==0.7.0
mdurl==0.1.2
@ -121,10 +126,10 @@ mypy-extensions==1.0.0
networkx==3.4.2
newrelic==10.4.0
nltk==3.9.1
libquadmath==2.2.2
numpy==2.2.2
oauthlib==3.2.2
ofxtools==0.9.5
openai==1.59.8
openai==1.60.0
opencv-contrib-python==4.11.0.86
opencv-python==4.11.0.86
opencv-python-headless==4.11.0.86
@ -135,8 +140,8 @@ packaging==24.2
pandas==2.2.3
pango==0.0.1
pdfkit==1.0.0
phonenumbers==8.13.53
pillow==11.1.0
phonenumbers==8.13.42
pillow==10.4.0
platformdirs==4.3.6
prometheus_client==0.21.1
propcache==0.2.1
@ -164,7 +169,7 @@ pyobjc-framework-Cocoa==11.0
pyobjc-framework-Quartz==11.0
pyparsing==3.2.1
pyperclip==1.9.0
pyphen==0.17.0
pyphen==0.17.2
pypng==0.20220715.0
PyRect==0.2.0
PyScreeze==1.0.1
@ -174,6 +179,7 @@ python-bidi==0.6.3
python-dateutil==2.9.0.post0
python-docx==1.1.2
python-openid==2.2.5
python-stdnum==1.20
python3-saml==1.16.0
pytweening==1.2.0
pytz==2024.2
@ -194,11 +200,12 @@ rich==13.9.4
rubicon-objc==0.5.0
sacremoses==0.1.1
scikit-image==0.25.0
libomp runtime library==1.6.1
libquadmath==1.15.1
selenium==4.27.1
scikit-learn==1.6.1
scipy==1.15.1
selenium==4.28.1
sentencepiece==0.2.0
shapely==2.0.6
simsimd==6.2.1
six==1.17.0
sniffio==1.3.1
snowballstemmer==2.2.0
@ -207,8 +214,11 @@ soupsieve==2.6
SQLAlchemy==2.0.37
sqlparse==0.5.3
stanza==1.10.1
stringzilla==3.11.3
suds==1.2.0
swapper==1.3.0
sympy==1.13.1
tablib==3.7.0
tablib==3.8.0
termcolor==2.5.0
threadpoolctl==3.5.0
tifffile==2025.1.10
@ -223,7 +233,7 @@ trio-websocket==0.11.1
twilio==9.4.3
typing-inspect==0.9.0
typing_extensions==4.12.2
tzdata==2024.2
tzdata==2025.1
Unidecode==1.3.8
upgrade-requirements==1.7.0
urllib3==2.3.0

452
static/css/appt-common.css Normal file
View File

@ -0,0 +1,452 @@
.djangoAppt_main-container {
max-width: 1200px;
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;
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 */
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,870 @@
// Constants
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, isUserStaffAdmin: true,
};
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
setUserStaffAdminFlag().then(() => {
setTimeout(initializeCalendar, 50);
});
});
const AppStateProxy = new Proxy(AppState, {
set(target, property, value) {
console.log(`Setting ${property} to ${value}`)
// Check if the property being changed is 'isCreating'
if (value === true) {
attachEventListeners(); // Attach event listeners if isCreating becomes true
// (didn't check if property is isCreating, since AppStateProxy is only set with it)
}
target[property] = value; // Set the property value
return true; // Indicate successful setting
}
});
function attachEventListeners() {
// Checks if the DOM is already loaded
if (document.readyState === "complete" || document.readyState === "interactive") {
// DOM is already ready, attach event listeners directly
attachEventListenersToDropdown();
} else {
// If the DOM is not yet ready, then wait for the DOMContentLoaded event
document.addEventListener('DOMContentLoaded', function () {
attachEventListenersToDropdown();
});
}
}
function attachEventListenersToDropdown() {
const staffDropdown = document.getElementById('staffSelect');
if (staffDropdown && !staffDropdown.getAttribute('listener-attached')) {
staffDropdown.setAttribute('listener-attached', 'true');
staffDropdown.addEventListener('change', async function () {
const selectedStaffId = this.value;
const servicesDropdown = document.getElementById('serviceSelect');
const services = await fetchServicesForStaffMember(selectedStaffId);
updateServicesDropdown(servicesDropdown, services);
});
}
}
function initializeCalendar() {
const formattedAppointments = formatAppointmentsForCalendar(appointments);
const calendarEl = document.getElementById('calendar');
AppState.calendar = new FullCalendar.Calendar(calendarEl, getCalendarConfig(formattedAppointments));
AppState.calendar.setOption('locale', locale);
AppState.calendar.render();
}
function formatAppointmentsForCalendar(appointments) {
return appointments.map(appointment => ({
id: appointment.id,
title: appointment.service_name,
start: appointment.start_time,
end: appointment.end_time,
client_name: appointment.client_name,
backgroundColor: appointment.background_color,
}));
}
function updateCalendarConfig() {
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) {
return {
initialView: 'dayGridMonth',
headerToolbar: getHeaderToolbarConfig(),
navLinks: true,
editable: true,
dayMaxEvents: true,
height: getCalendarHeight(),
aspectRatio: 1.0,
themeSystem: 'bootstrap5',
nowIndicator: true,
bootstrapFontAwesome: {
close: 'fa-times',
prev: 'fa-chevron-left',
next: 'fa-chevron-right',
prevYear: 'fa-angle-double-left',
nextYear: 'fa-angle-double-right'
},
defaultView: mobileCheck() ? "basicDay" : "dayGridMonth",
selectable: true,
events: events,
eventDisplay: getEventDisplayedStyle(),
timeZone: timezone,
eventClick: async function (info) {
AppState.eventIdSelected = info.event.id;
await showEventModal(info.event.id, false, false);
},
dateClick: function (info) {
// 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) {
},
dayCellClassNames: function (info) {
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
if (AppState.isUserStaffAdmin) {
dayCell.el.addEventListener('contextmenu', function (event) {
event.preventDefault();
handleCalendarRightClick(event, dayCell.date);
});
}
}
},
};
}
function displayEventList(events, date) {
let eventListHtml = '<h4 style="font-size: 14px; font-weight: bold;">' + eventsOnTxt + ' ' + moment(date).format('MMMM Do, YYYY') + '</h4>';
eventListHtml += '<hr>';
events.forEach(function (event) {
eventListHtml += `<div class="event-list-item-appt" data-event-id="${event.id}">${event.service_name}</div>`;
eventListHtml += `<div><i class="fa fa-clock-o" aria-hidden="true"></i> ${moment(event.start_time).format('h:mm a')} - ${moment(event.end_time).format('h:mm a')}</div>`;
eventListHtml += '<hr>';
});
const date_obj = new Date(date.toISOString())
if (events.length === 0) {
eventListHtml += `<div class="djangoAppt_no-events">` + noEventTxt + `</div>`;
}
eventListHtml += `<button class="btn btn-primary djangoAppt_btn-new-event" onclick="createNewAppointment('${date_obj}')">` + newEventTxt + `</button></div>`;
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');
AppState.eventIdSelected = eventId;
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'
};
} else if (window.matchMedia('(max-width: 991px)').matches) {
// Tablet configuration
return {
left: 'prev,today,next', center: 'title', right: 'dayGridMonth,timeGridWeek,timeGridDay'
};
} else {
// Desktop configuration
return {
left: 'prevYear,prev,today,next,nextYear',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek',
prevYear: 'prevYear',
nextYear: 'nextYear'
};
}
}
function getCalendarHeight() {
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 setUserStaffAdminFlag() {
return fetch(isUserStaffAdminURL)
.then(response => response.json())
.then(data => {
if (data.is_staff_admin) {
AppState.isUserStaffAdmin = true;
} else {
console.error(data.message || "Error fetching user details.");
AppState.isUserStaffAdmin = false;
}
})
.catch(error => {
console.error("Error checking user's staff admin status: ", error);
AppState.isUserStaffAdmin = false;
});
}
function handleCalendarRightClick(event, date) {
if (!AppState.isUserStaffAdmin) {
showErrorModal(notStaffMemberTxt)
return;
}
const contextMenu = document.getElementById("customContextMenu");
contextMenu.style.top = `${event.pageY}px`;
contextMenu.style.left = `${event.pageX}px`;
contextMenu.style.display = 'block';
const newAppointmentOption = document.getElementById("newAppointmentOption");
newAppointmentOption.onclick = () => createNewAppointment(date);
// 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(AppState.eventIdSelected));
if (event && event.url) {
closeModal()
window.location.href = event.url;
} else {
console.error("Event not found or doesn't have a URL.");
}
}
function closeModal() {
const modal = document.getElementById("eventDetailsModal");
const editButton = document.getElementById("eventEditBtn");
const submitButton = document.getElementById("eventSubmitBtn");
const closeButton = modal.querySelector(".btn-secondary[data-dismiss='modal']");
const cancelButton = document.getElementById("eventCancelBtn");
// Reset the modal buttons to their default state
editButton.style.display = "";
closeButton.style.display = "";
submitButton.style.display = "none";
cancelButton.style.display = "none";
// Reset the editing flag
AppStateProxy.isEditingAppointment = false;
// Close the modal
$('#eventDetailsModal').modal('hide');
}
// ################################################################ //
// Generic //
// ################################################################ //
async function cancelEdit() {
// Retrieve the appointment that matches the eventIdSelected
const appointment = appointments.find(app => Number(app.id) === Number(AppState.eventIdSelected));
if (!appointment) {
return;
}
// Extract only the time using Moment.js
const endTime = moment(appointment.end_time).format('HH:mm:ss');
// Find the modal, end time label, and end time input
const modal = document.getElementById("eventDetailsModal");
const endTimeLabel = modal.querySelector("label[for='endTime']");
const endTimeInput = modal.querySelector("input[name='endTime']");
// Set and display the end time label and input
endTimeInput.value = endTime;
endTimeLabel.style.display = "";
endTimeInput.style.display = "";
// Re-show the event modal with the original data
await showEventModal(appointment.id, false, false);
toggleEditMode(); // Turn off edit mode
}
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': getCookie('csrftoken'),
}, body: JSON.stringify(data)
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
$('#eventDetailsModal').modal('hide');
let event = AppState.calendar.getEventById(appointmentId);
if (event) {
event.remove();
}
showErrorModal(data.message, successTxt);
closeConfirmModal(); // Close the confirmation modal
// Remove the deleted appointment from the global appointments array
appointments = appointments.filter(appointment => appointment.id !== appointmentId);
// Refresh the event list for the current date
const currentDate = AppState.calendar.getDate();
const dateEvents = appointments
.filter(event => moment(currentDate).isSame(event.start_time, 'day'))
.sort((a, b) => new Date(a.start_time) - new Date(b.start_time));
displayEventList(dateEvents, currentDate);
})
.catch(error => {
console.error('Error:', error);
showErrorModal(updateApptErrorTitleTxt);
});
}
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));
}
function fetchStaffMembers(isEditMode = false) {
let url = isEditMode && AppState.eventIdSelected ? `${fetchStaffListURL}?appointmentId=${AppState.eventIdSelected}` : fetchStaffListURL;
return fetch(url)
.then(response => response.json())
.then(data => data.staff_members)
.catch(error => console.error("Error fetching staff members: ", error));
}
async function populateServices(selectedServiceId, isEditMode = false) {
const services = await fetchServices(isEditMode);
if (!services) {
showErrorModal(noServiceOfferedTxt)
}
const selectElement = document.createElement('select');
services.forEach(service => {
const option = document.createElement('option');
option.value = service.id; // Accessing the id
option.textContent = service.name; // Accessing the name
if (service.id === selectedServiceId) {
option.defaultSelected = true;
}
selectElement.appendChild(option);
});
return selectElement;
}
async function populateStaffMembers(selectedStaffId, isEditMode = false) {
const staffMembers = await fetchStaffMembers(isEditMode);
if (!staffMembers) {
showErrorModal(noStaffMemberTxt)
}
const selectElement = document.createElement('select');
staffMembers.forEach(staff => {
const option = document.createElement('option');
option.value = staff.id; // Accessing the id
option.textContent = staff.name; // Accessing the name
if (staff.id === selectedStaffId) {
option.defaultSelected = true;
}
selectElement.appendChild(option);
});
return selectElement;
}
// Function to fetch services for a specific staff member
async function fetchServicesForStaffMember(staffId) {
const url = `${fetchServiceListForStaffURL}?staff_member=${staffId}`;
try {
const response = await fetch(url);
if (!response.ok) throw new Error('Network response was not ok');
const data = await response.json();
return data.services_offered || [];
} catch (error) {
console.error("Error fetching services: ", error);
return []; // Return an empty array in case of error
}
}
// Function to update services dropdown options
function updateServicesDropdown(dropdown, services) {
// Clear existing options
dropdown.innerHTML = '';
// Populate with new options
services.forEach(service => {
const option = new Option(service.name, service.id); // Assuming service object has id and name properties
dropdown.add(option);
});
}
/*
function getCSRFToken() {
const metaTag = document.querySelector('meta[name="csrf-token"]');
if (metaTag) {
return metaTag.getAttribute('content');
} else {
console.error("CSRF token meta tag not found!");
return null;
}
}
*/
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]
};
try {
const validationResponse = await fetch(validateApptDateURL, {
method: 'POST', headers: {
'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'X-CSRFToken': getCookie('csrftoken'),
}, body: JSON.stringify(data)
});
if (validationResponse.ok) {
await updateAppointmentDate(event, revertFunction);
} else {
const responseData = await validationResponse.json();
showErrorModal(responseData.message);
revertFunction();
}
} catch (error) {
console.error('Failed to validate data:', error);
revertFunction();
}
}
async function updateAppointmentDate(event, revertFunction) {
const updatedStartTime = event.start.toISOString().split('T')[1];
const updatedDate = event.start.toISOString().split('T')[0];
const data = {
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': getCookie('csrftoken'),
}, body: JSON.stringify(data)
});
const responseData = await response.json();
if (response.ok) {
showErrorModal(responseData.message, successTxt)
} else {
console.error('Failed to update appointment date. Server responded with:', response.statusText);
showErrorModal(responseData.message, updateApptErrorTitleTxt);
revertFunction();
}
} catch (error) {
console.error('Failed to send data:', error);
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);
let staffDropdown = null;
if (isUserSuperUser) {
staffDropdown = await populateStaffMembers(null, false);
staffDropdown.id = "staffSelect";
staffDropdown.disabled = false; // Enable dropdown
attachEventListenersToDropdown(); // Attach event listener
}
servicesDropdown.id = "serviceSelect";
servicesDropdown.disabled = false; // Enable dropdown
document.getElementById('eventModalBody').innerHTML = prepareCreateAppointmentModalContent(servicesDropdown, staffDropdown, defaultStartTime, formattedDate);
adjustCreateAppointmentModalButtons();
AppStateProxy.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;
}
// Populate Staff Dropdown
async function getStaffDropdown(staffId, isEditMode) {
const staffDropdown = await populateStaffMembers(staffId, !isEditMode);
staffDropdown.id = "staffSelect";
staffDropdown.disabled = !isEditMode;
return staffDropdown;
}
// 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);
let staffDropdown = null;
if (isUserSuperUser) {
staffDropdown = await getStaffDropdown(appointment.staff_id, isEditMode);
attachEventListenersToDropdown(); // Attach event listener
}
document.getElementById('eventModalBody').innerHTML = generateModalContent(appointment, servicesDropdown, isEditMode, staffDropdown);
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));
AppStateProxy.isCreating = false; // Turn off creating mode
// Proceed only if an appointment is found
if (appointment) {
AppStateProxy.isEditingAppointment = !AppState.isEditingAppointment; // Toggle the editing state
updateModalUIForEditMode(modal, AppState.isEditingAppointment);
} else {
console.error("Appointment not found!");
}
}
function updateModalUIForEditMode(modal, isEditingAppointment) {
const inputs = modal.querySelectorAll("input");
const staffDropdown = document.getElementById("staffSelect");
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);
staffDropdown.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;
let staffId = null;
if (isUserSuperUser) {
// If the user is a superuser, get the staff ID from the dropdown
const staffDropdown = modal.querySelector("#staffSelect");
if (staffDropdown) {
staffId = staffDropdown.value;
}
}
const data = {
isCreating: AppState.isCreating,
service_id: serviceId,
appointment_id: AppState.eventIdSelected
};
if (staffId) {
data.staff_member = staffId;
}
inputs.forEach(input => {
if (input.name !== "date") {
let key = input.name.replace(/([A-Z])/g, '_$1').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(emailInput.value)) {
emailInput.style.border = "1px solid red";
emailError.textContent = "Invalid email address, yeah.";
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': getCookie('csrftoken'),
};
return fetch(updateApptMinInfoURL, {
method: 'POST', headers: headers, body: JSON.stringify(data)
});
}
// 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);
}

370
static/js/appointments.js Normal file
View File

@ -0,0 +1,370 @@
const calendarEl = document.getElementById('calendar');
let nextAvailableDateSelector = $('.djangoAppt_next-available-date')
const body = $('body');
let nonWorkingDays = [];
let selectedDate = rescheduledDate || null;
let staffId = $('#staff_id').val() || null;
let previouslySelectedCell = null;
let isRequestInProgress = false;
const calendar = new FullCalendar.Calendar(calendarEl, {
initialView: 'dayGridMonth',
initialDate: selectedDate,
timeZone: timezone,
headerToolbar: {
left: 'title',
right: 'prev,today,next',
},
height: '400px',
themeSystem: 'bootstrap',
nowIndicator: true,
bootstrapFontAwesome: {
close: 'fa-times',
prev: 'fa-chevron-left',
next: 'fa-chevron-right',
prevYear: 'fa-angle-double-left',
nextYear: 'fa-angle-double-right'
},
selectable: true,
dateClick: function (info) {
const day = info.date.getDay(); // Get the day of the week (0 for Sunday, 6 for Saturday)
if (nonWorkingDays.includes(day)) {
return;
}
// If there's a previously selected cell, remove the class
if (previouslySelectedCell) {
previouslySelectedCell.classList.remove('selected-cell');
}
// Add the class to the currently clicked cell
info.dayEl.classList.add('selected-cell');
// Store the currently clicked cell
previouslySelectedCell = info.dayEl;
selectedDate = info.dateStr;
getAvailableSlots(info.dateStr, staffId);
},
datesSet: function (info) {
highlightSelectedDate();
},
selectAllow: function (info) {
const day = info.start.getDay(); // Get the day of the week (0 for Sunday, 6 for Saturday)
if (nonWorkingDays.includes(day)) {
return false; // Disallow selection for non-working days
}
return (info.start >= getDateWithoutTime(new Date()));
},
dayCellClassNames: function (info) {
const day = info.date.getDay();
if (nonWorkingDays.includes(day)) {
return ['disabled-day'];
}
return [];
},
});
calendar.setOption('locale', locale);
$(document).ready(function () {
staffId = $('#staff_id').val() || null;
calendar.render();
const currentDate = rescheduledDate || moment.tz(timezone).format('YYYY-MM-DD');
getAvailableSlots(currentDate, staffId);
});
function highlightSelectedDate() {
setTimeout(function () {
const dateCell = document.querySelector(`.fc-daygrid-day[data-date='${selectedDate}']`);
if (dateCell) {
dateCell.classList.add('selected-cell');
previouslySelectedCell = dateCell;
}
}, 10);
}
body.on('click', '.djangoAppt_btn-request-next-slot', function () {
const serviceId = $(this).data('service-id');
requestNextAvailableSlot(serviceId);
})
body.on('click', '.btn-submit-appointment', function () {
const selectedSlot = $('.djangoAppt_appointment-slot.selected').text();
const selectedDate = $('.djangoAppt_date_chosen').text();
if (!selectedSlot || !selectedDate) {
alert(selectDateAndTimeAlertTxt);
return;
}
if (selectedSlot && selectedDate) {
const startTime = convertTo24Hour(selectedSlot);
const APPOINTMENT_BASE_TEMPLATE = localStorage.getItem('APPOINTMENT_BASE_TEMPLATE');
// Convert the selectedDate string to a valid format
const dateParts = selectedDate.split(', ');
const monthDayYear = dateParts[1] + "," + dateParts[2];
const formattedDate = new Date(monthDayYear + " " + startTime);
const date = formattedDate.toISOString().slice(0, 10);
const endTimeDate = new Date(formattedDate.getTime() + serviceDuration * 60000);
const endTime = formatTime(endTimeDate);
const reasonForRescheduling = $('#reason_for_rescheduling').val();
const form = $('.appointment-form');
let formAction = rescheduledDate ? appointmentRescheduleURL : appointmentRequestSubmitURL;
form.attr('action', formAction);
if (!form.find('input[name="appointment_request_id"]').length) {
form.append($('<input>', {
type: 'hidden',
name: 'appointment_request_id',
value: appointmentRequestId
}));
}
form.append($('<input>', {type: 'hidden', name: 'date', value: date}));
form.append($('<input>', {type: 'hidden', name: 'start_time', value: startTime}));
form.append($('<input>', {type: 'hidden', name: 'end_time', value: endTime}));
form.append($('<input>', {type: 'hidden', name: 'service', value: serviceId}));
form.append($('<input>', {type: 'hidden', name: 'reason_for_rescheduling', value: reasonForRescheduling}));
form.submit();
} else {
const warningContainer = $('.warning-message');
if (warningContainer.find('submit-warning') === 0) {
warningContainer.append('<p class="submit-warning">' + selectTimeSlotWarningTxt + '</p>');
}
}
});
$('#staff_id').on('change', function () {
staffId = $(this).val() || null; // If staffId is an empty string, set it to null
let currentDate = null
if (selectedDate == null) {
currentDate = moment.tz(timezone).format('YYYY-MM-DD');
} else {
currentDate = selectedDate;
}
fetchNonWorkingDays(staffId, function (newNonWorkingDays) {
nonWorkingDays = newNonWorkingDays; // Update the nonWorkingDays array
calendar.render(); // Re-render the calendar to apply changes
// Fetch available slots for the current date
getAvailableSlots(currentDate, staffId);
});
});
function fetchNonWorkingDays(staffId, callback) {
if (!staffId || staffId === 'none') {
nonWorkingDays = []; // Reset nonWorkingDays
calendar.render(); // Re-render the calendar
callback([]);
return; // Exit the function early
}
let ajaxData = {
'staff_member': staffId,
};
$.ajax({
url: getNonWorkingDaysURL,
data: ajaxData,
dataType: 'json',
success: function (data) {
if (data.error) {
console.error('Error fetching non-working days:', data.message);
callback([]);
} else {
nonWorkingDays = data.non_working_days;
calendar.render();
callback(data.non_working_days);
}
}
});
}
function getDateWithoutTime(dt) {
dt.setHours(0, 0, 0, 0);
return dt;
}
function convertTo24Hour(time12h) {
const [time, modifier] = time12h.split(' ');
let [hours, minutes] = time.split(':');
if (hours === '12') {
hours = '00';
}
if (modifier.toUpperCase() === 'PM') {
hours = parseInt(hours, 10) + 12;
}
return `${hours}:${minutes}`;
}
function formatTime(date) {
const hours = date.getHours();
const minutes = date.getMinutes();
return (hours < 10 ? '0' + hours : hours) + ':' + (minutes < 10 ? '0' + minutes : minutes);
}
function getAvailableSlots(selectedDate, staffId = null) {
// Update the slot list with the available slots for the selected date
const slotList = $('#slot-list');
const slotContainer = $('.slot-container');
const errorMessageContainer = $('.error-message');
// Clear previous error messages and slots
slotList.empty();
errorMessageContainer.find('.djangoAppt_no-availability-text').remove();
// Remove the "Next available date" message
nextAvailableDateSelector = $('.djangoAppt_next-available-date'); // Update the selector
nextAvailableDateSelector.remove();
// Correctly check if staffId is 'none', null, or undefined and exit the function if true
// Check if 'staffId' is 'none', null, or undefined and display an error message
if (staffId === 'none' || staffId === null || staffId === undefined) {
console.log('No staff ID provided, displaying error message.');
const errorMessage = $('<p class="djangoAppt_no-availability-text">' + noStaffMemberSelectedTxt + '</p>');
errorMessageContainer.append(errorMessage);
// Optionally disable the "submit" button here
$('.btn-submit-appointment').attr('disabled', 'disabled');
return; // Exit the function early
}
let ajaxData = {
'selected_date': selectedDate,
'staff_member': staffId,
};
fetchNonWorkingDays(staffId, function (nonWorkingDays) {
// Check if nonWorkingDays is an array
if (Array.isArray(nonWorkingDays)) {
// Update the FullCalendar configuration
// calendar.setOption('hiddenDays', nonWorkingDays);
} else {
// Handle the case where there's an error or no data
// For now, we'll just log it, but you can handle it more gracefully if needed
console.error('Failed to get non-working days:', nonWorkingDays);
}
});
// Send an AJAX request to get the available slots for the selected date
if (isRequestInProgress) {
return; // Exit the function if a request is already in progress
}
isRequestInProgress = true;
$.ajax({
url: availableSlotsAjaxURL,
data: ajaxData,
dataType: 'json',
success: function (data) {
if (data.available_slots.length === 0) {
const selectedDateObj = moment.tz(selectedDate, timezone);
const selectedD = selectedDateObj.toDate();
const today = new Date();
today.setHours(0, 0, 0, 0);
if (selectedD < today) {
// Show an error message
errorMessageContainer.append('<p class="djangoAppt_no-availability-text">' + dateInPastErrorTxt + '</p>');
if (slotContainer.find('.djangoAppt_btn-request-next-slot').length === 0) {
slotContainer.append(`<button class="btn btn-danger djangoAppt_btn-request-next-slot" data-service-id="${serviceId}">` + requestNonAvailableSlotBtnTxt + `</button>`);
}
// Disable the 'submit' button
$('.btn-submit-appointment').attr('disabled', 'disabled');
} else {
errorMessageContainer.find('.djangoAppt_no-availability-text').remove();
if (errorMessageContainer.find('.djangoAppt_no-availability-text').length === 0) {
errorMessageContainer.append(`<p class="djangoAppt_no-availability-text">${data.message}</p>`);
}
// 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(`<button class="btn btn-danger djangoAppt_btn-request-next-slot" data-service-id="${serviceId}">` + requestNonAvailableSlotBtnTxt + `</button>`);
}
} else {
$('.djangoAppt_btn-request-next-slot').remove();
}
}
} else {
// remove the button to request for next available slot
$('.djangoAppt_no-availability-text').remove();
$('.djangoAppt_btn-request-next-slot').remove();
const uniqueSlots = [...new Set(data.available_slots)]; // remove duplicates
for (let i = 0; i < uniqueSlots.length; i++) {
slotList.append('<li class="djangoAppt_appointment-slot">' + uniqueSlots[i] + '</li>');
}
// Attach click event to the slots
$('.djangoAppt_appointment-slot').on('click', function () {
// Remove the 'selected' class from all other appointment slots
$('.djangoAppt_appointment-slot').removeClass('selected');
// Add the 'selected' class to the clicked appointment slot
$(this).addClass('selected');
// Enable the submit button
$('.btn-submit-appointment').removeAttr('disabled');
// Continue with the existing logic
const selectedSlot = $(this).text();
$('#service-datetime-chosen').text(data.date_chosen + ' ' + selectedSlot);
});
}
// Update the date chosen
$('.djangoAppt_date_chosen').text(data.date_chosen);
$('#service-datetime-chosen').text(data.date_chosen);
isRequestInProgress = false;
},
error: function() {
isRequestInProgress = false; // Ensure the flag is reset even if the request fails
}
});
}
function requestNextAvailableSlot(serviceId) {
const requestNextAvailableSlotURL = requestNextAvailableSlotURLTemplate.replace('0', serviceId);
if (staffId === null) {
return;
}
let ajaxData = {
'staff_member': staffId,
};
$.ajax({
url: requestNextAvailableSlotURL,
data: ajaxData,
dataType: 'json',
success: function (data) {
// If there's an error, just log it and return
let nextAvailableDateResponse = null;
let formattedDate = null;
if (data.error) {
nextAvailableDateResponse = data.message;
} else {
// Set the date in the calendar to the next available date
nextAvailableDateResponse = data.next_available_date;
const selectedDateObj = moment.tz(nextAvailableDateResponse, timezone);
const nextAvailableDate = selectedDateObj.toDate();
formattedDate = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(nextAvailableDate);
}
// Check if the .next-available-date element already exists
nextAvailableDateSelector = $('.djangoAppt_next-available-date'); // Update the selector
let nextAvailableDateText = null;
if (data.error) {
nextAvailableDateText = nextAvailableDateResponse;
} else {
nextAvailableDateText = `Next available date: ${formattedDate}`;
}
if (nextAvailableDateSelector.length > 0) {
// Update the content of the existing .next-available-date element
nextAvailableDateSelector.text(nextAvailableDateText);
} else {
// If the .next-available-date element doesn't exist, create and append it
const nextDateText = `<p class="djangoAppt_next-available-date">${nextAvailableDateText}</p>`;
$('.djangoAppt_btn-request-next-slot').after(nextDateText);
}
}
});
}

8
static/js/js-utils.js Normal file
View File

@ -0,0 +1,8 @@
document.addEventListener('DOMContentLoaded', function () {
const messageElements = document.querySelectorAll('.alert-dismissible');
setTimeout(function () {
messageElements.forEach(function (element) {
element.style.display = 'none';
});
}, 5000);
});

View File

@ -0,0 +1,19 @@
let errorModalInstance = null;
function showErrorModal(message, title = errorTxt) {
// Insert the error message into the modal
document.getElementById('errorModalLabel').textContent = title;
document.getElementById('errorModalMessage').textContent = message;
// Show the modal
if (!errorModalInstance) {
errorModalInstance = new bootstrap.Modal(document.getElementById('errorModal'));
}
errorModalInstance.show();
}
function closeErrorModal() {
if (errorModalInstance) {
errorModalInstance.hide();
}
}

View File

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

View File

@ -0,0 +1,98 @@
{% extends BASE_TEMPLATE %}
{% load i18n %}
{% load static %}
{% block customCSS %}
<link rel="stylesheet" type="text/css" href="{% static 'css/app_admin/display_appointment.css' %}"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA=="
crossorigin="anonymous" referrerpolicy="no-referrer"/>
{% endblock %}
{% block title %}
{{ page_title }}
{% endblock %}
{% block description %}
{{ page_description }}
{% endblock %}
{% block body %}
<section class="content content-wrapper">
<div class="appointment-display-content">
<div class="app-content">
<div class="appointment-card">
<h2>{{ page_title }}</h2>
<div class="appointment-details">
<div class="appointment-detail hover-element">
<i class="fas fa-calendar-alt"></i>
<strong>{% trans 'Date' %}:</strong> {{ appointment.get_date }}
</div>
<div class="appointment-detail hover-element">
<i class="fas fa-clock"></i>
<strong>{% trans 'Start time' %}:</strong> {{ appointment.get_start_time|time:"g:i A" }}
</div>
<div class="appointment-detail hover-element">
<i class="fas fa-clock"></i>
<strong>{% trans 'End time' %}:</strong> {{ appointment.get_end_time|time:"g:i A" }}
</div>
<div class="appointment-detail hover-element">
<i class="fas fa-hands-helping"></i>
<strong>{% trans 'Service' %}:</strong> {{ appointment.get_service_name }}
</div>
<div class="appointment-detail hover-element">
<i class="fas fa-user"></i>
<strong>{% trans 'Client' %}:</strong> {{ appointment.get_client_name }}
</div>
<div class="appointment-detail hover-element">
<i class="fas fa-envelope"></i>
<strong>{% trans 'Email' %}:</strong> {{ appointment.client.email }}
</div>
<div class="appointment-detail hover-element">
<i class="fas fa-phone"></i>
<strong>{% trans 'Phone' %}:</strong> {{ appointment.phone }}
</div>
<div class="appointment-detail hover-element">
<i class="fas fa-comment"></i>
<strong>{% trans 'Wants reminder' %}:</strong> {{ appointment.want_reminder }}
</div>
<div class="appointment-detail hover-element">
<i class="fas fa-map-marker-alt"></i>
<strong>{% trans 'Client address' %}:</strong> {{ appointment.address }}
</div>
<div class="appointment-detail hover-element">
<i class="fas fa-info-circle"></i>
<strong>{% trans 'Additional Information' %}:</strong> {{ appointment.additional_info }}
</div>
<div class="appointment-detail {% if appointment.is_paid %} is-paid-true {% else %} is-paid-false {% endif %}">
<i class="fas fa-money-bill-wave"></i>
<strong>{% trans 'Is paid' %}:</strong> {{ appointment.is_paid_text }}
</div>
<div class="appointment-detail hover-element">
<i class="fas fa-dollar-sign"></i>
<strong>{% trans 'Service price' %}:</strong> {{ appointment.get_appointment_amount_to_pay_text }}
</div>
</div>
</div>
<div class="messages" style="margin: 20px 0">
{% if messages %}
{% for message in messages %}
<div class="alert alert-dismissible {% if message.tags %}alert-{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}danger{% else %}{{ message.tags }}{% endif %}{% endif %}"
role="alert">{{ message }}</div>
{% endfor %}
{% endif %}
</div>
</div>
</div>
</section>
{% endblock %}
{% block customJS %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/moment.js"
integrity="sha512-3CuraBvy05nIgcoXjVN33mACRyI89ydVHg7y/HMN9wcTVbHeur0SeBzweSd/rxySapO7Tmfu68+JlKkLTnDFNg=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.45/moment-timezone-with-data.min.js"
integrity="sha512-t/mY3un180WRfsSkWy4Yi0tAxEDGcY2rAEx873hb5BrkvLA0QLk54+SjfYgFBBoCdJDV1H86M8uyZdJhAOHeyA=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/6.1.10/index.global.min.js"
integrity="sha512-JCQkxdym6GmQ+AFVioDUq8dWaWN6tbKRhRyHvYZPupQ6DxpXzkW106FXS1ORgo/m3gxtt5lHRMqSdm2OfPajtg=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="{% static 'js/js-utils.js' %}"></script>
{% endblock %}

View File

@ -0,0 +1,41 @@
{% extends BASE_TEMPLATE %}
{% load i18n %}
{% load static %}
{% block customCSS %}
<link rel="stylesheet" type="text/css" href="{% static 'css/verification_code.css' %}"/>
{% endblock %}
{% block title %}
{% trans 'Enter Verification Code' %}
{% endblock %}
{% block description %}
{% trans 'Enter Verification Code' %}
{% endblock %}
{% block body %}
<section class="content content-wrapper">
<div class="main-container">
<div class="body-container">
<h1>{% trans 'Enter Verification Code' %}</h1>
<p>{% trans "We've sent a verification code to your email. Please enter it below" %}:</p>
<form method="post"
action="{% url 'appointment:email_change_verification_code' %}">
{% csrf_token %}
<label>{% trans 'Code' %}:
<input type="text" name="code" placeholder="X1Y2Z3" required>
</label>
<button class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
</form>
</div>
</div>
<div class="messages" style="margin: 20px 0">
{% if messages %}
{% for message in messages %}
<div class="alert alert-dismissible {% if message.tags %}alert-{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}danger{% else %}{{ message.tags }}{% endif %}{% endif %}"
role="alert">{{ message }}</div>
{% endfor %}
{% endif %}
</div>
</section>
{% endblock %}
{% block customJS %}
<script src="{% static 'js/js-utils.js' %}"></script>
{% endblock %}

View File

@ -0,0 +1,152 @@
{% extends BASE_TEMPLATE %}
{% load i18n %}
{% load static %}
{% block customCSS %}
<link rel="stylesheet" type="text/css" href="{% static 'css/app_admin/days_off.css' %}"/>
<!-- jQuery UI CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.13.2/themes/base/jquery-ui.css"
integrity="sha512-lCk0aEL6CvAGQvaZ47hoq1v/hNsunE8wD4xmmBelkJjg51DauW6uVdaWEJlwgAE6PxcY7/SThs1T4+IMwwpN7w=="
crossorigin="anonymous" referrerpolicy="no-referrer"/>
{% endblock %}
{% block body %}
<section class="content content-wrapper">
<div class="days-off-form-wrapper">
<div class="do-form-content">
<h2>{% trans "Manage Days Off" %}</h2>
<form method="post" action="">
{% csrf_token %}
<!-- Staff Member -->
{% if error_message %}
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function () {
showErrorModal("{{ error_message }}");
});
</script>
{% endif %}
{% if days_off_form.staff_member %}
<div class="form-group">
<label for="{{ days_off_form.staff_member.id_for_label }}">{% trans 'Staff Member' %}:</label>
{{ days_off_form.staff_member }}
</div>
{% endif %}
<!-- Start Date Display (read-only) -->
<div class="form-group">
<label for="{{ day_off_form.start_date.id_for_label }}_display">{% trans 'Start date' %}:</label>
<input type="text" id="{{ day_off_form.start_date.id_for_label }}_display"
class="datepicker-display"
value="{{ day_off_form.start_date.value }}" readonly>
<!-- Actual value to be submitted -->
<input type="hidden" id="{{ day_off_form.start_date.id_for_label }}"
name="{{ day_off_form.start_date.name }}" class="datepicker-actual"
value="{{ day_off_form.start_date.value }}">
</div>
<!-- End Date Display (read-only) -->
<div class="form-group">
<label for="{{ day_off_form.end_date.id_for_label }}_display">{% trans 'End date' %}:</label>
<input type="text" id="{{ day_off_form.end_date.id_for_label }}_display"
class="datepicker-display"
value="{{ day_off_form.end_date.value }}" readonly>
<!-- Actual value to be submitted -->
<input type="hidden" id="{{ day_off_form.end_date.id_for_label }}"
name="{{ day_off_form.end_date.name }}" class="datepicker-actual"
value="{{ day_off_form.end_date.value }}">
</div>
<div class="form-group">
<label for="{{ day_off_form.description.id_for_label }}">{% trans 'Description' %}:</label>
<input type="text" id="{{ day_off_form.description.id_for_label }}"
name="{{ day_off_form.description.name }}" value="{{ day_off_form.description.value }}">
</div>
<button type="submit" class="btn btn-primary">{% trans "Submit" %}</button>
</form>
<div class="row-form-errors" style="margin: 10px 0">
{% if days_off_form.errors %}
<div class="alert alert-danger">
{{ days_off_form.errors }}
</div>
{% endif %}
</div>
<div class="messages" style="margin: 20px 0">
{% if messages %}
{% for message in messages %}
<div class="alert alert-dismissible {% if message.tags %}alert-{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}danger{% else %}{{ message.tags }}{% endif %}{% endif %}"
role="alert">{{ message }}</div>
{% endfor %}
{% endif %}
</div>
{% include 'modal/error_modal.html' %}
</div>
</div>
</section>
{% endblock %}
{% block customJS %}
<!-- JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"
integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.13.2/jquery-ui.js"
integrity="sha512-ynDTbjF5rUHsWBjz7nsljrrSWqLTPJaORzSe5aGCFxOigRZRmwM05y+kuCtxaoCSzVGB1Ky3XeRZsDhbSLdzXQ=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script type="text/javascript">
$(document).ready(function () {
$.datepicker._defaults.monthNamesShort = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec"];
function updateHiddenField(displayElement, hiddenElement) {
const displayedDate = displayElement.val();
if (displayedDate) {
const parsedDate = $.datepicker.parseDate('M. dd, yy', displayedDate);
hiddenElement.val($.datepicker.formatDate('yy-mm-dd', parsedDate));
}
}
// Initialize datepicker
$("#id_start_date_display, #id_end_date_display").datepicker({
dateFormat: 'M. dd, yy',
onSelect: function () {
const hiddenElementId = $(this).attr('id').replace('_display', '');
updateHiddenField($(this), $('#' + hiddenElementId));
}
});
// Format the hidden fields correctly on page load
updateHiddenField($("#id_start_date_display"), $("#id_start_date"));
updateHiddenField($("#id_end_date_display"), $("#id_end_date"));
});
$('form').on('submit', function (event) {
event.preventDefault();
const formData = $(this).serialize();
$.ajax({
url: $(this).attr('action'),
type: 'POST',
data: formData,
dataType: 'json',
success: function (response) {
if (response.success) {
window.location.href = response.redirect_url;
} else {
showErrorModal(error.responseJSON.message);
}
},
error: function (error) {
showErrorModal(error.responseJSON.message);
}
});
});
</script>
<script src="{% static 'js/modal/error_modal.js' %}"></script>
<script src="{% static 'js/js-utils.js' %}"></script>
{% endblock %}

View File

@ -0,0 +1,51 @@
{% extends BASE_TEMPLATE %}
{% load i18n %}
{% load static %}
{% block customCSS %}
<link rel="stylesheet" type="text/css" href="{% static 'css/app_admin/btn.css' %}"/>
<link rel="stylesheet" type="text/css" href="{% static 'css/app_admin/service.css' %}"/>
{% endblock %}
{% block body %}
<section class="content content-wrapper">
<div class="service-form-wrapper">
<div class="service-form-content">
<h2>{{ page_title }}</h2>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
{% if btn_text %}
<button type="submit" class="btn btn-primary">{{ btn_text }}</button>
{% else %}
{% if request.user.is_superuser and service.id %}
<div class="service-btn-container">
<a href="{% url 'appointment:update_service' service_id=service.id %}"
class="modify-btn button-color-blue service-btn">
<i class="fas fa-pen"></i>
</a>
<a href="javascript:void(0)"
onclick="showModal('{{ modal_title }}', '{{ d_modal_message }}', '{{ delete_btn_modal }}', '{% url 'appointment:delete_service' service_id=service.id %}', null)"
class="modify-btn button-color-red service-btn">
<i class="fas fa-trash"></i>
</a>
</div>
{% endif %}
{% endif %}
</form>
<div class="messages" style="margin: 20px 0">
{% if messages %}
{% for message in messages %}
<div class="alert alert-dismissible {% if message.tags %}alert-{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}danger{% else %}{{ message.tags }}{% endif %}{% endif %}"
role="alert">{{ message }}</div>
{% endfor %}
{% endif %}
</div>
</div>
</div>
</section>
{% endblock %}
{% block customJS %}
<script src="{% static 'js/js-utils.js' %}"></script>
{% endblock %}

View File

@ -0,0 +1,87 @@
{% extends BASE_TEMPLATE %}
{% load i18n %}
{% load static %}
{% block customCSS %}
<link rel="stylesheet" type="text/css" href="{% static 'css/app_admin/staff_member.css' %}"/>
{% endblock %}
{% block body %}
<section class="content content-wrapper">
<div class="staff-form-wrapper">
<div class="staff-form-content">
<h3>{% trans 'Staff Appointment Information' %}</h3>
<form method="post">
{% csrf_token %}
{% if form.user %}
<div class="form-group">
{{ form.user.label_tag }}
{{ form.user.errors }}
{{ form.user }}
</div>
<div class="user-not-found">
<small>
{% translate 'User not found' %} ? <a href="{% url 'appointment:add_staff_member_personal_info' %}">{% translate 'Create staff member manually' %}</a>
</small>
</div>
{% endif %}
<div class="form-group">
{{ form.services_offered.label_tag }}
{{ form.services_offered.errors }}
{{ form.services_offered }}
<br><small>{% trans 'Hold down “Control”, or “Command” on a Mac, to select more than one.' %}</small>
</div>
<div class="form-group">
{{ form.slot_duration.label_tag }}
{{ form.slot_duration }}
<small>{{ form.slot_duration.help_text }}</small>
</div>
<div class="form-group">
{{ form.lead_time.label_tag }}
{{ form.lead_time }}
<small>{{ form.lead_time.help_text }}</small>
</div>
<div class="form-group">
{{ form.finish_time.label_tag }}
{{ form.finish_time }}
<small>{{ form.finish_time.help_text }}</small>
</div>
<div class="form-group">
{{ form.appointment_buffer_time.label_tag }}
{{ form.appointment_buffer_time }}
<small>{{ form.appointment_buffer_time.help_text }}</small>
</div>
<div class="form-check">
{{ form.work_on_saturday }}
{{ form.work_on_saturday.label_tag }}
</div>
<div class="form-check">
{{ form.work_on_sunday }}
{{ form.work_on_sunday.label_tag }}
</div>
<button type="submit" class="btn btn-primary">{% trans 'Save' %}</button>
</form>
<div class="messages" style="margin: 20px 0">
{% if messages %}
{% for message in messages %}
<div class="alert alert-dismissible {% if message.tags %}alert-{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}danger{% else %}{{ message.tags }}{% endif %}{% endif %}"
role="alert">{{ message }}</div>
{% endfor %}
{% endif %}
</div>
</div>
</div>
</section>
{% endblock %}
{% block customJS %}
<script src="{% static 'js/js-utils.js' %}"></script>
{% endblock %}

View File

@ -0,0 +1,49 @@
{% extends BASE_TEMPLATE %}
{% load i18n %}
{% load static %}
{% block customCSS %}
<link rel="stylesheet" type="text/css" href="{% static 'css/app_admin/staff_member.css' %}"/>
{% endblock %}
{% block body %}
<section class="content content-wrapper">
<div class="staff-form-wrapper">
<div class="staff-form-content">
<h3>{% trans 'Staff Personal Information' %}</h3>
<form id="updatePersonalInfoForm" method="post" action="">
{% csrf_token %}
<div class="form-group">
{{ form.first_name.label_tag }}
{{ form.first_name }}
</div>
<div class="form-group">
{{ form.last_name.label_tag }}
{{ form.last_name }}
</div>
<div class="form-group">
{{ form.email.label_tag }}
{{ form.email }}
</div>
<button type="submit" class="btn btn-primary">{{ btn_text }}</button>
</form>
<div class="messages" style="margin: 20px 0">
{% if messages %}
{% for message in messages %}
<div class="alert alert-dismissible {% if message.tags %}alert-{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}danger{% else %}{{ message.tags }}{% endif %}{% endif %}"
role="alert">{{ message }}</div>
{% endfor %}
{% endif %}
</div>
</div>
</div>
</section>
{% endblock %}
{% block customJS %}
<script src="{% static 'js/js-utils.js' %}"></script>
{% endblock %}

View File

@ -0,0 +1,183 @@
{% extends BASE_TEMPLATE %}
{% load i18n %}
{% load static %}
{% block customCSS %}
<link rel="stylesheet" type="text/css" href="{% static 'css/app_admin/working_hours.css' %}"/>
<!-- additional CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA=="
crossorigin="anonymous" referrerpolicy="no-referrer"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/4.6.2/css/bootstrap.min.css"
integrity="sha512-rt/SrQ4UNIaGfDyEXZtNcyWvQeOq0QLygHluFQcSjaGB04IxWhal71tKuzP6K8eYXYB6vJV4pHkXcmFGGQ1/0w=="
crossorigin="anonymous" referrerpolicy="no-referrer"/>
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/tempusdominus-bootstrap-4/5.39.0/css/tempusdominus-bootstrap-4.min.css"
integrity="sha512-3JRrEUwaCkFUBLK1N8HehwQgu8e23jTH4np5NHOmQOobuC4ROQxFwFgBLTnhcnQRMs84muMh0PnnwXlPq5MGjg=="
crossorigin="anonymous" referrerpolicy="no-referrer"/>
{% endblock %}
{% block body %}
<section class="content content-wrapper">
<div class="working-hours-form-wrapper">
<div class="wh-form-content">
<h2>{% trans "Manage Working Hours" %}</h2>
<form method="post" action="" id="workingHoursForm"
data-action="{% if working_hours_instance %}update{% else %}create{% endif %}"
data-working-hours-id="
{% if working_hours_instance %}{{ working_hours_instance.id }}{% else %}0{% endif %}"
data-staff-user-id="{% if staff_user_id %}{{ staff_user_id }}{% else %}0{% endif %}">
{% csrf_token %}
{% if working_hours_form.staff_member %}
<div class="form-group">
<label for="{{ working_hours_form.staff_member.id_for_label }}">{% trans 'Staff Member' %}:</label>
{{ working_hours_form.staff_member }}
</div>
{% endif %}
<div class="form-group">
<label for="{{ working_hours_form.day_of_week.id_for_label }}">{% trans 'Day of Week' %}:</label>
{{ working_hours_form.day_of_week }}
</div>
<div class="form-group">
<label for="{{ working_hours_form.start_time.id_for_label }}">{% trans 'Start time' %}:</label>
<div class="input-group date" id="start-timepicker" data-target-input="nearest">
<input type="text" class="form-control datetimepicker-input" data-toggle="datetimepicker"
data-target="#start-timepicker" name="{{ working_hours_form.start_time.name }}"
value="{{ working_hours_form.start_time.value|default:'09:00 AM' }}"
id="{{ working_hours_form.start_time.id_for_label }}">
<div class="input-group-append" data-toggle="datetimepicker"
data-target="#start-timepicker">
<div class="input-group-text"><i class="far fa-clock"></i></div>
</div>
</div>
</div>
<div class="form-group">
<label for="{{ working_hours_form.end_time.id_for_label }}">{% trans 'End time' %}:</label>
<div class="input-group date" id="end-timepicker" data-target-input="nearest">
<input type="text" class="form-control datetimepicker-input" data-toggle="datetimepicker"
data-target="#end-timepicker" name="{{ working_hours_form.end_time.name }}"
value="{{ working_hours_form.end_time.value|default:'05:00 PM' }}"
id="{{ working_hours_form.end_time.id_for_label }}">
<div class="input-group-append" data-toggle="datetimepicker" data-target="#end-timepicker">
<div class="input-group-text"><i class="far fa-clock"></i></div>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">{{ button_text }}</button>
<input type="hidden" id="addWorkingHoursUrl"
value="{% url 'appointment:add_working_hours_id' staff_user_id|default:user.id %}">
<input type="hidden" id="updateWorkingHoursUrl"
value="{% url 'appointment:update_working_hours_id' working_hours_id|default:0 staff_user_id|default:user.id %}">
</form>
{% include 'modal/error_modal.html' %}
<div class="messages" style="margin: 20px 0">
{% if messages %}
{% for message in messages %}
<div class="alert alert-dismissible {% if message.tags %}alert-{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}danger{% else %}{{ message.tags }}{% endif %}{% endif %}"
role="alert">{{ message }}</div>
{% endfor %}
{% endif %}
</div>
</div>
</div>
</section>
{% endblock %}
{% block customJS %}
<!-- JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"
integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.11.8/umd/popper.min.js"
integrity="sha512-TPh2Oxlg1zp+kz3nFA0C5vVC6leG/6mm1z9+mA81MI5eaUVqasPLO8Cuk4gMF4gUfP5etR73rgU/8PNMsSesoQ=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/4.6.2/js/bootstrap.min.js"
integrity="sha512-7rusk8kGPFynZWu26OKbTeI+QPoYchtxsmPeBqkHIEXJxeun4yJ4ISYe7C6sz9wdxeE1Gk3VxsIWgCZTc+vX3g=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/moment.js"
integrity="sha512-3CuraBvy05nIgcoXjVN33mACRyI89ydVHg7y/HMN9wcTVbHeur0SeBzweSd/rxySapO7Tmfu68+JlKkLTnDFNg=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tempusdominus-bootstrap-4/5.39.0/js/tempusdominus-bootstrap-4.min.js"
integrity="sha512-k6/Bkb8Fxf/c1Tkyl39yJwcOZ1P4cRrJu77p83zJjN2Z55prbFHxPs9vN7q3l3+tSMGPDdoH51AEU8Vgo1cgAA=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script type="text/javascript">
const addWorkingHoursUrl = $('#addWorkingHoursUrl').val();
const updateWorkingHoursUrl = $('#updateWorkingHoursUrl').val();
$(function () {
const startTimeInput = $('#start-timepicker input');
const endTimeInput = $('#end-timepicker input');
const startTimePicker = $('#start-timepicker');
const endTimePicker = $('#end-timepicker');
const defaultStartTime = startTimeInput.val() ? moment(startTimeInput.val(), 'hh:mm A') : moment().hour(12).minute(0);
const defaultEndTime = endTimeInput.val() ? moment(endTimeInput.val(), 'hh:mm A') : moment().hour(13).minute(0);
startTimePicker.datetimepicker({
format: 'hh:mm A',
icons: {
time: 'far fa-clock'
},
pick12HourFormat: false,
pickSeconds: false,
defaultDate: defaultStartTime
});
endTimePicker.datetimepicker({
format: 'hh:mm A',
icons: {
time: 'far fa-clock'
},
pick12HourFormat: false,
pickSeconds: false,
defaultDate: defaultEndTime
});
startTimePicker.on('change.datetimepicker', function (e) {
startTimeInput.val(e.date.format('hh:mm A'));
});
endTimePicker.on('change.datetimepicker', function (e) {
endTimeInput.val(e.date.format('hh:mm A'));
});
});
$(document).on('submit', '#workingHoursForm', function (e) {
e.preventDefault();
const form = $(this);
const action = form.data('action');
let postUrl = '';
if (action === 'create') {
postUrl = addWorkingHoursUrl;
} else if (action === 'update') {
postUrl = updateWorkingHoursUrl;
}
$.ajax({
url: postUrl,
type: 'POST',
data: form.serialize(),
success: function (response) {
if (response.success) {
window.location.href = response.redirect_url;
} else {
showErrorModal(response.message)
}
},
error: function (error) {
showErrorModal(error.responseJSON.message)
}
});
});
</script>
<script src="{% static 'js/modal/error_modal.js' %}"></script>
<script src="{% static 'js/js-utils.js' %}"></script>
{% endblock %}

View File

@ -0,0 +1,78 @@
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
{% block title %}
{% trans 'Service List' %}
{% endblock %}
{% block description %}
{% trans 'Service List' %}.
{% endblock %}
{% block body %}
<div class="pt-5 pb-9">
<section class="profile-section">
<div class="section-header">
<h2 class="section-header-itm">{% trans 'Service List' %}</h2>
</div>
<div class="responsive-table-container">
<table>
<thead>
<tr>
<th>{% trans 'Name' %}</th>
<th>{% trans 'Duration' %}</th>
<th>{% trans 'Price' %}</th>
<th>{% trans 'Action' %}</th>
</tr>
</thead>
<tbody>
{% for service in services %}
<tr>
<td>{{ service.name }}</td>
<td>{{ service.get_duration }}</td>
<td>{{ service.get_price_text }}</td>
<td>
<div class="buttons-container">
<a href="{% url 'appointment:view_service' service_id=service.id view=1 %}"
class="modify-btn button-color-green">
<i class="fas fa-eye"></i>
</a>
{% translate "Are you sure you want to delete this service?" as d_modal_message %}
{% if request.user.is_superuser %}
<a href="{% url 'appointment:update_service' service_id=service.id %}"
class="modify-btn button-color-blue">
<i class="fas fa-pen"></i>
</a>
<a href="javascript:void(0)"
onclick="showModal('{{ modal_title }}', '{{ d_modal_message }}', '{{ delete_btn_modal }}', '{% url 'appointment:delete_service' service_id=service.id %}', null)"
class="modify-btn button-color-red">
<i class="fas fa-trash"></i>
</a>
{% endif %}
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="3">{% trans 'No service found' %}.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
<div class="messages" style="margin: 20px 0">
{% if messages %}
{% for message in messages %}
<div class="alert alert-dismissible {% if message.tags %}alert-{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}danger{% else %}{{ message.tags }}{% endif %}{% endif %}"
role="alert">{{ message }}</div>
{% endfor %}
{% endif %}
</div>
</div>
{% endblock %}
{% block customJS %}
<script src="{% static 'js/js-utils.js' %}"></script>
{% endblock %}

View File

@ -0,0 +1,409 @@
{% extends BASE_TEMPLATE %}
{% load i18n %}
{% load static %}
{% block customMetaTag %}
<meta name="csrf-token" content="{{ csrf_token }}">
{% endblock %}
{% block customCSS %}
<link rel="stylesheet" type="text/css" href="{% static 'css/appt-common.css' %}"/>
<link rel="stylesheet" type="text/css" href="{% static 'css/app_admin/admin.css' %}"/>
<style>
@media (max-width: 991px) {
body {
font-size: 15px; /* Adjust font size for tablets */
}
#calendar {
max-width: 800px; /* Adjust calendar width for tablets */
}
}
/* Mobile Styles */
@media (max-width: 767px) {
body {
margin: 20px 5px; /* Reduce margins on mobile */
font-size: 14px; /* Adjust font size for mobiles */
}
#calendar {
max-width: 100%; /* Let the calendar take the full width on mobile */
}
.fc, .fc-toolbar-title, h2 {
font-size: 15px !important; /* Adjust as needed */
margin: 0; /* Remove any default margin */
}
}
.calendar-wrapper {
display: flex;
transition: all 0.3s; /* smooth transition for resizing */
width: 100%;
flex-direction: column;
margin-top: 25px;
}
#calendar {
flex-grow: 1; /* Makes the calendar grow to occupy all available space */
transition: all 0.3s; /* smooth transition for resizing */
width: 100%;
}
#event-details {
overflow-y: auto;
width: 0; /* initially hidden */
transition: all 0.3s; /* smooth transition for resizing */
border-left: 1px solid #ccc;
flex-shrink: 0; /* Prevents the div from shrinking beyond its content's size */
}
.modal-content {
background-color: #fff;
margin: 10% auto; /* Centering the modal vertically */
width: 100%;
position: relative; /* For positioning the close button */
box-shadow: 0 0 20px rgba(0, 0, 0, 0.2); /* A little shadow for depth */
}
.modal-footer {
text-align: right;
}
.close-button {
position: absolute;
top: 10px;
right: 10px;
background: none;
border: none;
font-size: 24px;
cursor: pointer;
}
.body-blur {
filter: blur(2px); /* This will blur the whole body when modal is open */
}
#eventDetailsModal .modal-content {
background-color: #f4f4f4;
border-radius: 10px;
}
#eventDetailsModal .modal-header,
#eventDetailsModal .modal-footer {
background-color: #2c3e50;
color: #ecf0f1;
}
#eventDetailsModal .modal-title {
font-weight: bold;
}
#eventDetailsModal label {
display: block;
margin-top: 10px;
}
#eventDetailsModal input {
width: 100%;
padding: 5px;
margin: 5px 0;
border: 1px solid #ddd;
border-radius: 5px;
}
#eventDetailsModal .btn {
margin-right: 10px;
}
#serviceSelect, #staffSelect {
width: 100%;
padding: 8px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background-color: #f9f9f9;
font-size: 14px;
transition: background-color 0.3s ease;
}
#serviceSelect:disabled, #staffSelect:disabled {
background-color: #e9ecef;
}
#customContextMenu ul {
list-style: none;
margin: 0;
padding: 0;
}
#customContextMenu ul li a {
padding: 5px 10px;
display: block;
text-decoration: none;
color: black;
}
#customContextMenu ul li a:hover {
background-color: #f0f0f0;
}
#customContextMenu {
border: 1px solid #ccc;
background-color: #fff;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);
padding: 5px 0;
position: absolute;
z-index: 1000;
display: none;
}
#eventDetailsModal input[type="checkbox"] {
width: auto;
margin: 0 5px 0 0;
}
/* General styles */
.flex-container-appt {
display: flex;
align-items: center;
margin-top: 5px;
}
.flex-container-appt label {
flex: 0 0 auto; /* Do not grow, do not shrink, and base width on content */
margin-right: 5px;
white-space: nowrap; /* Prevent label from wrapping */
}
.flex-container-appt input,
.flex-container-appt select {
flex: 1 1 auto; /* Grow and shrink based on available space */
}
/* Larger Modal on Desktop */
@media (min-width: 850px) {
.modal-content {
/* centering the modal horizontally */
margin: 10% auto;
left: 0;
right: 0;
}
}
/* Stack label and input on mobile */
@media (max-width: 767px) {
.flex-container-appt {
flex-direction: column;
align-items: flex-start;
}
.flex-container-appt label {
margin-right: 0;
margin-bottom: 5px;
}
}
.highlight-weekend {
background-color: rgba(155, 155, 155, 0.03); /* Light gray background for weekends */
}
.event-dot {
height: 5px;
width: 5px;
border-radius: 50%;
display: inline-block;
margin: 2px;
}
/* Hide event list container on larger screens */
@media (min-width: 768px) {
#event-list-container {
display: none; /* Hide the event list container */
}
}
/* Show event list container on smaller screens */
@media (max-width: 767px) {
#event-list-container {
/* Styles to display and position the event list container */
display: block; /* Show the event list container */
margin-top: 20px; /* Space from the calendar */
}
.event-list-item {
/* Styles for individual event list items */
padding: 10px;
border-bottom: 1px solid #ccc;
}
}
@media (max-width: 450px) {
.fc-daygrid-event-harness {
width: 5px;
}
.fc td, .fc th {
}
}
.event-list-item-appt {
/* pointer hand cursor */
cursor: pointer;
}
</style>
{% endblock %}
{% block title %}
{{ page_title }}
{% endblock %}
{% block description %}
{{ page_description }}
{% endblock %}
{% block body %}
<section class="content content-wrapper">
<div class="container">
<div class="calendar-wrapper">
<div id="calendar" class="calendarbox"></div>
<div id="event-list-container" class="event-list-container"></div>
</div>
{% include 'modal/event_details_modal.html' %}
{% include 'modal/error_modal.html' %}
<div class="messages" style="margin: 20px 0">
{% if messages %}
{% for message in messages %}
<div class="alert alert-dismissible {% if message.tags %}alert-{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}danger{% else %}{{ message.tags }}{% endif %}{% endif %}"
role="alert">{{ message }}</div>
{% endfor %}
{% endif %}
</div>
</div>
</section>
<div id="customContextMenu" style="display: none; position: absolute; z-index: 1000;">
<ul>
<li id="newAppointmentOption"><a href="#">New Appointment</a></li>
</ul>
</div>
{% include 'modal/confirm_modal.html' %}
{% endblock %}
{% block customJS %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/moment.js"
integrity="sha512-3CuraBvy05nIgcoXjVN33mACRyI89ydVHg7y/HMN9wcTVbHeur0SeBzweSd/rxySapO7Tmfu68+JlKkLTnDFNg=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.45/moment-timezone-with-data.min.js"
integrity="sha512-t/mY3un180WRfsSkWy4Yi0tAxEDGcY2rAEx873hb5BrkvLA0QLk54+SjfYgFBBoCdJDV1H86M8uyZdJhAOHeyA=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/6.1.10/index.global.min.js"
integrity="sha512-JCQkxdym6GmQ+AFVioDUq8dWaWN6tbKRhRyHvYZPupQ6DxpXzkW106FXS1ORgo/m3gxtt5lHRMqSdm2OfPajtg=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
const timezone = "{{ timezone }}";
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;
let appointments = {{ appointments|safe }};
const fetchServiceListForStaffURL = "{% url 'appointment:fetch_service_list_for_staff' %}";
const fetchStaffListURL = "{% url 'appointment:fetch_staff_list' %}";
const updateApptMinInfoURL = "{% url 'appointment:update_appt_min_info' %}";
const updateApptDateURL = "{% url 'appointment:update_appt_date_time' %}";
const validateApptDateURL = "{% url 'appointment:validate_appointment_date' %}";
const isUserStaffAdminURL = "{% url 'appointment:is_user_staff_admin' %}";
const isUserSuperUser = "{{ is_superuser }}" === "True";
</script>
<script>
{# Text for translation #}
const confirmDeletionTitleTxt = "{% trans 'Confirm Deletion' %}";
const confirmDeletionTxt = "{% trans 'Are you sure you want to delete this appointment?' %}";
const deleteBtnTxt = "{% trans 'Delete' %}";
const eventsOnTxt = "{% trans 'Events on' %}";
const noEventTxt = "{% trans 'No events for this day.' %}";
const newEventTxt = "{% trans 'New Event' %}";
const successTxt = "{% trans 'Success' %}";
const errorTxt = "{% trans 'Error' %}";
const updateApptErrorTitleTxt = "{% trans 'Error: Unable to delete appointment.' %}";
const apptNotFoundTxt = "{% trans 'Appointment not found.' %}";
const notStaffMemberTxt = "{% trans "You're not a staff member. Can't perform this action !" %}";
const noServiceOfferedTxt = "{% trans "You don't offer any service. Add new service from your profile." %}";
const noStaffMemberTxt = "{% trans "No staff members found." %}";
</script>
<script src="{% static 'js/modal/error_modal.js' %}"></script>
<script src="{% static 'js/app_admin/staff_index.js' %}"></script>
<script src="{% static 'js/modal/show_modal.js' %}"></script>
<script src="{% static 'js/js-utils.js' %}"></script>
<script>
function createCommonInputFields(appointment, servicesDropdown, isEditMode, defaultStartTime, staffDropdown) {
const startTimeValue = isEditMode ? moment(appointment.start_time).format('HH:mm:ss') : defaultStartTime;
const disabledAttribute = isEditMode ? '' : 'disabled';
let superuserInputField = '';
if (isUserSuperUser) {
superuserInputField = `
<div class="flex-container-appt">
<label>{% trans 'Staff Member' %}:</label>
${staffDropdown.outerHTML}
</div>
`;
}
return `
${superuserInputField}
<div class="flex-container-appt">
<label>{% trans 'Service Name' %}:</label>
${servicesDropdown.outerHTML}
</div>
<label for="clientName">{% trans 'Client Name' %}:</label>
<input type="text" name="clientName" value="${appointment.client_name || ''}" ${disabledAttribute} id="clientName" placeholder="John Doe">
<label for="clientEmail">{% trans 'Client Email' %}:</label>
<input type="email" name="clientEmail" value="${appointment.client_email || ''}" ${disabledAttribute} id="clientEmail" placeholder="john.doe@example.com">
<span id="emailError" style="display:none;"></span>
<label for="clientPhone">{% trans 'Phone Number' %}:</label>
<input type="tel" name="clientPhone" value="${appointment.client_phone || ''}" ${disabledAttribute} id="clientPhone" placeholder="+12392350799">
<label for="clientAddress">{% trans 'Client address' %}:</label>
<input type="text" name="clientAddress" value="${appointment.client_address || ''}" ${disabledAttribute} id="clientAddress" placeholder="Naples, Florida">
<div class="flex-container-appt">
<label for="want_reminder">{% trans 'Wants reminder' %}:</label>
<input type="checkbox" name="want_reminder" id="want_reminder" value="true" ${appointment.want_reminder ? 'checked' : ''} ${disabledAttribute}>
</div>
<label for="additional_info">{% trans 'Additional Information' %}:</label>
<input type="text" name="additional_info" value="${appointment.additional_info || ''}" ${disabledAttribute} placeholder="{% trans 'Client wants this and that' %}" id="additional_info">
<div class="flex-container-appt">
<label for="startTime">{% trans 'Start time' %}:</label>
<input type="time" name="startTime" value="${startTimeValue}" ${disabledAttribute}>
</div>
`;
}
function generateModalContent(appointment, servicesDropdown, isEditMode, staffDropdown) {
const startTimeValue = moment(appointment.start_time).format('HH:mm:ss');
const commonInputs = createCommonInputFields(appointment, servicesDropdown, isEditMode, startTimeValue, staffDropdown);
return `
${commonInputs}
<div class="flex-container-appt">
<label for="endTime">{% trans 'End time' %}:</label>
<input type="time" name="endTime" value="${moment(appointment.end_time).format('HH:mm:ss')}" ${!isEditMode ? 'disabled' : ''}>
</div>
`;
}
function prepareCreateAppointmentModalContent(servicesDropdown, staffDropdown, defaultStartTime, formattedDate) {
const appointment = {client_name: '', client_email: '', client_phone: '', client_address: ''};
const commonInputs = createCommonInputFields(appointment, servicesDropdown, true, defaultStartTime, staffDropdown);
return `
${commonInputs}
<label for="date" style="display: none"></label>
<input type="hidden" name="date" value="${formattedDate}">
`;
}
</script>
{% endblock %}

View File

@ -0,0 +1,81 @@
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
{% block customCSS %}
<link rel="stylesheet" type="text/css" href="{% static 'css/app_admin/btn.css' %}"/>
<link rel="stylesheet" type="text/css" href="{% static 'css/app_admin/user_profile.css' %}"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA=="
crossorigin="anonymous" referrerpolicy="no-referrer"/>
{% endblock %}
{% block title %}
Staff Members List
{% endblock %}
{% block description %}
{% trans 'List of all staff members' %}.
{% endblock %}
{% block body %}
<div class="profile-container">
<section class="profile-section">
<div class="section-header">
<h2 class="section-header-itm">{% trans 'Staff Members' %}</h2>
<div class="buttons-container section-header-itm">
<a href="{{ btn_staff_me_link }}"
class="modify-btn button-color-purple">
{{ btn_staff_me }}
</a>
<a href="{% url 'appointment:add_staff_member_info' %}"
class="modify-btn button-color-green">
<i class="fas fa-add"></i>
</a>
</div>
</div>
<div class="responsive-table-container">
<table>
<thead>
<tr>
<th>{% trans 'Name' %}</th>
<th>{% trans 'Email' %}</th>
<th>{% trans 'Details' %}</th>
</tr>
</thead>
<tbody>
{% for staff_member in staff_members %}
<tr>
<td>{{ staff_member.get_staff_member_name }}</td>
<td>{{ staff_member.user.email|default:"N/A" }}</td>
<td>
<a href="{% url 'appointment:user_profile' staff_member.user.id %}"
class="modify-btn button-color-blue">{% trans 'View Profile' %}</a>
<a href="{% url 'appointment:remove_staff_member' staff_member.user.id %}"
class="modify-btn button-color-red">{% trans 'Remove' %}</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="3">{% trans 'No staff members found' %}.</td>
</tr>
{% endfor %}
<small>
{% trans "PS: Remove means, deleting the staff status of the user. The user account is still active." %}
</small>
</tbody>
</table>
</div>
</section>
<div class="messages" style="margin: 20px 0">
{% if messages %}
{% for message in messages %}
<div class="alert alert-dismissible {% if message.tags %}alert-{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}danger{% else %}{{ message.tags }}{% endif %}{% endif %}"
role="alert">{{ message }}</div>
{% endfor %}
{% endif %}
</div>
</div>
{% endblock %}
{% block customJS %}
<script src="{% static 'js/js-utils.js' %}"></script>
{% endblock %}

View File

@ -0,0 +1,281 @@
{% extends BASE_TEMPLATE %}
{% load i18n %}
{% load static %}
{% block customCSS %}
<link rel="stylesheet" type="text/css" href="{% static 'css/app_admin/btn.css' %}"/>
<link rel="stylesheet" type="text/css" href="{% static 'css/app_admin/user_profile.css' %}"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA=="
crossorigin="anonymous" referrerpolicy="no-referrer"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/css/bootstrap.min.css"
integrity="sha512-b2QcS5SsA8tZodcDtGRELiGv5SaKSk1vDHDaQRda0htPYWZ6046lr3kJ5bAAQdpV2mmA/4v0wQF9MyU6/pDIAg=="
crossorigin="anonymous" referrerpolicy="no-referrer"/>
{% endblock %}
{% block title %}
{{ page_title }}
{% endblock %}
{% block description %}
{{ page_description }}
{% endblock %}
{% block body %}
<section class="content content-wrapper">
<div class="profile-container">
<div class="messages" style="padding: 20px 0">
{% if messages %}
{% for message in messages %}
<div class="alert alert-dismissible {% if message.tags %}alert-{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}danger{% else %}{{ message.tags }}{% endif %}{% endif %}"
role="alert">{{ message }}</div>
{% endfor %}
{% endif %}
</div>
<section class="profile-section">
{% translate "Confirm Deletion" as modal_title %}
{% translate "Delete" as delete_btn_modal %}
<h2>{% trans 'Personal Information' %}</h2>
<!-- Display fields from PersonalInformationForm -->
<div class="section-content">
<p><strong>{% trans 'First name' %}:</strong> {{ user.first_name|default:user.username }}</p>
<p><strong>{% trans 'Last name' %}:</strong> {{ user.last_name|default:"N/A" }}</p>
<p><strong>{% trans 'Email' %}:</strong> {{ user.email|default:"N/A" }}</p>
</div>
<a href="{% url 'appointment:update_user_info' user.id %}"
class="section-content-button modify-btn button-color-blue">
<i class="fas fa-pen"></i>
</a>
</section>
<!-- Appointment Information Section -->
<section class="profile-section">
<h2>{% trans 'Appointment Information' %}</h2>
<small>
{{ service_msg }}
</small>
{% if staff_member %}
<div class="section-content">
<p>
<strong>{% trans 'Slot duration' %}:</strong> {{ staff_member.get_slot_duration_text }}
<i class="fas fa-info-circle" data-toggle="tooltip"
title="{{ slot_duration_help_text }}"></i>
</p>
<p><strong>{% trans 'General start time' %}:</strong> {{ staff_member.get_lead_time }}</p>
<p><strong>{% trans 'General end time' %}:</strong> {{ staff_member.get_finish_time }}</p>
<p>
<strong>{% trans 'Weekend days you work' %}:</strong> {{ staff_member.get_weekend_days_worked_text }}
</p>
<p>
<strong>{% trans 'Appointment buffer time' %}:</strong> {{ staff_member.get_appointment_buffer_time_text }}
<i class="fas fa-info-circle" data-toggle="tooltip" title="{{ buffer_time_help_text }}"></i>
</p>
</div>
<a href="{% url 'appointment:update_staff_other_info' staff_member.user.id %}"
class="section-content-button modify-btn button-color-blue">
<i class="fas fa-pen"></i>
</a>
{% else %}
<div class="section-content">
<p>{% trans 'No staff member information yet for this user' %}.</p>
</div>
<a href="{% url 'appointment:update_staff_other_info' user.id %}"
class="section-content-button modify-btn button-color-green">
<i class="fas fa-add"></i>
</a>
{% endif %}
</section>
<!-- Days Off Information Section -->
<section class="profile-section">
<h2>{% trans 'Days Off' %}</h2>
<a href="{% url 'appointment:add_day_off' staff_user_id=user.id %}"
class="section-content-button modify-btn button-color-green">
<i class="fas fa-add"></i>
</a>
<small>
{% trans "Days off are days you're not working, you need to set them for holidays as well so clients don't book you those days." %}
</small>
<div class="responsive-table-container">
<table>
<thead>
<tr>
<th>{% trans 'Start date' %}</th>
<th>{% trans 'End date' %}</th>
<th>{% trans 'Description' %}</th>
<th>{% trans 'Action' %}</th>
</tr>
</thead>
<tbody>
{% for day_off in days_off %}
<tr>
<td>{{ day_off.start_date }}</td>
<td>{{ day_off.end_date }}</td>
<td>{{ day_off.description }}</td>
<td>
<div class="buttons-container">
{% if superuser %}
<a href="{% url 'appointment:update_day_off_id' day_off_id=day_off.id staff_user_id=user.id %}"
class="modify-btn button-color-blue">
<i class="fas fa-pen"></i>
</a>
{% else %}
<a href="{% url 'appointment:update_day_off' day_off_id=day_off.id %}"
class="modify-btn button-color-blue">
<i class="fas fa-pen"></i>
</a>
{% endif %}
{% translate "Are you sure you want to delete this working hours?" as d_modal_message %}
{% if superuser %}
<a href="javascript:void(0)"
onclick="showModal('{{ modal_title }}', '{{ d_modal_message }}', '{{ delete_btn_modal }}', '{% url 'appointment:delete_day_off_id' day_off_id=day_off.id staff_user_id=user.id %}', null)"
class="modify-btn button-color-red">
<i class="fas fa-trash"></i>
</a>
{% else %}
<a href="javascript:void(0)"
onclick="showModal('{{ modal_title }}', '{{ d_modal_message }}', '{{ delete_btn_modal }}', '{% url 'appointment:delete_day_off' day_off_id=day_off.id %}', null)"
class="modify-btn button-color-red">
<i class="fas fa-trash"></i>
</a>
{% endif %}
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="4">{% trans 'No days off have been set' %}.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
<!-- Working Hours Information Section -->
<section class="profile-section">
<h2>{% trans 'Working Hours' %}</h2>
<a href="{% url 'appointment:add_working_hours_id' user.id %}"
class="section-content-button modify-btn button-color-green">
<i class="fas fa-add"></i>
</a>
<small>
{% trans "Note: If you are a staff member, your working hours will be used to determine when you are available for appointments." %}
</small>
<div class="responsive-table-container">
<table>
<thead>
<tr>
<th>{% trans 'Day' %}</th>
<th>{% trans 'Start time' %}</th>
<th>{% trans 'End time' %}</th>
<th>{% trans 'Action' %}</th>
</tr>
</thead>
<tbody>
{% for working_hour in working_hours %}
<tr>
<td>{{ working_hour.get_day_of_week_str }}</td>
<td>{{ working_hour.start_time|time:"g:i A" }}</td>
<td>{{ working_hour.end_time|time:"g:i A" }}</td>
<td>
<div class="buttons-container">
{% if superuser %}
<a href="{% url 'appointment:update_working_hours_id' working_hours_id=working_hour.id staff_user_id=user.id %}"
class="modify-btn button-color-blue">
<i class="fas fa-pen"></i>
</a>
{% else %}
<a href="{% url 'appointment:update_working_hours' working_hours_id=working_hour.id %}"
class="modify-btn button-color-blue">
<i class="fas fa-pen"></i>
</a>
{% endif %}
{% translate "Are you sure you want to delete this working hours?" as w_modal_message %}
{% if superuser %}
<a href="javascript:void(0)"
onclick="showModal('{{ modal_title }}', '{{ w_modal_message }}', '{{ delete_btn_modal }}', '{% url 'appointment:delete_working_hours_id' working_hours_id=working_hour.id staff_user_id=user.id %}', null)"
class="modify-btn button-color-red">
<i class="fas fa-trash"></i>
</a>
{% else %}
<a href="javascript:void(0)"
onclick="showModal('{{ modal_title }}', '{{ w_modal_message }}', '{{ delete_btn_modal }}', '{% url 'appointment:delete_working_hours' working_hours_id=working_hour.id %}', null)"
class="modify-btn button-color-red">
<i class="fas fa-trash"></i>
</a>
{% endif %}
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="4">{% trans 'No working hours have been set' %}.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
<!-- Service Information Section -->
<section class="profile-section">
<h2>{% trans 'Service Offered' %}</h2>
<small>
{% if not superuser %}
{% trans "To add/modify a new service, make a request to an admin." %}
{% trans "Changes made in one service will change it for every staff member." %}
{% endif %}
</small>
<div class="responsive-table-container">
<table>
<thead>
<tr>
<th>{% trans 'Name' %}</th>
<th>{% trans 'Description' %}</th>
<th>{% trans 'Duration' %}</th>
<th>{% trans 'Price' %}</th>
<th>{% trans 'Down payment' %}</th>
</tr>
</thead>
<tbody>
{% for service in services_offered %}
<tr>
<td>{{ service.name }}</td>
<td>{{ service.description|default:"N/A" }}</td>
<td>{{ service.get_duration }}</td>
<td>{{ service.get_price_text }}</td>
<td>{{ service.get_down_payment_text }}</td>
</tr>
{% empty %}
<tr>
<td colspan="5">{% trans 'No service offered yet' %}.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
</div>
{% include 'modal/confirm_modal.html' %}
</section>
{% endblock %}
{% block customJS %}
<!-- Bootstrap's JS and CSS (if not already included) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"
integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.11.8/umd/popper.min.js"
integrity="sha512-TPh2Oxlg1zp+kz3nFA0C5vVC6leG/6mm1z9+mA81MI5eaUVqasPLO8Cuk4gMF4gUfP5etR73rgU/8PNMsSesoQ=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/js/bootstrap.min.js"
integrity="sha512-WW8/jxkELe2CAiE4LvQfwm1rajOS8PHasCCx+knHG0gBHt8EXxS6T6tJRTGuDQVnluuAvMxWF4j8SNFDKceLFg=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- Our custom modal JS -->
<script src="{% static 'js/modal/show_modal.js' %}"></script>
<script src="{% static 'js/js-utils.js' %}"></script>
{% endblock %}

View File

@ -0,0 +1,165 @@
{% extends BASE_TEMPLATE %}
{% load i18n %}
{% load static %}
{% block customCSS %}
<link rel="stylesheet" type="text/css" href="{% static 'css/appointments-user-details.css' %}"/>
{% endblock %}
{% block title %}
{% translate 'Client Information' %} - {{ ar.get_service_name }}
{% endblock %}
{% block description %}
{% blocktranslate with service_name=ar.get_service_name %}
Your appointment request for {{ service_name }} has been submitted.
Please provide your information to create an account and complete the payment process.
{% endblocktranslate %}
{% endblock %}
{% block body %}
<div class="main-container">
<div class="body-container">
{% if messages %}
{% for message in messages %}
<div class="alert alert-dismissible {% if message.tags %}alert-{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}danger{% else %}{{ message.tags }}{% endif %}{% endif %}"
role="alert">{{ message }}</div>
{% endfor %}
{% endif %}
<form method="post"
action="{% url 'appointment:appointment_client_information' ar.id ar.get_id_request %}"
class="page-body">
{% csrf_token %}
<div class="appointment-user-info">
<div class="appointment-user-info-title">
<div class="title">
{% trans "Fill out your details" %}
</div>
</div>
<hr class="second-part">
<div class="user-info-input">
<div class="user-info" id="user-info">
<h1 class="description-title">{% trans "Tell us a bit about yourself" %}</h1>
<div class="already-have-account">
<div>
{% trans 'Already have an account?' %}
<a href="#">{% trans 'Log in' %}</a> {% trans 'for faster booking.' %}
</div>
</div>
<div class="name-email">
<label for="{{ form.name.id_for_label }}" class="name">{% trans "Full Name" %} *<br>
{{ client_data_form.name }}
</label>
<label for="{{ form.email.id_for_label }}" class="email">{% trans "Email" %} *<br>
{{ client_data_form.email }}
</label>
</div>
<div class="receive-email">
<label for="{{ form.want_reminder.id_for_label }}">
{{ form.want_reminder }}
{% trans "I want to receive an EMAIL reminder 24 hours before this session starts" %}
</label>
</div>
<div class="phone-number">
<label for="{{ form.phone.id_for_label }}">
{% trans "Phone" %} *<br>
</label>
<div class="phone-input-container">
{{ form.phone }}
</div>
</div>
<div class="address">
<label for="{{ form.address.id_for_label }}">{% trans "City and State" %} * :<br>
{{ form.address }}
</label>
</div>
<div class="additional-information">
<label for="{{ form.additional_info.id_for_label }}">{% trans 'Additional Information' %}<br>
{{ form.additional_info }}
</label>
</div>
</div>
</div>
</div>
<div class="service-description-and-pay">
<div class="service-details-title">{% trans "Service Details" %}</div>
<hr class="second-part">
<div class="service-description-content">
<div class="item-name">{{ ar.get_service_name }}</div>
<div id="service-datetime-chosen"
class="service-datetime-chosen">
{{ ar.date }}&nbsp;{% trans "at" %}&nbsp;{{ ar.start_time }}
</div>
<div>{{ ar.service.get_duration }}</div>
</div>
<hr class="second-part">
{% if ar.is_a_paid_service %}
{% if APPOINTMENT_PAYMENT_URL %}
<div class="service-payment">
<div class="payment-details-title">{% trans "Payment Details" %}</div>
<div class="total">
<div>{% trans "Total" %}</div>
<div>${{ ar.get_service_price }}</div>
</div>
<div class="payment-options">
<button type="submit" class="btn btn-dark btn-pay-full" name="payment_type"
value="full">
{% trans "Pay" %}
</button>
{% if ar.accepts_down_payment %}
<button type="submit" class="btn btn-dark btn-pay-down-payment"
name="payment_type"
value="down">
{% trans "Down Payment" %} (${{ ar.get_service_down_payment }})
</button>
{% endif %}
</div>
</div>
{% else %}
<button type="submit" class="btn btn-dark btn-submit-appointment" name="payment_type"
value="full">
{% trans "Finish" %}
</button>
{% endif %}
{% else %}
<button type="submit" class="btn btn-dark btn-submit-appointment" name="payment_type"
value="full">
{% trans "Finish" %}
</button>
{% endif %}
</div>
</form>
</div>
</div>
{% endblock %}
{% block customJS %}
<script>
document.addEventListener('DOMContentLoaded', function () {
const messageElements = document.querySelectorAll('.alert-dismissible');
setTimeout(function () {
messageElements.forEach(function (element) {
element.style.display = 'none';
});
}, 3000);
// Get the form and the 'submit' button
const form = document.querySelector('form');
const submitButtons = Array.from(form.querySelectorAll('button[type="submit"]'));
// Disable the 'submit' buttons initially
submitButtons.forEach(button => button.disabled = true);
// Listen for input events on the form
form.addEventListener('input', function () {
// Get all required fields
const requiredFields = Array.from(form.querySelectorAll('[required]'));
// Check if all required fields are filled
const allFieldsFilled = requiredFields.every(field => field.value.trim() !== '');
// Enable or disable the 'submit' buttons based on whether all fields are filled
submitButtons.forEach(button => button.disabled = !allFieldsFilled);
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,134 @@
{% extends BASE_TEMPLATE %}
{% load i18n %}
{% load static %}
{% block customCSS %}
<link rel="stylesheet" type="text/css" href="{% static 'css/appt-common.css' %}"/>
<link rel="stylesheet" type="text/css" href="{% static 'css/appointments.css' %}"/>
{% endblock %}
{% block title %}
{{ page_title }}
{% endblock %}
{% block description %}
{{ page_description }}
{% endblock %}
{% block body %}
<div class="">
<div class="djangoAppt_main-container">
<div class="djangoAppt_body-container">
<h3 class="page-title">
{% if page_header %}{{ page_header }}{% else %}{{ service.name }}{% endif %} </h3>
<small class="page-description">
{% trans "Check out our availability and book the date and time that works for you" %}
</small>
<hr>
<div class="djangoAppt_page-body">
<div class="djangoAppt_appointment-calendar">
<div class="djangoAppt_appointment-calendar-title-timezone">
<div class="djangoAppt_title">
{% trans "Select a date and time" %}
</div>
<div class="djangoAppt_timezone-details">
{% trans "Timezone" %}:&nbsp;{{ timezoneTxt }}
</div>
</div>
<hr class="djangoAppt_second-part">
<div class="djangoAppt_calendar-and-slot">
<div class="djangoAppt_calendar" id="calendar">
</div>
<div class="djangoAppt_slot">
<div class="djangoAppt_date_chosen">{{ date_chosen }}</div>
<div class="slot-container">
<div class="error-message"></div>
<ul id="slot-list" class="djangoAppt_slot-list">
<!-- Slot list will be updated dynamically by the AJAX request -->
</ul>
</div>
</div>
</div>
{% if rescheduled_date %}
<div class="form-group" style="margin-top: 10px">
<label for="reason_for_rescheduling">{% trans "Reason for rescheduling" %}:</label>
<textarea name="reason_for_rescheduling" id="reason_for_rescheduling"
class="form-control" rows="1" required></textarea>
</div>
{% endif %}
</div>
<div class="djangoAppt_service-description">
<form method="post" action="{% url 'appointment:appointment_request_submit' %}"
class="appointment-form">
{% csrf_token %}
<div class="staff-members-list">
<label class="djangoAppt_item-name" for="staff_id">{{ label }}</label>
<select name="staff_member" id="staff_id">
{% if not staff_member %}
<option value="none"
selected>{% trans 'Please select a staff member' %}</option>
{% endif %}
{% for sf in all_staff_members %}
<option value="{{ sf.id }}"
{% if staff_member and staff_member.id == sf.id %}selected{% endif %}>{{ sf.get_staff_member_name }}</option>
{% endfor %}
</select>
</div>
<div>{% trans "Service Details" %}</div>
<hr class="djangoAppt_second-part">
<div class="djangoAppt_service-description-content">
<p class="djangoAppt_item-name">{{ service.name }}</p>
<p id="service-datetime-chosen" class="service-datetime-chosen">{{ date_chosen }}</p>
<p>{{ service.get_duration }}</p>
<p>{{ service.get_price_text }}</p>
<button type="submit"
class="btn btn-phoenix-primary btn-submit-appointment"
disabled>{% trans 'Next' %}</button>
</div>
</form>
</div>
</div>
{% if messages %}
{% for message in messages %}
<div class="alert alert-dismissible {% if message.tags %}alert-{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}danger{% else %}{{ message.tags }}{% endif %}{% endif %}"
role="alert">{{ message }}</div>
{% endfor %}
{% endif %}
</div>
</div>
</div>
{% endblock %}
{% block customJS %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/moment.js"
integrity="sha512-3CuraBvy05nIgcoXjVN33mACRyI89ydVHg7y/HMN9wcTVbHeur0SeBzweSd/rxySapO7Tmfu68+JlKkLTnDFNg=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.45/moment-timezone-with-data.min.js"
integrity="sha512-t/mY3un180WRfsSkWy4Yi0tAxEDGcY2rAEx873hb5BrkvLA0QLk54+SjfYgFBBoCdJDV1H86M8uyZdJhAOHeyA=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/6.1.10/index.global.min.js"
integrity="sha512-JCQkxdym6GmQ+AFVioDUq8dWaWN6tbKRhRyHvYZPupQ6DxpXzkW106FXS1ORgo/m3gxtt5lHRMqSdm2OfPajtg=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
const timezone = "{{ timezoneTxt }}";
const locale = "{{ locale }}";
const availableSlotsAjaxURL = "{% url 'appointment:available_slots_ajax' %}";
const requestNextAvailableSlotURLTemplate = "{% url 'appointment:request_next_available_slot' service_id=0 %}";
const getNonWorkingDaysURL = "{% url 'appointment:get_non_working_days_ajax' %}";
const serviceId = "{{ service.id }}";
const serviceDuration = parseInt("{{ service.duration.total_seconds }}") / 60;
const rescheduledDate = "{{ rescheduled_date }}";
const appointmentRequestId = "{{ ar_id_request }}";
const appointmentRequestSubmitURL = "{% url 'appointment:appointment_request_submit' %}";
const appointmentRescheduleURL = "{% url 'appointment:reschedule_appointment_submit' %}";
</script>
<script>
const requestNonAvailableSlotBtnTxt = "{% trans 'Request next available slot' %}";
const noStaffMemberSelectedTxt = "{% trans 'No staff member selected.' %}";
const selectTimeSlotWarningTxt = "{% trans 'Please select a time slot before submitting the appointment request.' %}";
const dateInPastErrorTxt = "{% trans 'Date is in the past.' %}";
const selectDateAndTimeAlertTxt = "{% trans 'Please select a date and time' %}";
</script>
<script src="{% static 'js/appointments.js' %}"></script>
<script src="{% static 'js/app_admin/staff_index.js' %}"></script>
<script src="{% static 'js/js-utils.js' %}"></script>
{% endblock %}

View File

@ -0,0 +1,36 @@
{% extends BASE_TEMPLATE %}
{% load i18n %}
{% load static %}
{% block customCSS %}
<link rel="stylesheet" type="text/css" href="{% static 'css/thank_you.css' %}"/>
{% endblock %}
{% block title %}
{{ page_title }}
{% endblock %}
{% block description %}
{{ page_description }}
{% endblock %}
{% block body %}
<div class="container content-body-apd">
<div class="main-content">
<h1 class="thank-you-title">{% trans "See you soon" %} !</h1>
<p class="thank-you-message">{% trans "We've successfully scheduled your appointment! Please check your email for all the details" %}.</p>
<p class="appointment-details-title">{% trans "Appointment details" %}:</p>
<ul class="appointment-details">
<li>{% trans 'Service' %}: {{ appointment.get_service_name }}</li>
<li>{% trans 'Appointment Date' %}: {{ appointment.get_appointment_date }}</li>
<li>{% trans 'Appointment Time' %}: {{ appointment.get_start_time }}</li>
<li>{% trans 'Duration' %}: {{ appointment.get_service_duration }}</li>
</ul>
{% if messages %}
{% for message in messages %}
<div class="alert alert-dismissible {% if message.tags %}alert-{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}danger{% else %}{{ message.tags }}{% endif %}{% endif %}"
role="alert">{{ message }}</div>
{% endfor %}
{% endif %}
</div>
</div>
{% endblock %}
{% block customJS %}
<script src="{% static 'js/js-utils.js' %}"></script>
{% endblock %}

View File

@ -0,0 +1,39 @@
{% extends BASE_TEMPLATE %}
{% load i18n %}
{% load static %}
{% block customCSS %}
<link rel="stylesheet" href="{% static 'css/verification_code.css' %}">
{% endblock %}
{% block title %}{% trans 'Enter Verification Code' %}{% endblock %}
{% block description %}{% trans 'Enter Verification Code' %}{% endblock %}
{% block body %}
<div class="vcode-container">
<div class="vcode-card">
<h1 class="vcode-title">{% trans 'Enter Verification Code' %}</h1>
<p class="vcode-instruction">{% trans "We've sent a verification code to your email. Please enter it below" %}:</p>
<form method="post" class="vcode-form">
{% csrf_token %}
<div class="vcode-input-group">
<label for="verification-code" class="vcode-label">{% trans 'Code' %}:</label>
<input type="text" id="verification-code" name="code" class="vcode-input" required>
</div>
<button type="submit" class="vcode-button">{% trans 'Submit' %}</button>
</form>
{% if messages %}
{% for message in messages %}
<div class="vcode-alert vcode-alert-{% if message.tags %}{{ message.tags }}{% endif %}">
{{ message }}
</div>
{% endfor %}
{% endif %}
</div>
</div>
{% endblock %}
{% block customJS %}
{% endblock %}

View File

@ -0,0 +1,79 @@
{% extends BASE_TEMPLATE %}
{% load i18n %}
{% load static %}
{% block customCSS %}
<style>
.content-body-apd {
font-family: 'Poppins', sans-serif;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 40vh;
}
.confirmation-message {
max-width: 600px;
text-align: center;
padding: 20px;
background-color: #ffffff;
border: 1px solid #dee2e6;
border-radius: 5px;
box-shadow: 0 4px 6px rgba(0, 0, 0, .1);
}
.confirmation-message h1 {
color: #333;
font-size: 24px;
font-weight: 600;
margin-bottom: 20px;
}
.confirmation-message p {
color: #555;
font-size: 16px;
line-height: 1.6;
}
.confirmation-message a {
display: inline-block;
margin-top: 25px;
padding: 10px 25px;
background-color: #007bff;
color: #ffffff;
border-radius: 5px;
text-decoration: none;
font-weight: 500;
}
.confirmation-message a:hover {
background-color: #0056b3;
}
</style>
{% endblock %}
{% block title %}
{% trans "Rescheduling Successful" %}
{% endblock %}
{% block description %}
{% trans "Your appointment rescheduling was successful. Please confirm via email." %}
{% endblock %}
{% block body %}
<div class="container content-body-apd">
<div class="main-content">
<div class="confirmation-message">
<h1>{% trans "Rescheduling Successful" %}</h1>
<p>{% trans "Your appointment rescheduling request has been successfully submitted. Please check your email and click on the confirmation link to finalize the rescheduling process." %}</p>
<a href="/">{% trans "Go to Homepage" %}</a>
</div>
{% if messages %}
{% for message in messages %}
<div class="alert alert-dismissible {% if message.tags %}alert-{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}danger{% else %}{{ message.tags }}{% endif %}{% endif %}"
role="alert">{{ message }}</div>
{% endfor %}
{% endif %}
</div>
</div>
{% endblock %}
{% block customJS %}
<script src="{% static 'js/js-utils.js' %}"></script>
{% endblock %}

View File

@ -0,0 +1,104 @@
{% extends BASE_TEMPLATE %}
{% load i18n %}
{% load static %}
{% block customCSS %}
<style>
body {
font-family: 'Arial', sans-serif;
background-color: #f5f5f5;
color: #333;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.container {
background-color: #fff;
padding: 40px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 400px;
}
h2 {
color: #333;
margin-bottom: 20px;
}
.messages {
list-style: none;
padding: 0;
margin-bottom: 20px;
}
.messages li {
background-color: #f8d7da;
color: #721c24;
padding: 10px;
margin-bottom: 10px;
border-radius: 4px;
}
form {
display: flex;
flex-direction: column;
}
form p {
margin-bottom: 10px;
}
input[type="password"] {
padding: 10px;
margin-bottom: 20px;
border: 1px solid #ccc;
border-radius: 4px;
}
button {
background-color: #007bff;
color: #fff;
padding: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background-color: #0056b3;
}
</style>
{% endblock %}
{% block title %}
{% trans 'Reset Your Password' %}
{% endblock %}
{% block description %}
{% trans 'Reset Your Password' %}
{% endblock %}
{% block body %}
<div class="container">
<h2>{% trans 'Reset Your Password' %}</h2>
<!-- Display messages -->
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
<!-- Password Reset Form -->
<form method="post">
{% csrf_token %}
{{ form.as_p }} <!-- Renders the form fields -->
<button type="submit">{% trans 'Reset Password' %}</button>
</form>
{% endblock %}
{% block customJS %}
<script src="{% static 'js/js-utils.js' %}"></script>
{% endblock %}

View File

@ -0,0 +1,53 @@
{% extends BASE_TEMPLATE %}
{% load i18n %}
{% load static %}
{% block customCSS %}
<style>
body {
font-family: 'Nunito', sans-serif;
background-color: #f7f7f7;
margin: 0;
padding: 20px;
}
.container {
max-width: 600px;
background: #ffffff;
margin: 40px auto;
padding: 40px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.title {
color: #333;
text-align: center;
}
.message {
font-size: 18px;
text-align: center;
line-height: 1.6;
margin-top: 20px;
}
</style>
{% endblock %}
{% block title %}{{ page_title }}{% endblock %}
{% block description %}{{ page_description }}{% endblock %}
{% block body %}
<div class="container">
<h1 class="title">{{ page_title }}</h1>
<p class="message">
{{ page_message }}
</p>
{% if messages %}
{% for message in messages %}
<div class="alert alert-dismissible {% if message.tags %}alert-{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}danger{% else %}{{ message.tags }}{% endif %}{% endif %}"
role="alert">{{ message }}</div>
{% endfor %}
{% endif %}
</div>
{% endblock %}
{% block customJS %}
<script src="{% static 'js/js-utils.js' %}"></script>
{% endblock %}

View File

@ -46,7 +46,9 @@
<link href="{% static 'css/theme-rtl.min.css' %}" type="text/css" rel="stylesheet" id="style-rtl">
<link href="{% static 'css/user-rtl.min.css' %}" type="text/css" rel="stylesheet" id="user-style-rtl">
{% endif %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"
integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
{% block customCSS %}
{% endblock %}
@ -56,13 +58,13 @@
<main class="main" id="top">
{% include 'header.html' %}
<div class="content">
<section class="content">
{% block content %}
{% endblock content%}
{% block body %}
{% endblock body%}
</section>
{% include 'footer.html' %}
</div>
</main>

View File

@ -0,0 +1,74 @@
{% load i18n %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% translate 'Appointment Request Notification' %}</title>
<style>
body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
background-color: #f5f5f5;
color: #333;
padding: 20px;
margin: 0;
}
.email-container {
background-color: #ffffff;
padding: 25px;
margin: 0 auto;
max-width: 650px;
border-radius: 5px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
}
h1 {
color: #333;
font-size: 24px;
}
p {
font-size: 16px;
line-height: 1.6;
}
.appointment-details {
background-color: #f9f9f9;
padding: 15px;
margin-top: 20px;
border-left: 5px solid #007bff;
}
.footer {
margin-top: 30px;
font-size: 14px;
text-align: left;
color: #999;
}
</style>
</head>
<body>
<div class="email-container">
<h1>{% translate 'New Appointment Request' %}</h1>
<p>{% translate 'Dear Admin,' %}</p>
<p>{% translate 'You have received a new appointment request. Here are the details:' %}</p>
<div class="appointment-details">
<p><strong>{% translate 'Client Name' %}:</strong> {{ client_name }}</p>
<p><strong>{% translate 'Service Requested' %}:</strong> {{ appointment.get_service_name }}</p>
<p><strong>{% translate 'Appointment Date' %}:</strong> {{ appointment.appointment_request.date }}</p>
<p><strong>{% translate 'Time' %}:</strong> {{ appointment.appointment_request.start_time }}
- {{ appointment.appointment_request.end_time }}</p>
<p><strong>{% translate 'Contact Details' %}:</strong> {{ appointment.phone }} | {{ appointment.client.email }}</p>
<p><strong>{% translate 'Additional Info' %}:</strong> {{ appointment.additional_info|default:"N/A" }}</p>
</div>
<p>{% translate 'Please review the appointment request and take the necessary action.' %}</p>
<div class="footer">
<p>{% translate 'This is an automated message. Please do not reply directly to this email.' %}</p>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,97 @@
{% load i18n %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% translate 'Appointment Reminder' %}</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background-color: #f0f0f0;
color: #636363;
}
.email-container {
max-width: 600px;
background: #ffffff;
margin: 0 auto;
padding: 25px;
border-radius: 8px;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
}
.email-header {
color: #ffffff;
background-color: rgb(5, 100, 129);
padding: 15px;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
text-align: center;
}
.email-body {
padding: 25px;
line-height: 1.6;
}
.email-footer {
padding-top: 20px;
font-size: 14px;
text-align: left;
color: #aaaaaa;
}
.button {
background-color: rgb(5, 100, 129);
color: #ffffff;
padding: 10px 20px;
border-radius: 5px;
text-decoration: none;
display: inline-block;
margin-top: 20px;
text-decoration-color: white;
}
.button:hover {
background-color: #37aee9;
color: #ffffff;
text-decoration: none;
}
</style>
</head>
<body>
<div class="email-container">
<div class="email-header">
<h2>{% translate 'Appointment Reminder' %}</h2>
</div>
<div class="email-body">
<p>
{% if recipient_type == 'client' %}
{% translate 'Dear' %} {{ first_name }},
{% else %}
{% translate 'Dear Administrator,' %}
{% endif %}
</p>
<p>{% translate 'This is a reminder for your upcoming appointment.' %}</p>
<p><strong>{% translate 'Service' %}:</strong> {{ appointment.get_service_name }}</p>
<p><strong>{% translate 'Date' %}:</strong> {{ appointment.appointment_request.date }}</p>
<p><strong>{% translate 'Time' %}:</strong> {{ appointment.appointment_request.start_time }}
- {{ appointment.appointment_request.end_time }}</p>
<p><strong>{% translate 'Location' %}:</strong> {{ appointment.address }}</p>
{% if recipient_type == 'client' %}
<p>{% translate 'If you need to reschedule, please click the button below or contact us for further assistance.' %}</p>
<a href="{{ reschedule_link }}" class="button">{% translate 'Reschedule Appointment' %}</a>
<p>{% translate 'Thank you for choosing us!' %}</p>
{% else %}
<p>{% translate 'Please ensure the appointment setup is complete and ready for the client.' %}</p>
{% endif %}
</div>
<div class="email-footer">
{% translate 'This is an automated message. Please do not reply directly to this email.' %}
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,93 @@
{% load i18n %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% trans "Appointment Reschedule Confirmation" %}</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 0;
}
.email-container {
max-width: 600px;
background: #ffffff;
margin: 20px auto;
padding: 20px;
border: 1px solid #dddddd;
}
.email-content {
margin: 20px 0;
}
.fallback-link {
margin-top: 20px;
}
a {
color: #007bff;
text-decoration: none;
}
</style>
</head>
<body>
<!-- email_template.html -->
<div class="email-container">
<h2>{% trans "Appointment Reschedule" %}</h2>
<div class="email-content">
{% if is_confirmation %}
<p>{% trans "Dear" %} {{ first_name }},</p>
{% else %}
<p>{% trans "Hi" %},</p>
{% endif %}
{% if is_confirmation %}
<p>
{% trans "You have requested to reschedule your appointment. Please review the changes below and confirm:" %}
</p>
{% else %}
<p>
{% trans "An appointment with" %} <b>{{ client_name }}</b> {% trans "for the service" %}
<b>{{ service_name }}</b> {% trans "has been rescheduled." %}
{% if reason_for_rescheduling %}
<br><b>{% trans "Reason for rescheduling:" %}</b>
{{ reason_for_rescheduling }}{% endif %}
</p>
{% endif %}
<p>
<b>{% trans "Original Appointment:" %}</b><br>
{% trans "Date" %}: {{ old_date }}<br>
{% trans "Time" %}: {{ old_start_time }} {% trans ' to ' %} {{ old_end_time }}
</p>
<p>
<b>{% trans "Rescheduled Appointment:" %}</b><br>
{% trans "Date" %}: {{ reschedule_date }}<br>
{% trans "Time" %}: {{ start_time }} {% trans ' to ' %} {{ end_time }}
</p>
{% if is_confirmation %}
<p>
{% trans "This link will expire in 5 minutes. If you do not confirm within this time frame, you will need to submit a new reschedule request." %}
</p>
<a href="{{ confirmation_link }}"
style="background-color: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">
{% trans "Confirm Appointment" %}
</a>
<div class="fallback-link">
{% trans "If the button above does not work, please copy and paste the following link into your browser:" %}
<br>
<a href="{{ confirmation_link }}">{{ confirmation_link }}</a>
</div>
{% endif %}
</div>
<div class="email-footer">
<p>{% trans "Thank you," %}<br>{{ company }}</p>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,290 @@
{% load i18n %}
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
<style type="text/css">
/* CLIENT-SPECIFIC STYLES */
body, table, td, a {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table, td {
mso-table-lspace: 0;
mso-table-rspace: 0;
}
img {
-ms-interpolation-mode: bicubic;
}
/* RESET STYLES */
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
}
table {
border-collapse: collapse !important;
}
body {
height: 100% !important;
margin: 0 !important;
padding: 0 !important;
width: 100% !important;
}
/* iOS BLUE LINKS */
a[x-apple-data-detectors] {
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
.appointment-details li {
padding: 0 !important; /* Add padding left to the details */
}
/* MOBILE STYLES */
@media screen and (max-width: 500px) {
.img-max {
width: 100% !important;
max-width: 100% !important;
height: auto !important;
}
.max-width {
max-width: 100% !important;
}
.mobile-wrapper {
width: 85% !important;
max-width: 85% !important;
}
.mobile-padding {
padding-left: 5% !important;
padding-right: 5% !important;
}
}
/* ANDROID CENTER FIX */
div[style*="margin: 16px 0;"] {
margin: 0 !important;
}
</style>
</head>
<body style="margin: 0 !important; padding: 0; !important background-color: #ffffff;" bgcolor="#ffffff">
<!-- HIDDEN PRE-HEADER TEXT -->
<div style="display: none; font-size: 1px; color: #fefefe; line-height: 1px; font-family: Open Sans, Helvetica, Arial, sans-serif; max-height: 0; max-width: 0; opacity: 0; overflow: hidden;">
{% if pre_header %}
{{ pre_header }}
{% endif %}
</div>
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td align="center" valign="top" width="100%" bgcolor="#3b4a69"
style="background: #3b4a69 url('{% static 'img/email_hd_bg.jpg' %}'); background-size: cover; padding: 50px 15px;"
class="mobile-padding">
<!--[if (gte mso 9)|(IE)]>
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
<tr>
<td align="center" valign="top" width="600">
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width:600px;">
<tr>
<td align="center" valign="top" style="padding: 0 0 20px 0;">
{% comment %}<!--Add a logo img in future-->{% endcomment %}
</td>
</tr>
<tr>
<td align="center" valign="top"
style="padding: 0; font-family: Open Sans, Helvetica, Arial, sans-serif;">
<h1 style="font-size: 28px; color: #ffffff;">{{ main_title }}</h1>
</td>
</tr>
<tr>
<td align="center" valign="top"
style="padding: 0 0 35px 0; font-family: Open Sans, Helvetica, Arial, sans-serif;">
<table align="center" border="0" cellpadding="0" cellspacing="0" width="80%"
style="max-width: 200px;">
<tr>
<td align="center" bgcolor="red"
style="color: #ffffff; font-family: Open Sans, Helvetica, Arial, sans-serif; font-size: 14px; padding: 10px; border-radius: 3px 3px 0 0;">
{{ month_year }}
</td>
</tr>
<tr>
<td align="center" bgcolor="#ffffff"
style="color: #444444; font-family: Open Sans, Helvetica, Arial, sans-serif; font-size: 48px; padding: 15px; border-radius: 0 0 3px 3px;">
{{ day }}
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="center" valign="top"
style="font-family: Open Sans, Helvetica, Arial, sans-serif; padding-bottom: 30px;">
<p style="color: #ffffff; font-size: 14px; line-height: 24px; margin: 0;">
{% trans 'Thank you for choosing us.' %}
</p>
</td>
</tr>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
<tr style="max-width: 600px !important;">
<td align="center" valign="top" width="100%" bgcolor="#ffffff" style="padding: 50px 15px;"
class="mobile-padding">
<!--[if (gte mso 9)|(IE)]>
<table align="center" border="0" cellspacing="0" cellpadding="0" width="500">
<tr>
<td align="center" valign="top" width="500">
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width:500px;">
{% if account_details %}
<tr>
<td align="left" valign="top"
style="padding: 0; font-family: Open Sans, Helvetica, Arial, sans-serif;">
<p style="font-size: 14px; margin-top: 15px !important;">Hi {{ first_name }},</p>
<p style="font-size: 14px; margin-top: 5px !important;">{{ message_1 }}</p>
</td>
</tr>
<tr>
<td align="left" valign="top"
style="padding: 0; font-family: Open Sans, Helvetica, Arial, sans-serif;">
<p style="font-size: 14px; margin-top: 5px !important;">{{ message_2 }}</p>
</td>
</tr>
{% if activation_link %}
<tr>
<td align="left" valign="top"
style="padding: 0; font-family: Open Sans, Helvetica, Arial, sans-serif;">
<h3>{% trans 'Account Activation' %}</h3>
<p style="font-size: 14px; margin-top: 15px !important;">
{% blocktranslate with link=activation_link %}
To activate your account and set your password, please use the following secure
link: <a href="{{ link }}" style="color: #1155cc;">Set Your Password</a>. Please
note that this link will expire in 2 days for your security.
{% endblocktranslate %}
</p>
</td>
<tr>
{% endif %}
</tr>
<tr>
<td align="left" valign="top"
style="font-family: Open Sans, Helvetica, Arial, sans-serif; padding-top: 0;">
<h3>{% trans "Account Information" %}</h3>
<div style="color: #000000; font-size: 14px; line-height: 24px;">
<ul>
{% for key, value in account_details.items %}
<li>{{ key }}: {{ value }}</li>
{% endfor %}
</ul>
</div>
</td>
</tr>
{% endif %}
{% if more_details %}
<tr>
<td align="left" valign="top"
style="font-family: Open Sans, Helvetica, Arial, sans-serif; padding-top: 0;">
<h3>{% trans "Appointment Details" %}</h3>
<div style="color: #000000; font-size: 14px; line-height: 24px;">
<ul>
{% for key, value in more_details.items %}
<li>{{ key }}: {{ value }}</li>
{% endfor %}
</ul>
</div>
</td>
</tr>
{% endif %}
{% if reschedule_link %}
<tr>
<td align="left" valign="top"
style="padding: 0; font-family: Open Sans, Helvetica, Arial, sans-serif;">
<h3>{% trans 'Rescheduling' %}</h3>
<p style="margin-top: 15px !important; font-size: 14px">
{% translate 'If your plans change and you need to reschedule your appointment, you can easily do so by following this link: ' %}
<a href="{{ reschedule_link }}">
{% translate 'Reschedule Appointment' %}
</a>
</p>
</td>
</tr>
{% endif %}
<tr>
<td align="left" valign="top"
style="padding: 0; font-family: Open Sans, Helvetica, Arial, sans-serif;">
<h3>{% trans 'Support' %}</h3>
<p style="margin-top: 15px !important; font-size: 14px">
{% blocktranslate %}
Should you have any inquiries or require further assistance, our support team is here to
help. You can reach us anytime.
{% endblocktranslate %}
</p>
<p style="margin-top: 15px !important; font-size: 14px">
{% trans "We look forward to serving you and ensuring that your experience with us is both rewarding and satisfactory." %}
</p>
<p style="margin-top: 15px !important; font-size: 14px">{% trans "Warm regards" %},</p>
<p style="margin-top: 15px !important; font-size: 14px">{% trans "The Team" %}</p>
</td>
</tr>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
<tr>
<td align="center" height="100%" valign="top" width="100%" bgcolor="#f6f6f6" style="padding: 40px 15px;">
<!--[if (gte mso 9)|(IE)]>
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
<tr>
<td align="center" valign="top" width="600">
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width:600px;">
<tr>
<td align="center" valign="top"
style="padding: 0; font-family: Open Sans, Helvetica, Arial, sans-serif; color: #999999;">
&copy; {{ current_year }} {{ company }}. {% trans "All rights reserved" %}.
</td>
</tr>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</table>
</body>
</html>

View File

@ -34,7 +34,7 @@
</div>
</div>
<div class="col-12 col-sm-auto flex-1">
<h3 class="mb-2">{{ cars.first.id_car_make.get_local_name }}<span class="ms-2 text-body-tertiary fw-semibold">{{ cars.first.id_car_model.get_local_name }}</span></h3>
<div><h3 class="mb-2">{{ cars.first.id_car_make.get_local_name }}&nbsp;<span class="ms-2 text-body-tertiary fw-semibold">{{ cars.first.id_car_model.get_local_name }}</span></h3></div>
<p class="text-body-tertiary fw-semibold">{{ cars.first.id_car_serie.name }}, <span class="fs-10">{{ cars.first.id_car_trim.name }}</span></p>
</div>
</div>

View File

@ -0,0 +1,23 @@
{% load i18n %}
<!-- Generic Confirmation Modal -->
<div class="modal fade" id="confirmModal" tabindex="-1" role="dialog" aria-labelledby="modalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalLabel">Modal title</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"
onclick="closeConfirmModal()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" id="modalBody">
Modal body text goes here.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal"
onclick="closeConfirmModal()">{% trans 'Close' %}</button>
<a href="#" class="btn btn-primary" id="modalActionBtn">{% trans 'Action' %}</a>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,21 @@
{% load i18n %}
<!-- Error Modal -->
<div class="modal fade" id="errorModal" tabindex="-1" aria-labelledby="errorModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="errorModalLabel">{% trans 'Error' %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"
onclick="closeErrorModal()"></button>
</div>
<div class="modal-body">
<!-- Error message will be inserted here -->
<p id="errorModalMessage"></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"
onclick="closeErrorModal()">{% trans 'Close' %}</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,38 @@
{% load i18n %}
<!-- Event Details Modal -->
<div class="modal fade" id="eventDetailsModal" tabindex="-1" role="dialog" aria-labelledby="eventModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="eventModalLabel">{% trans 'Event Details' %}</h5>
<button type="button" class="btn-close" data-dismiss="modal" aria-label="Close"
onclick="closeModal()"></button>
</div>
<div class="modal-body" id="eventModalBody">
<!-- Event details will be populated here -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success" id="eventGoBtn"
onclick="goToEvent()">{% trans 'Go' %}</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal"
onclick="closeModal()">{% trans 'Close' %}
</button>
<button type="button" class="btn btn-secondary" id="eventCancelBtn" style="display: none;"
onclick="cancelEdit()">{% trans 'Cancel' %}
</button>
<button type="button" class="btn btn-primary" id="eventEditBtn"
onclick="toggleEditMode()">{% trans 'Edit' %}</button>
<button type="button" class="btn btn-primary" id="eventSubmitBtn" style="display: none;"
onclick="submitChanges()">{% trans 'Submit' %}
</button>
<button type="button" class="btn btn-danger" id="eventDeleteBtn"
onclick="deleteAppointment()" style="display: none;">
{% trans 'Delete' %}
</button>
</div>
</div>
</div>
</div>

View File

@ -55,7 +55,7 @@
</div>
<!-- ============================================-->
<!-- <section> begin ============================-->
<section class="pt-5 pb-9 bg-body-emphasis dark__bg-gray-1200 border-top">
<section class="pt-5 pb-9">
<div class="row-small mt-3">
<div class="d-flex justify-content-between align-items-end mb-4">
<h2 class="mb-0">{% trans 'Invoice' %}</h2>
@ -82,8 +82,8 @@
<div class="d-flex bg-success-subtle rounded flex-center me-3 mb-sm-3 mb-md-0 mb-xl-3 mb-xxl-0" style="width:32px; height:32px"><span class="text-success-dark" data-feather="dollar-sign" style="width:24px; height:24px"></span></div>
<div>
<p class="fw-bold mb-1">{% trans 'Paid Amount' %}</p>
<h4 class="fw-bolder text-nowrap {% if invoice.is_paid %}text-success{% endif %}">${{invoice.amount_paid}}</h4>
<h6 class="fw-bolder text-nowrap">Owned <span class="fw-semibold text-nowrap text-success">${{invoice.get_amount_open}}</span></h6>
<h4 class="fw-bolder text-nowrap {% if invoice.is_paid %}text-success{% endif %}">{{invoice.amount_paid}}&nbsp;{{ _("SAR") }}</h4>
<h6 class="fw-bolder text-nowrap">{{ _("Owned") }} <span class="fw-semibold text-nowrap text-success">{{invoice.get_amount_open|floatformat}}&nbsp;{{ _("SAR") }}</span></h6>
<div class="progress" style="height:17px">
<div class="progress-bar fw-semibold bg-{% if invoice.get_progress_percent < 100 %}secondary{% else %}success{% endif %} rounded-2" role="progressbar" style="width: {{invoice.get_progress_percent}}%" aria-valuenow="{{invoice.get_progress_percent}}" aria-valuemin="0" aria-valuemax="100">{{invoice.get_progress_percent}}%</div>
</div>
@ -135,9 +135,9 @@
<div>
<p class="fw-bold mb-1">{% trans 'Due Amount' %}</p>
{% if invoice.is_paid %}
<s><h4 class="fw-bolder text-nowrap">${{invoice.amount_due}} </h4></s>
<s><h4 class="fw-bolder text-nowrap">{{invoice.amount_due}}&nbsp;{{ _("SAR") }}</h4></s>
{% else %}
<h4 class="fw-bolder text-nowrap">${{invoice.amount_due}} </h4>
<h4 class="fw-bolder text-nowrap">{{invoice.amount_due}}&nbsp;{{ _("SAR") }}</h4>
{% endif %}
</div>
</div>
@ -259,7 +259,7 @@
</div>
</div>
</div>
</div>
</section>
{% endblock %}
{% block customJS %}

View File

@ -1,3 +1,4 @@
{% load i18n static%}
<!DOCTYPE html>
<html lang="en">
<head>
@ -44,6 +45,13 @@
.invoice-details {
margin-bottom: 2.5rem;
}
.invoice-details-en {
text-align: left;
}
.invoice-details-ar {
text-align: right;
}
.invoice-details p {
margin: 0.75rem 0;
color: #555;
@ -119,80 +127,84 @@
<div class="invoice-row" id="invoice-content">
<!-- Header -->
<div class="invoice-header">
<svg width="101" height="24" viewBox="0 0 101 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.6137 3.58771C10.4439 3.18691 10.1617 2.63007 9.74664 2.02802C9.29071 1.36664 8.6691 0.642639 7.85043 0.00667055C8.24423 -0.0103968 8.62134 0.00633189 8.97016 0.0449277C9.22762 0.296602 9.45627 0.570444 9.65587 0.851578C10.2117 1.63443 10.5253 2.45013 10.6078 2.94348C10.6117 2.96726 10.6204 2.98943 10.6329 3.00938C10.6454 2.98943 10.6541 2.96726 10.6581 2.94348C10.7405 2.45013 11.0542 1.63443 11.61 0.851578C11.8096 0.570444 12.0382 0.296602 12.2957 0.0449277C12.6445 0.00633189 13.0216 -0.0103968 13.4154 0.00667055C12.5967 0.642639 11.9751 1.36664 11.5192 2.02802C11.1042 2.63007 10.8219 3.18691 10.6521 3.58771V3.68024C10.6459 3.66492 10.6395 3.64933 10.6329 3.63346C10.6263 3.64933 10.6199 3.66492 10.6137 3.68024V3.58771Z" fill="#15447A"></path>
<path d="M10.6329 0.444309C10.3872 0.342418 10.0542 0.237088 9.6597 0.154569C9.79826 0.314955 9.92612 0.479313 10.0434 0.644481C10.2707 0.964706 10.4609 1.29156 10.6137 1.60401V1.68377C10.6201 1.6704 10.6264 1.65699 10.6329 1.64355C10.6394 1.65699 10.6458 1.6704 10.6521 1.68377V1.60401C10.805 1.29156 10.9951 0.964706 11.2224 0.644481C11.3397 0.479313 11.4676 0.314955 11.6061 0.154569C11.2116 0.237088 10.8787 0.342418 10.6329 0.444309Z" fill="#15447A"></path>
<path d="M9.35604 2.2307C8.85756 1.50759 8.15998 0.716893 7.22345 0.0624878C7.0191 0.090277 6.81206 0.1281 6.60382 0.177455C7.31803 0.54199 7.9033 1.09239 8.40781 1.65537C8.68137 1.96062 8.93833 2.27817 9.18023 2.57711C9.28281 2.70387 9.38274 2.82737 9.48001 2.94507C9.72692 3.24385 9.95688 3.50558 10.1809 3.71063C10.0223 3.33892 9.75523 2.80979 9.35604 2.2307Z" fill="#15447A"></path>
<path d="M8.04851 1.89774C7.45706 1.23776 6.80045 0.65364 6.00092 0.35318C5.73621 0.445432 5.47179 0.558446 5.21061 0.695255C5.92328 0.829437 6.54003 1.21259 7.07401 1.64688C7.45942 1.96033 7.81811 2.31503 8.14305 2.63636C8.26719 2.75911 8.38647 2.87706 8.50036 2.98591C8.71043 3.18669 8.90231 3.35711 9.08111 3.48353C9.09058 3.49022 9.09996 3.49675 9.10925 3.50313C9.21969 3.5196 9.33465 3.54464 9.45539 3.57847C9.34001 3.45 9.22515 3.31434 9.11013 3.17516C9.0075 3.05098 8.90461 2.92381 8.80036 2.79498C8.56125 2.49945 8.31499 2.19511 8.04851 1.89774Z" fill="#15447A"></path>
<path d="M10.6329 23.6168C10.6394 23.6401 10.6458 23.6634 10.6521 23.6867C10.6521 23.6422 10.6521 23.5965 10.6521 23.5497C10.7466 23.225 10.8649 22.9062 10.9957 22.5926C11.1575 22.2046 11.3397 21.8219 11.5209 21.4456L11.5779 21.3274C11.7403 20.9905 11.9002 20.6589 12.0453 20.3278C12.6727 18.8965 13.0007 17.528 11.9954 16.0568C11.5872 16.1828 11.1035 16.2385 10.6329 16.2426C10.1623 16.2385 9.67864 16.1828 9.27039 16.0568C8.26513 17.528 8.59313 18.8965 9.22051 20.3278C9.36565 20.6589 9.52552 20.9905 9.68792 21.3274L9.74491 21.4456C9.92612 21.8219 10.1083 22.2046 10.2701 22.5926C10.4009 22.9062 10.5192 23.225 10.6137 23.5497C10.6137 23.5965 10.6137 23.6422 10.6137 23.6867C10.62 23.6634 10.6264 23.6401 10.6329 23.6168Z" fill="#15447A"></path>
<path d="M8.8519 15.8953C7.77224 17.5087 8.15717 19.0128 8.7951 20.4682C8.94307 20.8057 9.10592 21.1435 9.26792 21.4795L9.32496 21.5978C9.50617 21.9742 9.68446 22.3489 9.84214 22.727C10.0183 23.1493 10.1672 23.5723 10.2644 24C9.99197 23.5384 9.68792 23.1404 9.35947 22.7886C9.23044 22.537 9.08359 22.2831 8.92894 22.0286C8.77411 21.7738 8.61143 21.5183 8.44795 21.2616C8.10742 20.7268 7.76335 20.1865 7.47921 19.6351C7.06097 18.8235 6.79433 18.0253 6.8776 17.2553C6.95075 16.5791 7.29628 15.904 8.0969 15.2471C8.24704 15.5186 8.5143 15.7344 8.8519 15.8953Z" fill="#15447A"></path>
<path d="M6.42446 17.2184C6.51489 16.3823 6.96917 15.5727 7.96671 14.8163C7.96216 14.7607 7.96169 14.7033 7.96555 14.6444C7.99745 14.6087 8.06958 14.5651 8.23143 14.5204C8.39229 14.4759 8.58642 14.4444 8.81088 14.408C8.88483 14.396 8.96207 14.3835 9.0425 14.3698C9.35407 14.3167 9.69962 14.2475 10.0075 14.1212C10.2431 14.0245 10.464 13.8915 10.6329 13.7017C10.8018 13.8915 11.0227 14.0245 11.2583 14.1212C11.5662 14.2475 11.9118 14.3167 12.2233 14.3698C12.3038 14.3835 12.381 14.396 12.4549 14.408C12.6794 14.4444 12.8735 14.4759 13.0344 14.5204C13.1962 14.5651 13.2684 14.6087 13.3003 14.6444C13.3041 14.7033 13.3037 14.7607 13.2991 14.8163C14.2967 15.5727 14.7509 16.3823 14.8414 17.2184C14.9355 18.0887 14.6323 18.9615 14.2021 19.7962C13.9102 20.3627 13.5497 20.929 13.2042 21.4719C13.1238 21.5981 13.044 21.7235 12.9663 21.8469C13.6095 21.3656 14.3043 20.9694 15.0124 20.5656L15.0982 20.5167C15.8209 18.8951 15.8807 17.6163 15.6157 16.5962C15.3003 15.3818 14.5176 14.5056 13.7704 13.8335C13.7687 13.8352 13.7671 13.8369 13.7655 13.8386C13.762 13.8422 13.7586 13.8458 13.7552 13.8492C13.6945 13.9113 13.6393 13.9613 13.5941 13.9982C13.5692 14.0185 13.544 14.0374 13.5207 14.0532C13.5043 14.064 13.4879 14.0743 13.4697 14.0843C13.4321 14.1056 13.3884 14.1227 13.3441 14.1338C13.3044 14.1438 13.2623 14.1516 13.2175 14.1571C13.2024 14.1526 13.1874 14.1483 13.1726 14.1442C12.9822 14.0915 12.7481 14.0536 12.5162 14.0161C12.4473 14.0049 12.3784 13.9938 12.3111 13.9823C12.0042 13.93 11.7062 13.8681 11.4528 13.7642C11.209 13.6642 11.0191 13.5304 10.903 13.3382L11.0461 12.5177C11.0907 12.2625 11.2413 12.0298 11.4809 11.8769C11.505 11.8615 11.5292 11.8462 11.5534 11.8308L11.5574 11.8282C11.8858 11.6194 12.2274 11.4022 12.5639 11.0619C12.7201 10.904 12.7918 10.6982 12.7918 10.4901C12.5451 10.6955 12.2165 10.8102 11.8746 10.8102H9.39121C9.04934 10.8102 8.72069 10.6955 8.47402 10.4901C8.47402 10.6982 8.54573 10.904 8.70191 11.0619C9.03847 11.4022 9.37999 11.6194 9.70838 11.8282L9.70961 11.8289C9.73479 11.845 9.75989 11.8609 9.7849 11.8769C10.0245 12.0298 10.1751 12.2625 10.2197 12.5177L10.3629 13.3382C10.2467 13.5304 10.0568 13.6642 9.81298 13.7642C9.55963 13.8681 9.26164 13.93 8.95477 13.9823C8.88745 13.9938 8.81873 14.0049 8.74981 14.016C8.51791 14.0536 8.28359 14.0915 8.09326 14.1442C8.07841 14.1483 8.06342 14.1526 8.04834 14.1571C8.00349 14.1516 7.96145 14.1438 7.92173 14.1338C7.87743 14.1227 7.83374 14.1056 7.79612 14.0843C7.77791 14.0743 7.76153 14.064 7.74514 14.0532C7.72178 14.0374 7.69659 14.0185 7.67171 13.9982C7.6265 13.9613 7.57128 13.9113 7.5106 13.8492C7.50567 13.8441 7.50061 13.8389 7.49544 13.8335C6.74819 14.5056 5.96555 15.3818 5.6501 16.5962C5.38513 17.6163 5.44493 18.8951 6.16759 20.5167L6.25338 20.5656C6.96157 20.9694 7.65633 21.3656 8.29954 21.8469C8.22186 21.7235 8.14231 21.5986 8.06197 21.4723C7.71642 20.9295 7.3556 20.3627 7.06371 19.7962C6.63353 18.9615 6.33034 18.0887 6.42446 17.2184Z" fill="#15447A"></path>
<path d="M7.22643 13.507C6.43758 14.2076 5.55638 15.161 5.20611 16.5094C4.94733 17.5056 4.98224 18.7013 5.53298 20.1504C4.97386 19.8213 4.4144 19.4683 3.87296 19.0465C3.71754 18.6249 3.64281 18.2317 3.6301 17.8641C3.59808 16.9372 3.95932 16.1473 4.46884 15.4466C4.87109 14.8934 5.35642 14.407 5.81057 13.9517C5.93661 13.8254 6.06047 13.7012 6.17927 13.5789C6.42983 13.321 6.66005 13.0688 6.83477 12.8215L6.86158 12.822C6.93076 13.0063 7.01723 13.1819 7.11918 13.3461C7.15499 13.4036 7.19101 13.4573 7.22643 13.507Z" fill="#15447A"></path>
<path d="M6.48059 12.5661C6.58817 12.4065 6.65974 12.2611 6.6945 12.1262C6.67335 11.9329 6.67246 11.7378 6.69197 11.5421C6.70835 11.3786 6.73354 11.1683 6.74871 11.0448L6.61611 10.9448C6.5844 10.9208 6.55328 10.8968 6.52274 10.8726C6.51136 10.9078 6.49849 10.9438 6.48416 10.9806C6.40066 11.1951 6.26249 11.4499 6.05613 11.7399C5.64334 12.32 4.95142 13.0502 3.8547 13.8937C2.54064 14.9044 2.10622 15.911 2.12187 16.8034C2.12522 16.9945 2.14924 17.1818 2.19051 17.3644C2.52826 17.8037 2.88483 18.1849 3.25384 18.5236C3.20795 18.3018 3.18243 18.086 3.17517 17.8759C3.1396 16.8464 3.54337 15.9792 4.08392 15.2359C4.50646 14.6548 5.02181 14.1385 5.47858 13.6809C5.60135 13.5579 5.71989 13.4391 5.83125 13.3245C6.09802 13.0499 6.32269 12.8004 6.48059 12.5661Z" fill="#15447A"></path>
<path d="M6.13199 10.5312C6.12972 10.6025 6.10925 10.7096 6.05295 10.8543C5.98268 11.0348 5.86104 11.2623 5.66885 11.5324C5.28456 12.0725 4.62448 12.7739 3.55278 13.5982C2.24073 14.6074 1.7134 15.6496 1.66937 16.6225C1.50706 16.371 1.35819 16.1188 1.22208 15.8664C1.22885 15.701 1.24768 15.5365 1.27909 15.3732C1.46813 14.3905 2.11791 13.4256 3.40203 12.5503C4.50063 11.8014 5.12613 11.0999 5.48854 10.5665C5.60271 10.3985 5.69031 10.2479 5.75813 10.1186C5.86751 10.2569 5.9916 10.3947 6.13199 10.5312Z" fill="#15447A"></path>
<path d="M5.43516 9.76882C5.44716 9.74253 5.458 9.71802 5.46788 9.69519C5.33764 9.47282 5.24112 9.25144 5.17167 9.03469C5.02639 9.21876 4.83242 9.43745 4.58444 9.6841C4.0785 10.1873 3.33892 10.8157 2.3059 11.5165C1.1271 12.3162 0.651561 13.3546 0.510504 14.2311C0.612148 14.5336 0.73026 14.8378 0.866048 15.1425C1.11568 14.1065 1.8324 13.1181 3.12084 12.2398C4.17529 11.521 4.76306 10.8567 5.09648 10.366C5.26333 10.1205 5.36763 9.91673 5.43516 9.76882Z" fill="#15447A"></path>
<path d="M5.04323 8.44796C4.95954 8.61202 4.71564 8.95289 4.24146 9.42453C3.75612 9.90726 3.03793 10.5186 2.02555 11.2054C1.07659 11.8492 0.543725 12.633 0.266962 13.3851C0.101194 12.6961 0.0171512 12.0209 0 11.3724C0.250667 10.8157 0.968904 9.95765 2.53151 9.3215C3.55502 8.90481 4.30466 8.41124 4.81798 7.97113C4.89326 7.90659 4.9638 7.84289 5.02962 7.78047C5.01336 7.98885 5.01457 8.21317 5.04323 8.44796Z" fill="#15447A"></path>
<path d="M4.49807 7.69026C4.8701 7.37129 5.09347 7.09568 5.20021 6.92195C5.24622 6.77746 5.30072 6.63355 5.36347 6.49067C5.24214 6.44507 5.12631 6.39623 5.01586 6.34445C4.60563 6.64706 3.85936 7.11273 2.32681 7.64173C1.47974 7.93411 0.960278 8.3747 0.660343 8.86071C0.393172 9.29363 0.293996 9.77296 0.306262 10.2334C0.763607 9.78902 1.4237 9.33631 2.33817 8.96402C3.31386 8.56681 4.02088 8.09939 4.49807 7.69026Z" fill="#15447A"></path>
<path d="M5.54191 6.1274C4.1583 5.60641 3.60877 4.63334 3.59469 3.76657C3.57975 2.84587 4.15038 2.11601 4.90179 2.03319C5.23789 1.99615 5.57801 2.14367 6.07436 2.50925C6.30922 2.68224 6.56591 2.89359 6.86396 3.13899L6.89801 3.16703C7.14327 3.36895 7.41417 3.59141 7.71738 3.82825C7.58616 3.91367 7.45845 4.01004 7.33028 4.1115C7.02097 3.66852 6.19263 3.21655 4.56726 3.30994C4.44186 3.31715 4.34693 3.41119 4.35523 3.52C4.36354 3.6288 4.47193 3.71116 4.59734 3.70395C6.28324 3.60709 6.84914 4.12699 6.98485 4.39332C6.96413 4.41099 6.94356 4.42873 6.92313 4.44653C6.55386 4.35139 6.10454 4.33199 5.68105 4.35294C5.20568 4.37645 4.73422 4.45235 4.3976 4.5497C4.27837 4.58418 4.21393 4.696 4.25368 4.79944C4.29342 4.90289 4.4223 4.9588 4.54153 4.92431C4.83324 4.83995 5.26399 4.76908 5.70696 4.74717C6.01275 4.73204 6.31105 4.74095 6.56612 4.77951C6.14205 5.20323 5.7974 5.6577 5.54191 6.1274Z" fill="#15447A"></path>
<path d="M7.21129 2.88059C7.48351 3.1047 7.78305 3.35058 8.12167 3.61231C8.16992 3.59263 8.21903 3.57489 8.26915 3.55931C8.32832 3.54094 8.38791 3.52569 8.44822 3.51359C8.35395 3.43091 8.25896 3.34268 8.16337 3.25132C8.03916 3.13262 7.91369 3.00853 7.78584 2.8821C7.46586 2.56565 7.13081 2.23431 6.76274 1.93497C6.53377 1.74874 6.2876 1.57338 6.02661 1.42804C5.19245 0.96348 4.16573 1.27297 3.49078 1.94831C2.19335 3.2465 2.6179 4.28484 3.03138 5.21756C2.53384 5.28094 2.00592 5.63932 1.53232 6.21991C1.86072 6.04741 2.20182 5.91947 2.53313 5.82646C3.04605 5.68247 3.54267 5.62007 3.94533 5.60674C3.39777 5.0544 3.14971 4.39457 3.13961 3.77213C3.12305 2.75245 3.76302 1.76066 4.84444 1.64147C5.39081 1.58125 5.86666 1.83829 6.36891 2.20823C6.61366 2.38849 6.87873 2.60676 7.17229 2.84847L7.21129 2.88059Z" fill="#15447A"></path>
<path d="M2.67325 6.20216C2.34233 6.29506 2.00578 6.42459 1.68774 6.60102C1.12083 6.91551 0.735687 7.47007 0.514997 8.07965C0.465447 8.21651 0.418784 8.35713 0.375307 8.50124C0.739742 8.00071 1.31385 7.5664 2.15856 7.27482C3.50581 6.80979 4.21021 6.40184 4.60713 6.12531C4.54768 6.08898 4.4902 6.05168 4.43466 6.01349C4.07074 5.9762 3.39369 5.9999 2.67325 6.20216Z" fill="#15447A"></path>
<path d="M9.78184 7.32671L9.03802 9.5549C9.24339 8.92891 9.17584 8.32456 9.08739 7.906C8.7825 7.93275 8.55999 7.91268 8.35032 7.86624C7.79712 7.74371 7.4282 7.31869 7.07691 6.91398C7.02467 6.85379 6.9728 6.79404 6.92079 6.73582C6.84938 6.65588 6.76566 6.57288 6.666 6.48188C6.82654 6.50707 6.98355 6.53011 7.13758 6.55271C8.09007 6.69247 8.92848 6.81549 9.78184 7.32671Z" fill="#15447A"></path>
<path d="M9.03802 9.5549C9.03526 9.56332 9.03244 9.57175 9.02958 9.58019L9.03802 9.5549Z" fill="#15447A"></path>
<path d="M11.9098 2.2307C12.4083 1.50759 13.1058 0.716893 14.0424 0.0624878C14.2467 0.090277 14.4538 0.1281 14.662 0.177455C13.9478 0.54199 13.3625 1.09239 12.858 1.65537C12.5845 1.96062 12.3275 2.27817 12.0856 2.57711C11.983 2.70387 11.8831 2.82737 11.7858 2.94507C11.5389 3.24385 11.3089 3.50558 11.0849 3.71063C11.2435 3.33892 11.5106 2.80979 11.9098 2.2307Z" fill="#15447A"></path>
<path d="M13.2173 1.89774C13.8088 1.23776 14.4654 0.65364 15.2649 0.35318C15.5296 0.445432 15.794 0.558446 16.0552 0.695255C15.3425 0.829437 14.7258 1.21259 14.1918 1.64688C13.8064 1.96033 13.4477 2.31503 13.1228 2.63636C12.9986 2.75911 12.8794 2.87706 12.7655 2.98591C12.5554 3.18669 12.3635 3.35711 12.1847 3.48353C12.1752 3.49022 12.1659 3.49675 12.1566 3.50313C12.0461 3.5196 11.9312 3.54464 11.8104 3.57847C11.9258 3.45 12.0407 3.31434 12.1557 3.17516C12.2583 3.05098 12.3612 2.92383 12.4654 2.795C12.7046 2.49947 12.9508 2.19511 13.2173 1.89774Z" fill="#15447A"></path>
<path d="M12.4139 15.8953C13.4936 17.5087 13.1087 19.0128 12.4707 20.4682C12.3228 20.8057 12.1599 21.1435 11.9979 21.4795L11.9409 21.5978C11.7597 21.9742 11.5814 22.3489 11.4237 22.727C11.2476 23.1493 11.0986 23.5723 11.0015 24C11.2738 23.5384 11.5779 23.1404 11.9064 22.7886C12.0354 22.537 12.1822 22.2831 12.3369 22.0286C12.4917 21.7738 12.6544 21.5183 12.8179 21.2616C13.1584 20.7268 13.5025 20.1865 13.7866 19.6351C14.2049 18.8235 14.4715 18.0253 14.3882 17.2553C14.3151 16.5791 13.9695 15.904 13.1689 15.2471C13.0188 15.5186 12.7515 15.7344 12.4139 15.8953Z" fill="#15447A"></path>
<path d="M14.0394 13.507C14.8282 14.2076 15.7094 15.161 16.0597 16.5094C16.3185 17.5056 16.2836 18.7013 15.7328 20.1504C16.292 19.8213 16.8514 19.4683 17.3929 19.0465C17.5483 18.6249 17.623 18.2317 17.6357 17.8641C17.6677 16.9372 17.3065 16.1473 16.797 15.4466C16.3947 14.8934 15.9094 14.407 15.4553 13.9517C15.3292 13.8254 15.2053 13.7012 15.0866 13.5789C14.836 13.321 14.6058 13.0688 14.4311 12.8215L14.4042 12.822C14.3351 13.0063 14.2486 13.1819 14.1466 13.3461C14.1108 13.4036 14.0748 13.4573 14.0394 13.507Z" fill="#15447A"></path>
<path d="M14.7852 12.5661C14.6777 12.4065 14.6061 12.2611 14.5713 12.1262C14.5925 11.9329 14.5934 11.7378 14.5739 11.5421C14.5575 11.3786 14.5323 11.1683 14.5171 11.0448L14.6497 10.9448C14.6814 10.9208 14.7125 10.8968 14.7431 10.8726C14.7545 10.9078 14.7673 10.9438 14.7817 10.9806C14.8652 11.1951 15.0033 11.4499 15.2097 11.7399C15.6225 12.32 16.3144 13.0502 17.4111 13.8937C18.7252 14.9044 19.1596 15.911 19.144 16.8034C19.1406 16.9945 19.1166 17.1818 19.0753 17.3644C18.7376 17.8037 18.381 18.1849 18.012 18.5236C18.0579 18.3018 18.0834 18.086 18.0907 17.8759C18.1262 16.8464 17.7225 15.9792 17.1819 15.2359C16.7594 14.6548 16.244 14.1385 15.7872 13.6809C15.6645 13.5579 15.5459 13.4391 15.4346 13.3245C15.1678 13.0499 14.9431 12.8004 14.7852 12.5661Z" fill="#15447A"></path>
<path d="M15.1338 10.5312C15.1361 10.6025 15.1566 10.7096 15.2129 10.8543C15.2831 11.0348 15.4048 11.2623 15.597 11.5324C15.9813 12.0725 16.6413 12.7739 17.713 13.5982C19.0251 14.6074 19.5524 15.6496 19.5965 16.6225C19.7588 16.371 19.9076 16.1188 20.0437 15.8664C20.037 15.701 20.0181 15.5365 19.9867 15.3732C19.7977 14.3905 19.1479 13.4256 17.8638 12.5503C16.7652 11.8014 16.1397 11.0999 15.7773 10.5665C15.6631 10.3985 15.5755 10.2479 15.5077 10.1186C15.3983 10.2569 15.2742 10.3947 15.1338 10.5312Z" fill="#15447A"></path>
<path d="M15.8307 9.76882C15.8187 9.74253 15.8078 9.71802 15.7979 9.69519C15.9282 9.47282 16.0247 9.25144 16.0942 9.03469C16.2394 9.21876 16.4334 9.43745 16.6814 9.6841C17.1873 10.1873 17.9269 10.8157 18.9599 11.5165C20.1387 12.3162 20.6143 13.3546 20.7553 14.2311C20.6537 14.5336 20.5356 14.8378 20.3998 15.1425C20.1501 14.1065 19.4334 13.1181 18.145 12.2398C17.0905 11.521 16.5028 10.8567 16.1693 10.366C16.0025 10.1205 15.8982 9.91673 15.8307 9.76882Z" fill="#15447A"></path>
<path d="M16.2226 8.44796C16.3063 8.61202 16.5502 8.95289 17.0244 9.42453C17.5097 9.90726 18.2279 10.5186 19.2403 11.2054C20.1892 11.8492 20.7221 12.633 20.9989 13.3851C21.1646 12.6961 21.2487 12.0209 21.2658 11.3724C21.0152 10.8157 20.2969 9.95765 18.7343 9.3215C17.7108 8.90481 16.9612 8.41124 16.4478 7.97113C16.3726 7.90659 16.302 7.84289 16.2362 7.78047C16.2525 7.98885 16.2513 8.21317 16.2226 8.44796Z" fill="#15447A"></path>
<path d="M16.7678 7.69026C16.3957 7.37129 16.1724 7.09568 16.0656 6.92195C16.0196 6.77746 15.9651 6.63355 15.9024 6.49067C16.0237 6.44507 16.1395 6.39623 16.25 6.34445C16.6602 6.64706 17.4065 7.11273 18.939 7.64173C19.7861 7.93411 20.3055 8.3747 20.6055 8.86071C20.8727 9.29363 20.9718 9.77296 20.9596 10.2334C20.5022 9.78902 19.8421 9.33631 18.9277 8.96402C17.952 8.56681 17.2449 8.09939 16.7678 7.69026Z" fill="#15447A"></path>
<path d="M15.7239 6.1274C17.1075 5.60641 17.6571 4.63334 17.6711 3.76657C17.6861 2.84587 17.1154 2.11601 16.364 2.03319C16.0279 1.99615 15.6878 2.14367 15.1915 2.50925C14.9566 2.68224 14.6999 2.89359 14.4019 3.13899L14.3678 3.16703C14.1226 3.36895 13.8517 3.59141 13.5484 3.82825C13.6797 3.91367 13.8074 4.01004 13.9355 4.1115C14.2449 3.66852 15.0732 3.21655 16.6986 3.30994C16.824 3.31715 16.9189 3.41119 16.9106 3.52C16.9023 3.6288 16.7939 3.71116 16.6685 3.70395C14.9826 3.60709 14.4167 4.12699 14.281 4.39332C14.3017 4.41099 14.3223 4.42873 14.3427 4.44653C14.712 4.35139 15.1613 4.33199 15.5848 4.35294C16.0601 4.37645 16.5316 4.45235 16.8682 4.5497C16.9875 4.58418 17.0519 4.696 17.0121 4.79944C16.9724 4.90289 16.8435 4.9588 16.7243 4.92431C16.4326 4.83995 16.0018 4.76908 15.5589 4.74717C15.2531 4.73204 14.9548 4.74095 14.6997 4.77951C15.1238 5.20323 15.4684 5.6577 15.7239 6.1274Z" fill="#15447A"></path>
<path d="M14.0545 2.88059C13.7823 3.1047 13.4828 3.35058 13.1441 3.61231C13.0959 3.59263 13.0468 3.57489 12.9967 3.55931C12.9375 3.54094 12.8779 3.52569 12.8176 3.51359C12.9119 3.43091 13.0069 3.34268 13.1025 3.25132C13.2267 3.13262 13.3521 3.00853 13.48 2.8821C13.8 2.56565 14.135 2.23431 14.5031 1.93497C14.7321 1.74874 14.9782 1.57338 15.2392 1.42804C16.0734 0.96348 17.1001 1.27297 17.775 1.94831C19.0725 3.2465 18.6479 4.28484 18.2344 5.21756C18.732 5.28094 19.2599 5.63932 19.7335 6.21991C19.4051 6.04741 19.064 5.91947 18.7327 5.82646C18.2198 5.68247 17.7232 5.62007 17.3205 5.60674C17.868 5.0544 18.1161 4.39457 18.1262 3.77213C18.1428 2.75245 17.5028 1.76066 16.4214 1.64147C15.875 1.58125 15.3992 1.83829 14.8969 2.20823C14.6522 2.38849 14.3871 2.60676 14.0935 2.84847L14.0545 2.88059Z" fill="#15447A"></path>
<path d="M18.5926 6.20216C18.9235 6.29506 19.26 6.42459 19.5781 6.60101C20.145 6.9155 20.5301 7.47008 20.7508 8.07967C20.8004 8.21653 20.847 8.35714 20.8905 8.50124C20.5261 8.00071 19.952 7.5664 19.1073 7.27482C17.76 6.80979 17.0556 6.40184 16.6587 6.12531C16.7181 6.08898 16.7756 6.05168 16.8312 6.01349C17.1951 5.9762 17.8721 5.9999 18.5926 6.20216Z" fill="#15447A"></path>
<path d="M11.484 7.32671L12.2278 9.5549C12.2306 9.56332 12.2334 9.57175 12.2362 9.58019L12.2278 9.5549C12.0224 8.92891 12.09 8.32456 12.1784 7.906C12.4833 7.93275 12.7058 7.91268 12.9155 7.86624C13.4687 7.74371 13.8376 7.31869 14.1889 6.91397C14.2412 6.85379 14.293 6.79404 14.345 6.73582C14.4164 6.65588 14.5002 6.57288 14.5998 6.48188C14.4393 6.50707 14.2823 6.53011 14.1282 6.55271C13.1758 6.69247 12.3373 6.81549 11.484 7.32671Z" fill="#15447A"></path>
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.1432 13.8528C31.9112 13.2771 31.7952 12.6455 31.7952 11.958C31.7952 11.2705 31.9112 10.6433 32.1432 10.0761C32.3752 9.50035 32.7018 9.00631 33.1228 8.59378C33.5525 8.17272 34.0552 7.85053 34.6309 7.62706C35.2153 7.39505 35.8597 7.27905 36.5643 7.27905C37.269 7.27905 37.9092 7.39505 38.485 7.62706C39.0693 7.85053 39.572 8.17272 39.993 8.59378C40.4227 9.00631 40.7535 9.50035 40.9855 10.0761C41.2176 10.6433 41.3336 11.2705 41.3336 11.958C41.3336 12.6455 41.2176 13.2771 40.9855 13.8528C40.7535 14.4199 40.4227 14.914 39.993 15.3351C39.572 15.7476 39.0693 16.0698 38.485 16.3018C37.9092 16.5253 37.269 16.637 36.5643 16.637C35.8597 16.637 35.2153 16.5253 34.6309 16.3018C34.0552 16.0698 33.5525 15.7476 33.1228 15.3351C32.7018 14.914 32.3752 14.4199 32.1432 13.8528ZM39.5548 11.958C39.5548 11.3565 39.4259 10.8323 39.1681 10.3855C38.9189 9.93001 38.5708 9.57341 38.124 9.31559C37.6772 9.05786 37.1573 8.92895 36.5643 8.92895C35.98 8.92895 35.4602 9.05786 35.0047 9.31559C34.5578 9.57341 34.2055 9.93001 33.9478 10.3855C33.6986 10.8323 33.574 11.3565 33.574 11.958C33.574 12.5509 33.6986 13.0751 33.9478 13.5306C34.2055 13.986 34.5578 14.3426 35.0047 14.6004C35.4602 14.8582 35.98 14.9871 36.5643 14.9871C37.1573 14.9871 37.6772 14.8582 38.124 14.6004C38.5708 14.3426 38.9189 13.986 39.1681 13.5306C39.4259 13.0751 39.5548 12.5509 39.5548 11.958Z" fill="#15447A"></path>
<path d="M49.7141 9.71522H49.8172V7.4466C49.6884 7.49815 49.5423 7.52396 49.379 7.52396C49.2329 7.52396 48.9966 7.48532 48.67 7.40796C48.3521 7.322 47.9654 7.27905 47.51 7.27905C46.496 7.27905 45.6066 7.47671 44.8418 7.87196C44.0856 8.25868 43.497 8.80434 43.076 9.50895C42.6549 10.205 42.4443 11.0213 42.4443 11.958C42.4443 12.6369 42.5603 13.2641 42.7923 13.8399C43.0243 14.407 43.3552 14.9011 43.7849 15.3222C44.2145 15.7433 44.7172 16.0698 45.293 16.3018C45.8773 16.5253 46.5175 16.637 47.2135 16.637C48.1244 16.637 48.9106 16.4479 49.5723 16.0698C50.234 15.6917 50.741 15.1676 51.0933 14.4973C51.4542 13.827 51.6347 13.0537 51.6347 12.1771C51.6347 12.1103 51.6379 12.0467 51.6444 11.9864C51.6546 11.8916 51.6729 11.8048 51.6991 11.726C51.7507 11.5971 51.8194 11.4897 51.9054 11.4038V11.3007H47.7678C47.7299 11.3007 47.6918 11.3003 47.6535 11.2996C47.6182 11.299 47.5828 11.2981 47.5472 11.2968C47.4916 11.2949 47.4357 11.2923 47.3795 11.2889C47.3161 11.2852 47.2521 11.2805 47.1878 11.2748C46.9901 11.2576 46.8139 11.2319 46.6593 11.1975V13.2986H46.7624C46.8655 13.1524 47.0373 13.0364 47.278 12.9505C47.5186 12.856 47.8322 12.8087 48.2189 12.8087H49.8036C49.7929 13.2127 49.6814 13.5779 49.4692 13.9044C49.2458 14.2395 48.9365 14.5059 48.5411 14.7035C48.1545 14.8925 47.7119 14.9871 47.2135 14.9871C46.6292 14.9871 46.1093 14.8582 45.6539 14.6004C45.207 14.3426 44.8547 13.986 44.597 13.5306C44.3477 13.0751 44.2231 12.5509 44.2231 11.958C44.2231 11.3479 44.3606 10.8151 44.6356 10.3597C44.9105 9.90428 45.2973 9.55197 45.7957 9.30276C46.3026 9.05355 46.8913 8.92895 47.5616 8.92895C47.8022 8.92895 48.0256 8.94616 48.2318 8.9805C48.4466 9.00631 48.6443 9.05355 48.8248 9.12231C49.0138 9.18246 49.1857 9.27265 49.3403 9.39295C49.5036 9.51325 49.6282 9.62065 49.7141 9.71522Z" fill="#15447A"></path>
<path fill-rule="evenodd" clip-rule="evenodd" d="M53.0135 13.8528C52.7815 13.2771 52.6655 12.6455 52.6655 11.958C52.6655 11.2705 52.7815 10.6433 53.0135 10.0761C53.2455 9.50035 53.572 9.00631 53.9931 8.59378C54.4228 8.17272 54.9255 7.85053 55.5012 7.62706C56.0856 7.39505 56.73 7.27905 57.4346 7.27905C58.1393 7.27905 58.7795 7.39505 59.3552 7.62706C59.9395 7.85053 60.4423 8.17272 60.8633 8.59378C61.293 9.00631 61.6238 9.50035 61.8558 10.0761C62.0879 10.6433 62.2039 11.2705 62.2039 11.958C62.2039 12.6455 62.0879 13.2771 61.8558 13.8528C61.6238 14.4199 61.293 14.914 60.8633 15.3351C60.4423 15.7476 59.9395 16.0698 59.3552 16.3018C58.7795 16.5253 58.1393 16.637 57.4346 16.637C56.73 16.637 56.0856 16.5253 55.5012 16.3018C54.9255 16.0698 54.4228 15.7476 53.9931 15.3351C53.572 14.914 53.2455 14.4199 53.0135 13.8528ZM60.4251 11.958C60.4251 11.3565 60.2961 10.8323 60.0383 10.3855C59.7892 9.93001 59.4411 9.57341 58.9943 9.31559C58.5475 9.05786 58.0276 8.92895 57.4346 8.92895C56.8503 8.92895 56.3305 9.05786 55.875 9.31559C55.4281 9.57341 55.0758 9.93001 54.8181 10.3855C54.5689 10.8323 54.4443 11.3565 54.4443 11.958C54.4443 12.5509 54.5689 13.0751 54.8181 13.5306C55.0758 13.986 55.4281 14.3426 55.875 14.6004C56.3305 14.8582 56.8503 14.9871 57.4346 14.9871C58.0276 14.9871 58.5475 14.8582 58.9943 14.6004C59.4411 14.3426 59.7892 13.986 60.0383 13.5306C60.2961 13.0751 60.4251 12.5509 60.4251 11.958Z" fill="#15447A"></path>
<path d="M76.8346 8.72268C76.5167 9.16955 76.203 9.77537 75.8937 10.5401L74.9011 12.9763C74.7035 13.4661 74.5188 13.8399 74.3469 14.0977C74.1836 14.3555 74.0204 14.5317 73.857 14.6262C73.7024 14.7207 73.5305 14.768 73.3415 14.768C73.1954 14.768 73.0579 14.7422 72.929 14.6906C72.8001 14.6305 72.6884 14.5488 72.5939 14.4457H72.4908V16.3792C72.5035 16.3827 72.5164 16.3861 72.5298 16.3893C72.5506 16.3945 72.5722 16.3995 72.5946 16.4043C72.6203 16.4098 72.647 16.415 72.6746 16.42C72.7221 16.4287 72.7725 16.4365 72.8259 16.4436C72.9634 16.4608 73.1138 16.4694 73.277 16.4694C73.6809 16.4694 74.0418 16.4135 74.3598 16.3018C74.6863 16.1901 74.9785 16.014 75.2363 15.7734C75.494 15.5242 75.7389 15.2062 75.971 14.8195C76.203 14.4328 76.4307 13.9645 76.6541 13.4146L77.6466 10.9784C77.8443 10.4886 78.0291 10.1148 78.2009 9.85703C78.3814 9.59061 78.5489 9.41016 78.7036 9.31567C78.7142 9.30862 78.7247 9.30172 78.7352 9.29512L78.7583 9.28118L78.7751 9.27139L78.7871 9.26471L78.8044 9.25537C78.8129 9.25092 78.8213 9.24662 78.8298 9.24246C78.8477 9.23364 78.8654 9.22563 78.8831 9.21828C78.9752 9.18009 79.0657 9.16095 79.1547 9.16095C79.2922 9.16095 79.4168 9.19106 79.5285 9.25121C79.6402 9.30276 79.7262 9.36291 79.7864 9.43167H79.8895V7.40796C79.8379 7.42517 79.7864 7.438 79.7348 7.4466C79.7154 7.44986 79.6959 7.45253 79.6765 7.45461C79.6649 7.4558 79.6534 7.45676 79.6418 7.4575C79.6332 7.4581 79.6246 7.45854 79.616 7.45884C79.604 7.45928 79.592 7.45951 79.5801 7.45951H79.4125C79.3856 7.45498 79.3494 7.45172 79.3037 7.44957C79.2618 7.44757 79.2122 7.4466 79.1547 7.4466C78.6821 7.4466 78.2568 7.54547 77.8787 7.74306C77.5091 7.94072 77.1611 8.26728 76.8346 8.72268Z" fill="#15447A"></path>
<path d="M80.7295 9.03212C80.7295 8.6454 80.6952 8.34034 80.6264 8.11694C80.5576 7.88494 80.4589 7.71739 80.3299 7.61422V7.51113H82.8435V7.61422C82.7146 7.71739 82.6157 7.88494 82.5469 8.11694C82.4783 8.34034 82.4438 8.6454 82.4438 9.03212V13.1697C82.4438 13.7626 82.5728 14.2137 82.8306 14.5231C83.0883 14.8325 83.4664 14.9872 83.9648 14.9872C84.4633 14.9872 84.8414 14.8325 85.0991 14.5231C85.3569 14.2137 85.4858 13.7626 85.4858 13.1697V9.03212C85.4858 8.6454 85.4515 8.34034 85.3827 8.11694C85.314 7.88494 85.2151 7.71739 85.0863 7.61422V7.51113H87.5997V7.61422C87.4708 7.71739 87.372 7.88494 87.3033 8.11694C87.2345 8.34034 87.2002 8.6454 87.2002 9.03212V13.247C87.2002 13.9774 87.0756 14.5962 86.8264 15.1032C86.5858 15.6101 86.2248 15.9925 85.7436 16.2503C85.2624 16.5082 84.6694 16.6371 83.9648 16.6371C83.2602 16.6371 82.6672 16.5082 82.186 16.2503C81.7048 15.9925 81.3396 15.6101 81.0904 15.1032C80.8498 14.5962 80.7295 13.9774 80.7295 13.247V9.03212Z" fill="#15447A"></path>
<path d="M88.9259 9.13521C88.9259 8.74857 88.8872 8.4263 88.8099 8.16849C88.7325 7.91068 88.6208 7.72592 88.4747 7.61422V7.51113H90.7691V7.58849C90.6401 7.75173 90.5886 7.94079 90.6144 8.15559C90.6401 8.37045 90.7561 8.57242 90.9624 8.7614L93.2596 10.8238L95.3965 8.83876C95.6285 8.62396 95.7617 8.40479 95.796 8.1814C95.839 7.94939 95.8003 7.75173 95.68 7.58849V7.51113H97.9744V7.61422C97.8369 7.72592 97.7252 7.91068 97.6393 8.16849C97.5619 8.4263 97.5233 8.74857 97.5233 9.13521V14.884C97.5233 15.2707 97.5576 15.5801 97.6264 15.8121C97.6951 16.0355 97.7939 16.1988 97.9228 16.3019V16.405H95.4094V16.3019C95.5382 16.1988 95.6371 16.0355 95.7058 15.8121C95.7746 15.5801 95.8089 15.2707 95.8089 14.884V10.5628L93.2568 12.8733H93.2052L90.6401 10.5889V14.884C90.6401 15.2707 90.6746 15.5801 90.7433 15.8121C90.812 16.0355 90.9109 16.1988 91.0398 16.3019V16.405H88.5262V16.3019C88.6552 16.1988 88.7539 16.0355 88.8227 15.8121C88.8915 15.5801 88.9259 15.2707 88.9259 14.884V9.13521Z" fill="#15447A"></path>
<path fill-rule="evenodd" clip-rule="evenodd" d="M68.9013 14.884C68.9013 15.2793 68.9401 15.593 69.0174 15.825C69.1033 16.0484 69.2193 16.2074 69.3654 16.3019V16.405H66.7874V16.3019C66.9164 16.1988 67.0152 16.0355 67.0839 15.8121C67.1527 15.5801 67.1871 15.2707 67.1871 14.884V9.03212C67.1871 8.6454 67.1484 8.34034 67.0711 8.11694C66.9937 7.88494 66.8863 7.71739 66.7488 7.61422V7.51113H69.9583C71.0583 7.51113 71.9176 7.79468 72.5363 8.36185C73.1636 8.92902 73.4772 9.71099 73.4772 10.7078C73.4772 11.3694 73.3354 11.9409 73.0519 12.4221C72.7683 12.8947 72.3644 13.2599 71.8402 13.5177C71.3161 13.7755 70.6888 13.9044 69.9583 13.9044H68.9013V14.884ZM68.9013 9.08367V12.3319H69.881C70.474 12.3319 70.9251 12.1944 71.2344 11.9194C71.5438 11.6358 71.6984 11.232 71.6984 10.7078C71.6984 10.175 71.5438 9.77114 71.2344 9.49612C70.9251 9.22118 70.474 9.08367 69.881 9.08367H68.9013Z" fill="#15447A"></path>
<path d="M63.5594 8.11694C63.6282 8.34034 63.6625 8.6454 63.6625 9.03212V14.884C63.6625 15.0479 63.6564 15.198 63.644 15.3341C63.6272 15.5191 63.5991 15.6784 63.5594 15.8121C63.5391 15.8783 63.5161 15.9393 63.4904 15.9949C63.4664 16.0471 63.44 16.0945 63.4114 16.1373C63.3674 16.2031 63.318 16.258 63.263 16.3019V16.405H65.7765V16.3019C65.6476 16.1988 65.5487 16.0355 65.48 15.8121C65.4113 15.5801 65.3769 15.2707 65.3769 14.884V9.03212C65.3769 8.6454 65.4113 8.34034 65.48 8.11694C65.5487 7.88494 65.6476 7.71739 65.7765 7.61422V7.51113H63.263V7.61422C63.3919 7.71739 63.4907 7.88494 63.5594 8.11694Z" fill="#15447A"></path>
<path d="M27.2239 14.8067H29.8404C30.2272 14.8067 30.5365 14.7723 30.7685 14.7035C31.0005 14.6263 31.1681 14.5188 31.2712 14.3814H31.3743V16.5082C31.2197 16.4651 31.0436 16.4351 30.8459 16.4179C30.6568 16.4093 30.4635 16.405 30.2658 16.405H25.0713V16.3019C25.2088 16.1988 25.3162 16.0355 25.3936 15.8121C25.4708 15.5801 25.5096 15.2707 25.5096 14.884V9.03212C25.5096 8.6454 25.4751 8.34034 25.4064 8.11694C25.3377 7.88494 25.2388 7.71739 25.1099 7.61422V7.51113H27.6879V7.61422C27.6239 7.65561 27.5657 7.70938 27.5132 7.77554C27.446 7.86039 27.3882 7.96556 27.3399 8.09113C27.2625 8.3146 27.2239 8.62827 27.2239 9.03212V14.8067Z" fill="#15447A"></path>
<path fill-rule="evenodd" clip-rule="evenodd" d="M92.6524 20.7501C92.6524 21.1821 92.7273 21.5141 92.8771 21.7463C93.0269 21.9785 93.2404 22.0946 93.5176 22.0946C93.7997 22.0946 94.0144 21.9785 94.1617 21.7463C94.3115 21.5141 94.3865 21.1821 94.3865 20.7501C94.3865 20.3182 94.3115 19.9861 94.1617 19.7539C94.0144 19.5217 93.7997 19.4056 93.5176 19.4056C93.2404 19.4056 93.0269 19.5217 92.8771 19.7539C92.7273 19.9861 92.6524 20.3182 92.6524 20.7501ZM94.0606 20.7501C94.0606 21.0922 94.0144 21.3518 93.9221 21.5291C93.8322 21.7064 93.6973 21.795 93.5176 21.795C93.3403 21.795 93.2055 21.7064 93.1131 21.5291C93.0207 21.3518 92.9745 21.0922 92.9745 20.7501C92.9745 20.4081 93.0207 20.1484 93.1131 19.9711C93.2055 19.7938 93.3403 19.7052 93.5176 19.7052C93.6973 19.7052 93.8322 19.7938 93.9221 19.9711C94.0144 20.1484 94.0606 20.4081 94.0606 20.7501Z" fill="#15447A"></path>
<path d="M91.907 21.7463H91.1985L91.6373 21.1509C91.7996 20.9311 91.9157 20.7414 91.9856 20.5816C92.058 20.4218 92.0942 20.272 92.0942 20.1322C92.0942 19.9124 92.0193 19.7389 91.8695 19.6116C91.7197 19.4817 91.5162 19.4168 91.2591 19.4168C91.1667 19.4168 91.0893 19.4231 91.0269 19.4355C90.9644 19.448 90.9133 19.4605 90.8733 19.473C90.8359 19.483 90.8034 19.488 90.7759 19.488H90.7347C90.7222 19.4855 90.7035 19.483 90.6786 19.4805V19.9486H90.7048C90.7222 19.9212 90.7422 19.8962 90.7647 19.8737C90.7897 19.8513 90.8184 19.83 90.8508 19.8101C90.8908 19.7851 90.9432 19.7626 91.0081 19.7427C91.073 19.7227 91.143 19.7127 91.2179 19.7127C91.3851 19.7127 91.5175 19.7539 91.6149 19.8363C91.7122 19.9187 91.7609 20.031 91.7609 20.1734C91.7609 20.2483 91.7434 20.3331 91.7085 20.428C91.6735 20.5204 91.6173 20.629 91.5399 20.7539C91.465 20.8787 91.3664 21.0248 91.2441 21.1921L90.6411 22.016V22.0422H92.0156C92.063 22.0422 92.1092 22.0435 92.1542 22.046C92.1991 22.051 92.2391 22.0584 92.274 22.0684V21.5965H92.244C92.2191 21.644 92.1791 21.6814 92.1242 21.7089C92.0718 21.7339 91.9994 21.7463 91.907 21.7463Z" fill="#15447A"></path>
<path d="M82.3448 21.7389H81.416V20.7314H81.8954C81.9778 20.7314 82.039 20.7401 82.0789 20.7576C82.1189 20.7726 82.1488 20.7951 82.1688 20.825H82.195V20.3381H82.1688C82.1488 20.3656 82.1189 20.3881 82.0789 20.4056C82.039 20.423 81.9778 20.4318 81.8954 20.4318H81.416V19.7614H82.3223C82.4147 19.7614 82.4871 19.7739 82.5396 19.7988C82.5945 19.8238 82.6344 19.8613 82.6594 19.9112H82.6894V19.4318C82.6544 19.4393 82.6145 19.4455 82.5695 19.4505C82.5246 19.4555 82.4784 19.458 82.431 19.458H80.9628V19.488C81.0028 19.5105 81.034 19.5492 81.0565 19.6041C81.0814 19.659 81.0939 19.7364 81.0939 19.8363V21.6639C81.0939 21.7638 81.0814 21.8412 81.0565 21.8962C81.034 21.9511 81.0028 21.9898 80.9628 22.0123V22.0422H82.4534C82.5009 22.0422 82.5471 22.0435 82.592 22.046C82.6394 22.051 82.6794 22.0584 82.7118 22.0684V21.589H82.6856C82.6607 21.6365 82.6207 21.6739 82.5658 21.7014C82.5109 21.7264 82.4372 21.7389 82.3448 21.7389Z" fill="#15447A"></path>
<path d="M84.1532 19.8775H84.127C84.1095 19.855 84.087 19.835 84.0596 19.8176C84.0346 19.7976 84.0059 19.7814 83.9734 19.7689C83.951 19.7614 83.9247 19.7551 83.8948 19.7501C83.8648 19.7427 83.8311 19.7389 83.7937 19.7389C83.6663 19.7389 83.5639 19.7714 83.4865 19.8363C83.4091 19.9012 83.3704 19.9836 83.3704 20.0835C83.3704 20.1609 83.3917 20.2295 83.4341 20.2895C83.4766 20.3469 83.5502 20.4106 83.6551 20.4805L83.8498 20.6078C83.9597 20.6802 84.0496 20.7551 84.1195 20.8325C84.1919 20.9074 84.2456 20.9873 84.2805 21.0722C84.3155 21.1571 84.333 21.2507 84.333 21.3531C84.333 21.5628 84.2568 21.7339 84.1045 21.8662C83.9547 21.996 83.74 22.0609 83.4603 22.0609C83.3879 22.0609 83.3143 22.0559 83.2394 22.046C83.167 22.0385 83.107 22.0297 83.0596 22.0197V21.6003H83.0858L83.1457 21.6602C83.1657 21.6752 83.1882 21.6889 83.2131 21.7014C83.2431 21.7189 83.2806 21.7339 83.3255 21.7463C83.3704 21.7588 83.4204 21.7651 83.4753 21.7651C83.6326 21.7651 83.7575 21.7276 83.8498 21.6527C83.9422 21.5753 83.9884 21.4754 83.9884 21.3531C83.9884 21.2557 83.9584 21.1696 83.8985 21.0947C83.8411 21.0173 83.7437 20.9336 83.6064 20.8437L83.4154 20.7127C83.2781 20.6228 83.1782 20.5279 83.1158 20.428C83.0558 20.3282 83.0259 20.2146 83.0259 20.0872C83.0259 19.8925 83.097 19.7364 83.2394 19.6191C83.3842 19.4992 83.5827 19.4393 83.8349 19.4393C83.8848 19.4393 83.926 19.4418 83.9584 19.4468C83.9934 19.4493 84.0234 19.4505 84.0483 19.4505C84.0758 19.4505 84.1108 19.4443 84.1532 19.4318V19.8775Z" fill="#15447A"></path>
<path d="M84.8352 19.7614H85.2584V21.6864C85.2584 21.7813 85.2472 21.8537 85.2247 21.9036C85.2048 21.9536 85.1748 21.991 85.1348 22.016V22.0422H85.7041V22.016C85.6642 21.991 85.633 21.9536 85.6105 21.9036C85.5905 21.8537 85.5805 21.7813 85.5805 21.6864V19.7614H86.0037C86.0961 19.7614 86.1685 19.7739 86.221 19.7988C86.2759 19.8238 86.3158 19.8613 86.3408 19.9112H86.3708V19.4318C86.3358 19.4393 86.2959 19.4455 86.2509 19.4505C86.206 19.4555 86.1598 19.458 86.1124 19.458H84.7266C84.6817 19.458 84.6355 19.4555 84.5881 19.4505C84.5431 19.4455 84.5032 19.4393 84.4682 19.4318V19.9112H84.4982C84.5231 19.8613 84.5618 19.8238 84.6143 19.7988C84.6692 19.7739 84.7429 19.7614 84.8352 19.7614Z" fill="#15447A"></path>
<path fill-rule="evenodd" clip-rule="evenodd" d="M87.4372 19.458C87.7394 19.458 87.9978 19.5092 88.2125 19.6116C88.4297 19.7139 88.5958 19.8625 88.7106 20.0573C88.828 20.2495 88.8866 20.4805 88.8866 20.7501C88.8866 21.0198 88.828 21.252 88.7106 21.4467C88.5958 21.639 88.4297 21.7863 88.2125 21.8887C87.9978 21.991 87.7394 22.0422 87.4372 22.0422H86.6245V22.0123C86.6645 21.9898 86.6957 21.9511 86.7182 21.8962C86.7431 21.8412 86.7556 21.7638 86.7556 21.6639V19.8363C86.7556 19.7364 86.7431 19.659 86.7182 19.6041C86.6957 19.5492 86.6645 19.5105 86.6245 19.488V19.458H87.4372ZM88.5496 20.7501C88.5496 20.5504 88.5059 20.3768 88.4185 20.2295C88.3311 20.0797 88.2075 19.9649 88.0477 19.885C87.8904 19.8026 87.7019 19.7614 87.4822 19.7614H87.0777V21.7389H87.4822C87.7019 21.7389 87.8904 21.6989 88.0477 21.619C88.2075 21.5366 88.3311 21.4218 88.4185 21.2744C88.5059 21.1246 88.5496 20.9499 88.5496 20.7501Z" fill="#15447A"></path>
<path d="M89.0963 21.8475C89.0963 21.9124 89.1175 21.9673 89.1599 22.0123C89.2049 22.0572 89.261 22.0797 89.3285 22.0797C89.3934 22.0797 89.4471 22.0572 89.4895 22.0123C89.5344 21.9673 89.5569 21.9124 89.5569 21.8475C89.5569 21.785 89.5344 21.7314 89.4895 21.6864C89.4471 21.6415 89.3934 21.619 89.3285 21.619C89.261 21.619 89.2049 21.6415 89.1599 21.6864C89.1175 21.7314 89.0963 21.785 89.0963 21.8475Z" fill="#15447A"></path>
<path d="M95.6502 21.6864C95.6502 21.7813 95.6614 21.8537 95.6839 21.9036C95.7089 21.9536 95.7413 21.991 95.7813 22.016V22.0422H95.1933V22.016C95.2357 21.991 95.2682 21.9536 95.2907 21.9036C95.3156 21.8537 95.3281 21.7813 95.3281 21.6864V19.8775L94.7588 20.1509H94.7289V19.84L95.6202 19.4318H95.6502V21.6864Z" fill="#15447A"></path>
<path fill-rule="evenodd" clip-rule="evenodd" d="M97.3451 20.9973C97.2528 21.0348 97.1554 21.0535 97.0531 21.0535C96.8983 21.0535 96.7597 21.0185 96.6374 20.9486C96.515 20.8787 96.4189 20.7826 96.349 20.6602C96.2791 20.5379 96.2441 20.3993 96.2441 20.2445C96.2441 20.0797 96.2816 19.9349 96.3565 19.8101C96.4314 19.6827 96.5337 19.5841 96.6636 19.5142C96.7934 19.4418 96.9432 19.4056 97.113 19.4056C97.2803 19.4056 97.4288 19.4405 97.5587 19.5105C97.6885 19.5779 97.7896 19.6727 97.862 19.7951C97.9369 19.9174 97.9744 20.0585 97.9744 20.2183C97.9744 20.3282 97.9557 20.4393 97.9182 20.5516C97.8808 20.6615 97.8183 20.7876 97.731 20.9299L97.0531 22.0422H96.6935V22.016L97.3451 20.9973ZM97.6561 20.2333C97.6561 20.3381 97.6323 20.4318 97.5849 20.5142C97.5375 20.5966 97.4725 20.6615 97.3901 20.7089C97.3078 20.7539 97.2129 20.7763 97.1055 20.7763C96.9981 20.7763 96.9045 20.7539 96.8246 20.7089C96.7447 20.664 96.6811 20.6016 96.6336 20.5217C96.5887 20.4393 96.5662 20.3469 96.5662 20.2445C96.5662 20.1372 96.5887 20.0435 96.6336 19.9636C96.6811 19.8837 96.746 19.8213 96.8284 19.7764C96.9108 19.7289 97.0056 19.7052 97.113 19.7052C97.2179 19.7052 97.3102 19.7277 97.3901 19.7726C97.4725 19.8176 97.5375 19.88 97.5849 19.9599C97.6323 20.0373 97.6561 20.1284 97.6561 20.2333Z" fill="#15447A"></path>
<path d="M100.343 6.74274C100.343 7.17876 99.9895 7.53222 99.5534 7.53222C99.1174 7.53222 98.7639 7.17876 98.7639 6.74274C98.7639 6.30671 99.1174 5.95325 99.5534 5.95325C99.9895 5.95325 100.343 6.30671 100.343 6.74274Z" fill="#15447A"></path>
</svg>
<h1 style="margin-top: 10px;">invoice</h1>
<p>Thank you for choosing us. We appreciate your business!</p>
<h3 style="margin-top: 10px;">Invoice / فاتورة</h3>
<p></p>
</div>
<!-- Details -->
<div class="invoice-details">
<p><strong>invoice Number:</strong> <span class="highlight">#{{invoice.invoice_number}}</span></p>
<p><strong>Date:</strong> {{invoice.date_in_review}}</p>
<p><strong>Customer:</strong> {{invoice.customer.customer_name}}</p>
<p><strong>Email:</strong> {{invoice.customer.email}}</p>
<p><strong>Terms:</strong> {{invoice.terms|title}}</p>
<div class="details-row">
<div class="col-12">
<table class="table table-responsive">
<tr>
<td class="col-3 text-start p-2">
<strong>Invoice Number</strong>
</td>
<td class="col-6 text-center p-2">
<span class="highlight">{{invoice.invoice_number}}</span>
</td>
<td class="col-3 text-end p-2">
<strong>رقم الفاتورة</strong>
</td>
</tr>
<tr>
<td class="col-3 text-start p-2">
<strong>Date</strong>
</td>
<td class="col-6 text-center p-2">
<span class="highlight">{{invoice.date_in_review}}</span>
</td>
<td class="col-3 text-end p-2">
<strong>التاريخ</strong>
</td>
</tr>
<tr>
<td class="col-3 text-start p-2">
<strong>Customer</strong>
</td>
<td class="col-6 text-center p-2">
<span class="highlight">{{invoice.customer.customer_name}}</span>
</td>
<td class="col-3 text-end p-2">
<strong>العميل</strong>
</td>
</tr>
<tr>
<td class="col-3 text-start p-2">
<strong>Email</strong>
</td>
<td class="col-6 text-center p-2">
<span class="highlight">{{invoice.customer.email}}</span>
</td>
<td class="col-3 text-end p-2">
<strong>البريد الالكتروني</strong>
</td>
</tr>
<tr>
<td class="col-3 text-start p-2">
<strong>Terms</strong>
</td>
<td class="col-6 text-center p-2">
<span class="highlight">{{invoice.get_terms_display}}</span>
</td>
<td class="col-3 text-end p-2">
<strong>طريقة الدفع</strong>
</td>
</tr>
</table>
</div>
</div>
<!-- Items Table -->
<div class="invoice-table">
<div class="invoice-table mt-3">
<table class="table table-bordered">
<thead>
<tr>
<th>Item</th>
<th>Quantity</th>
<th>Unit Price</th>
<th>Total</th>
<th><small class="fs-10">Item</small><br><small class="fs-10">الصنف</small></th>
<th><small class="fs-10">Quantity</small><br><small class="fs-10">العدد</small></th>
<th><small class="fs-10">Unit Price</small><br><small class="fs-10">سعر الوحدة</small></th>
<th><small class="fs-10">Total</small><br><small class="fs-10">الإجمالي</small></th>
</tr>
</thead>
<tbody>
@ -210,24 +222,25 @@
<!-- Additional Charges (VAT and Services) -->
<div class="additional-charges">
<p><strong>VAT ({{vat}}%):</strong> <span class="highlight">${{vat_amount}}</span></p>
<p><strong>Additional Services:</strong>
<p><strong>VAT/ضريبة القيمة المضافة ({{vat}}%):</strong> <span class="highlight">{{vat_amount}}&nbsp;{{ _("SAR") }}</span></p>
<p><strong>Additional Services/ الخدمات الإضافية</strong>
<br>
{% for service in additional_services %}
<span class="highlight">{{service.name}} - ${{service.price}}</span><br>
<span class="highlight">{{service.name}} - {{service.price}}&nbsp;{{ _("SAR") }}</span><br>
{% endfor %}
</p>
</div>
<!-- Total -->
<div class="invoice-total">
<p><strong>Total Amount:</strong> <span class="highlight">${{total}}</span></p>
<p><strong>Total/الإجمالي</strong> <span class="highlight">{{total}}&nbsp;{{ _("SAR") }}</span></p>
</div>
<!-- Footer Note -->
<div class="footer-note">
<p>If you have any questions, feel free to contact us at <a href="mailto:support@example.com">support@example.com</a>.</p>
<p>Thank you for your business!</p>
<p><small class="text-end">تواصل معنا <a href="mailto:haikal@tenhal.sa">haikal@tenhal.sa</a>.</small>
<small class="text-start">Contact US <a href="mailto:haikal@tenhal.sa">haikal@tenhal.sa</a>.</small></p>
<p>Made by TENHAL.SA</p>
</div>
<!-- Button row -->
</div>
@ -245,7 +258,7 @@
// Options for html2pdf.js
const options = {
margin: 0, // No margin
filename: 'invoice.pdf', // Name of the downloaded file
filename: '{{invoice.invoice_number}}.pdf', // Name of the downloaded file
image: { type: 'jpeg', quality: 0.98 }, // Image quality
html2canvas: {
scale: 2, // Increase scale for better quality

12
test.txt Normal file
View File

@ -0,0 +1,12 @@
from django.core.mail import send_mail
send_mail(
'Test Email Subject',
'This is a test email message.',
'info@tenhal.sa', # From email
['recipient@example.com'], # To email
fail_silently=False,
)