feat: enhance appointment management with email service integration and calendar view options
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
Class for all funktions of the executive -> Lehrer
|
||||
"""
|
||||
import datetime
|
||||
from flask import url_for
|
||||
import Web.modules.emailservice.email as mail_service
|
||||
import Web.modules.database.termine as termin
|
||||
import Web.modules.database.settings as cfg
|
||||
@@ -56,11 +57,14 @@ def new(date_start: str, date_end: str, time_span: list, slots: int, slot_lenght
|
||||
if tenant_context:
|
||||
subdomain = getattr(tenant_context, 'subdomain', '') or getattr(tenant_context, 'tenant_id', '') or ''
|
||||
|
||||
host = f"https://{subdomain}.invario.eu" if subdomain else "invario.eu"
|
||||
link = host + "/terminplaner/client" + "?" + "client_id=" + id_str
|
||||
try:
|
||||
link = url_for('terminplaner.client', appointment_id=id_str, _external=True)
|
||||
except Exception:
|
||||
host = f"https://{subdomain}.invario.eu" if subdomain else "https://invario.eu"
|
||||
link = host + "/terminplaner/client/" + id_str
|
||||
subject = f"Terminanfrage von {user}"
|
||||
note_link = note + f"Bitte klicken sie auf den folgenden Link um einen Termin zu vereinbaren: {link}"
|
||||
if normalized_mail:
|
||||
if normalized_mail and cfg.EMAIL_ENABLED:
|
||||
mail_service.send(normalized_mail, subject, note_link)
|
||||
return link
|
||||
|
||||
|
||||
@@ -75,13 +75,28 @@ def configure():
|
||||
|
||||
if not start or not end or not time or not slots_amount or not slot_lenght:
|
||||
flash('Bitte alle Pflichtfelder ausfüllen.', 'error')
|
||||
return render_template('termin_configure.html', school_periods=cfg.SCHOOL_PERIODS, generated_link=None)
|
||||
return render_template(
|
||||
'termin_configure.html',
|
||||
school_periods=cfg.SCHOOL_PERIODS,
|
||||
generated_link=None,
|
||||
email_service_enabled=cfg.EMAIL_ENABLED,
|
||||
)
|
||||
|
||||
link = appointment_service.new(start, end, time, slots_amount, slot_lenght, session["username"], mail, note)
|
||||
flash('Der Terminplan wurde angelegt.', 'success')
|
||||
return render_template('termin_configure.html', school_periods=cfg.SCHOOL_PERIODS, generated_link=link)
|
||||
return render_template(
|
||||
'termin_configure.html',
|
||||
school_periods=cfg.SCHOOL_PERIODS,
|
||||
generated_link=link,
|
||||
email_service_enabled=cfg.EMAIL_ENABLED,
|
||||
)
|
||||
elif request.method == "GET":
|
||||
return render_template('termin_configure.html', school_periods=cfg.SCHOOL_PERIODS, generated_link=None)
|
||||
return render_template(
|
||||
'termin_configure.html',
|
||||
school_periods=cfg.SCHOOL_PERIODS,
|
||||
generated_link=None,
|
||||
email_service_enabled=cfg.EMAIL_ENABLED,
|
||||
)
|
||||
|
||||
@appoint_bp.route('/')
|
||||
def main():
|
||||
|
||||
@@ -269,23 +269,6 @@
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Compact appointment badge on cards */
|
||||
.appointment-badge {
|
||||
margin-top: 10px;
|
||||
padding: 10px 12px;
|
||||
background-color: #e1f5fe;
|
||||
border-left: 4px solid #03a9f4;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.appointment-badge .appointment-time {
|
||||
display: inline-block;
|
||||
margin-top: 4px;
|
||||
color: #01579b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Search styles */
|
||||
.search-container {
|
||||
display: flex;
|
||||
@@ -1006,23 +989,6 @@
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Add appointment badge if item has upcoming appointments
|
||||
let appointmentBadge = '';
|
||||
if (item.appointments && Array.isArray(item.appointments) && item.appointments.length > 0) {
|
||||
const upcomingAppointment = getUpcomingAppointment(item.appointments);
|
||||
if (upcomingAppointment) {
|
||||
const formattedDate = formatAppointmentDate(upcomingAppointment.date);
|
||||
const startPeriod = formatAppointmentPeriod(upcomingAppointment.start_period);
|
||||
const endPeriod = formatAppointmentPeriod(upcomingAppointment.end_period);
|
||||
|
||||
appointmentBadge = `
|
||||
<div class="appointment-badge">
|
||||
<strong>Geplant für:</strong> ${formattedDate}<br>
|
||||
<span class="appointment-time">${startPeriod}${endPeriod && endPeriod !== startPeriod ? ' - ' + endPeriod : ''}</span>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="card-content" data-item-id="${item._id}">
|
||||
<h3 class="item-col-name">${item.Name}</h3>
|
||||
@@ -1037,7 +1003,6 @@
|
||||
${imagesHtml}
|
||||
</div>
|
||||
${borrowerBadge}
|
||||
${appointmentBadge}
|
||||
</div>
|
||||
<div class="actions">
|
||||
${isAvailableForBorrow && !item.BlockedNow ?
|
||||
@@ -1614,27 +1579,6 @@
|
||||
`;
|
||||
}
|
||||
|
||||
// Add appointment info panel if item has appointments
|
||||
let appointmentInfoHtml = '';
|
||||
if (item.appointments && Array.isArray(item.appointments) && item.appointments.length > 0) {
|
||||
const upcomingAppointment = getUpcomingAppointment(item.appointments);
|
||||
if (upcomingAppointment) {
|
||||
const formattedDate = formatAppointmentDate(upcomingAppointment.date);
|
||||
const startPeriod = formatAppointmentPeriod(upcomingAppointment.start_period);
|
||||
const endPeriod = formatAppointmentPeriod(upcomingAppointment.end_period);
|
||||
const timeDisplay = startPeriod + (endPeriod && endPeriod !== startPeriod ? ' - ' + endPeriod : '');
|
||||
|
||||
appointmentInfoHtml = `
|
||||
<div class="appointment-info-panel">
|
||||
<h4>Geplanter Termin</h4>
|
||||
<p class="appointment-date">Datum: ${formattedDate}</p>
|
||||
<p class="appointment-time">Zeit: ${timeDisplay}</p>
|
||||
${upcomingAppointment.notes ? `<p class="appointment-notes">Notizen: ${upcomingAppointment.notes}</p>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Build modal content HTML
|
||||
modalContent.innerHTML = `
|
||||
<h2>${item.Name}</h2>
|
||||
@@ -1642,7 +1586,6 @@
|
||||
<button type="button" class="bookmark-btn" id="modal-bookmark-btn" title="Merken">${(window.currentFavorites||new Set()).has(item._id) ? '★' : '☆'}</button>
|
||||
</div>
|
||||
${borrowerInfoHtml}
|
||||
${appointmentInfoHtml}
|
||||
|
||||
<div class="modal-image-container">
|
||||
${imagesHtml}
|
||||
@@ -2804,66 +2747,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions for appointment display
|
||||
function formatAppointmentDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric'
|
||||
});
|
||||
} catch (e) {
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
|
||||
function formatAppointmentPeriod(period) {
|
||||
if (!period) return '';
|
||||
const periodMap = {
|
||||
'1': '1. Stunde',
|
||||
'2': '2. Stunde',
|
||||
'3': '3. Stunde',
|
||||
'4': '4. Stunde',
|
||||
'5': '5. Stunde',
|
||||
'6': '6. Stunde',
|
||||
'7': '7. Stunde',
|
||||
'8': '8. Stunde',
|
||||
'9': '9. Stunde',
|
||||
'10': '10. Stunde',
|
||||
'pause1': '1. Pause',
|
||||
'pause2': '2. Pause',
|
||||
'mittagspause': 'Mittagspause'
|
||||
};
|
||||
return periodMap[period] || period;
|
||||
}
|
||||
|
||||
function getUpcomingAppointment(appointments) {
|
||||
if (!appointments || !Array.isArray(appointments) || appointments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const today = now.toDateString();
|
||||
|
||||
// Filter appointments that are today or in the future
|
||||
const futureAppointments = appointments.filter(apt => {
|
||||
try {
|
||||
const aptDate = new Date(apt.date);
|
||||
return aptDate.toDateString() === today || aptDate >= now.setHours(0, 0, 0, 0);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (futureAppointments.length === 0) return null;
|
||||
|
||||
// Sort by date and return the earliest
|
||||
futureAppointments.sort((a, b) => new Date(a.date) - new Date(b.date));
|
||||
return futureAppointments[0];
|
||||
}
|
||||
|
||||
function formatDateForInput(dateString) {
|
||||
const options = { year: 'numeric', month: '2-digit', day: '2-digit' };
|
||||
const date = new Date(dateString);
|
||||
@@ -3947,16 +3830,6 @@
|
||||
.borrower-info-panel h4 { color: #856404; margin: 0 0 10px 0; font-size: 1.1rem; }
|
||||
.borrower-name, .borrow-time { margin: 5px 0; color: #856404; font-weight: 500; }
|
||||
|
||||
.appointment-info-panel {
|
||||
background-color: #e1f5fe;
|
||||
border: 1px solid #81d4fa;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.appointment-info-panel h4 { color: #0277bd; margin: 0 0 10px 0; font-size: 1.1rem; }
|
||||
.appointment-date, .appointment-time, .appointment-user { margin: 5px 0; color: #0277bd; font-weight: 500; }
|
||||
|
||||
/* Modal Details Styling (text formatting) */
|
||||
.modal-details { margin: 20px 0; }
|
||||
.detail-group { display: flex; margin-bottom: 10px; align-items: flex-start; }
|
||||
|
||||
@@ -62,7 +62,13 @@
|
||||
{% if generated_link %}
|
||||
<div class="alert alert-success mt-4 shadow-sm rounded-4">
|
||||
<div class="fw-bold mb-1">Buchungslink erstellt</div>
|
||||
<div class="mb-2">Teilen Sie diesen Link mit den Teilnehmenden:</div>
|
||||
<div class="mb-2">
|
||||
{% if email_service_enabled %}
|
||||
Teilen Sie diesen Link mit den Teilnehmenden oder versenden Sie ihn direkt per E-Mail.
|
||||
{% else %}
|
||||
Der E-Mail-Service ist deaktiviert. Teilen Sie diesen Link manuell mit den Teilnehmenden.
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="{{ generated_link }}" class="d-inline-block text-break">{{ generated_link }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -19,6 +19,11 @@
|
||||
<span id="current-day-display"></span>
|
||||
<button id="next-day">Nächster Tag</button>
|
||||
<button id="today">Heute</button>
|
||||
<div class="calendar-view-switch" role="group" aria-label="Kalenderansicht wechseln">
|
||||
<button type="button" class="calendar-view-btn active" data-calendar-view="timeGridDay">Tag</button>
|
||||
<button type="button" class="calendar-view-btn" data-calendar-view="timeGridWeek">Woche</button>
|
||||
<button type="button" class="calendar-view-btn" data-calendar-view="dayGridMonth">Monat</button>
|
||||
</div>
|
||||
<button id="new-booking" class="primary-button">Neue Reservierung</button>
|
||||
</div>
|
||||
<div class="calendar-legend">
|
||||
@@ -179,6 +184,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
let currentEventId = null;
|
||||
let calendar;
|
||||
let showCompletedBookings = false;
|
||||
let activeCalendarView = 'timeGridDay';
|
||||
|
||||
// School period times from server configuration
|
||||
let schoolPeriods;
|
||||
@@ -423,8 +429,23 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
showEventDetails(info.event);
|
||||
},
|
||||
datesSet: function(dateInfo) {
|
||||
|
||||
function setCalendarView(viewName) {
|
||||
activeCalendarView = viewName;
|
||||
calendar.changeView(viewName);
|
||||
document.querySelectorAll('[data-calendar-view]').forEach(button => {
|
||||
button.classList.toggle('active', button.dataset.calendarView === viewName);
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-calendar-view]').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
setCalendarView(this.dataset.calendarView);
|
||||
});
|
||||
});
|
||||
updateCurrentDayDisplay();
|
||||
}
|
||||
setCalendarView(activeCalendarView);
|
||||
});
|
||||
|
||||
// Render calendar
|
||||
@@ -1256,6 +1277,32 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.calendar-view-switch {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px;
|
||||
border: 1px solid #d7dce3;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
}
|
||||
|
||||
.calendar-view-btn {
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: var(--ui-text);
|
||||
font-weight: 600;
|
||||
padding: 7px 14px;
|
||||
transition: background-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.calendar-view-btn.active {
|
||||
background: var(--module-accent-color);
|
||||
color: #fff;
|
||||
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
|
||||
.calendar-actions .primary-button {
|
||||
background-color: #3788d8;
|
||||
color: white;
|
||||
@@ -1281,6 +1328,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
width: 100%;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.calendar-view-switch {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.calendar-view-btn {
|
||||
flex: 1 1 30%;
|
||||
min-width: 88px;
|
||||
}
|
||||
|
||||
.calendar-actions .primary-button {
|
||||
margin-left: 0;
|
||||
|
||||
Reference in New Issue
Block a user