Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ef2d1dfab0 | |||
| 1c5aa9beed |
+1
-1
@@ -1 +1 @@
|
|||||||
v0.8.9
|
v0.8.10
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
Class for all funktions of the executive -> Lehrer
|
Class for all funktions of the executive -> Lehrer
|
||||||
"""
|
"""
|
||||||
import datetime
|
import datetime
|
||||||
|
from flask import url_for
|
||||||
import Web.modules.emailservice.email as mail_service
|
import Web.modules.emailservice.email as mail_service
|
||||||
import Web.modules.database.termine as termin
|
import Web.modules.database.termine as termin
|
||||||
import Web.modules.database.settings as cfg
|
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:
|
if tenant_context:
|
||||||
subdomain = getattr(tenant_context, 'subdomain', '') or getattr(tenant_context, 'tenant_id', '') or ''
|
subdomain = getattr(tenant_context, 'subdomain', '') or getattr(tenant_context, 'tenant_id', '') or ''
|
||||||
|
|
||||||
host = f"https://{subdomain}.invario.eu" if subdomain else "invario.eu"
|
try:
|
||||||
link = host + "/terminplaner/client" + "?" + "client_id=" + id_str
|
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}"
|
subject = f"Terminanfrage von {user}"
|
||||||
note_link = note + f"Bitte klicken sie auf den folgenden Link um einen Termin zu vereinbaren: {link}"
|
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)
|
mail_service.send(normalized_mail, subject, note_link)
|
||||||
return 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:
|
if not start or not end or not time or not slots_amount or not slot_lenght:
|
||||||
flash('Bitte alle Pflichtfelder ausfüllen.', 'error')
|
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)
|
link = appointment_service.new(start, end, time, slots_amount, slot_lenght, session["username"], mail, note)
|
||||||
flash('Der Terminplan wurde angelegt.', 'success')
|
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":
|
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('/')
|
@appoint_bp.route('/')
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -269,23 +269,6 @@
|
|||||||
line-height: 1;
|
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 styles */
|
||||||
.search-container {
|
.search-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1006,23 +989,6 @@
|
|||||||
</div>`;
|
</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 = `
|
card.innerHTML = `
|
||||||
<div class="card-content" data-item-id="${item._id}">
|
<div class="card-content" data-item-id="${item._id}">
|
||||||
<h3 class="item-col-name">${item.Name}</h3>
|
<h3 class="item-col-name">${item.Name}</h3>
|
||||||
@@ -1037,7 +1003,6 @@
|
|||||||
${imagesHtml}
|
${imagesHtml}
|
||||||
</div>
|
</div>
|
||||||
${borrowerBadge}
|
${borrowerBadge}
|
||||||
${appointmentBadge}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
${isAvailableForBorrow && !item.BlockedNow ?
|
${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
|
// Build modal content HTML
|
||||||
modalContent.innerHTML = `
|
modalContent.innerHTML = `
|
||||||
<h2>${item.Name}</h2>
|
<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>
|
<button type="button" class="bookmark-btn" id="modal-bookmark-btn" title="Merken">${(window.currentFavorites||new Set()).has(item._id) ? '★' : '☆'}</button>
|
||||||
</div>
|
</div>
|
||||||
${borrowerInfoHtml}
|
${borrowerInfoHtml}
|
||||||
${appointmentInfoHtml}
|
|
||||||
|
|
||||||
<div class="modal-image-container">
|
<div class="modal-image-container">
|
||||||
${imagesHtml}
|
${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) {
|
function formatDateForInput(dateString) {
|
||||||
const options = { year: 'numeric', month: '2-digit', day: '2-digit' };
|
const options = { year: 'numeric', month: '2-digit', day: '2-digit' };
|
||||||
const date = new Date(dateString);
|
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-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; }
|
.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 Styling (text formatting) */
|
||||||
.modal-details { margin: 20px 0; }
|
.modal-details { margin: 20px 0; }
|
||||||
.detail-group { display: flex; margin-bottom: 10px; align-items: flex-start; }
|
.detail-group { display: flex; margin-bottom: 10px; align-items: flex-start; }
|
||||||
|
|||||||
@@ -62,7 +62,13 @@
|
|||||||
{% if generated_link %}
|
{% if generated_link %}
|
||||||
<div class="alert alert-success mt-4 shadow-sm rounded-4">
|
<div class="alert alert-success mt-4 shadow-sm rounded-4">
|
||||||
<div class="fw-bold mb-1">Buchungslink erstellt</div>
|
<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>
|
<a href="{{ generated_link }}" class="d-inline-block text-break">{{ generated_link }}</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -19,6 +19,11 @@
|
|||||||
<span id="current-day-display"></span>
|
<span id="current-day-display"></span>
|
||||||
<button id="next-day">Nächster Tag</button>
|
<button id="next-day">Nächster Tag</button>
|
||||||
<button id="today">Heute</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>
|
<button id="new-booking" class="primary-button">Neue Reservierung</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="calendar-legend">
|
<div class="calendar-legend">
|
||||||
@@ -179,6 +184,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
let currentEventId = null;
|
let currentEventId = null;
|
||||||
let calendar;
|
let calendar;
|
||||||
let showCompletedBookings = false;
|
let showCompletedBookings = false;
|
||||||
|
let activeCalendarView = 'timeGridDay';
|
||||||
|
|
||||||
// School period times from server configuration
|
// School period times from server configuration
|
||||||
let schoolPeriods;
|
let schoolPeriods;
|
||||||
@@ -423,8 +429,23 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
showEventDetails(info.event);
|
showEventDetails(info.event);
|
||||||
},
|
},
|
||||||
datesSet: function(dateInfo) {
|
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();
|
updateCurrentDayDisplay();
|
||||||
}
|
}
|
||||||
|
setCalendarView(activeCalendarView);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Render calendar
|
// Render calendar
|
||||||
@@ -1256,6 +1277,32 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
white-space: nowrap;
|
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 {
|
.calendar-actions .primary-button {
|
||||||
background-color: #3788d8;
|
background-color: #3788d8;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -1281,6 +1328,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 5px 0;
|
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 {
|
.calendar-actions .primary-button {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user