Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c3615bac76 | |||
| 0868caf35f | |||
| 297ee45590 | |||
| b85e19b94a | |||
| 0a6ecda7a2 | |||
| 5d6235679a | |||
| 4533fd6402 |
@@ -0,0 +1,4 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[Buildset]
|
||||||
|
BuildItems=@Variant(\x00\x00\x00\t\x00\x00\x00\x00\x01\x00\x00\x00\x0b\x00\x00\x00\x00\x01\x00\x00\x00\x10\x00S\x00G\x00T\x00M\x00e\x00n\x00s\x00a)
|
||||||
|
|
||||||
|
[Project]
|
||||||
|
VersionControlSupport=kdevgit
|
||||||
BIN
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+30
-28
@@ -3,33 +3,35 @@ from django.contrib import admin
|
|||||||
# Register your models here.
|
# Register your models here.
|
||||||
from .models import Person # Ersetze dies durch deine echten Klassennamen
|
from .models import Person # Ersetze dies durch deine echten Klassennamen
|
||||||
|
|
||||||
class PersonInlineChildren(admin.TabularInline):
|
|
||||||
"""
|
|
||||||
Inline für das Feld ``children`` (Schüler eines Elternteils).
|
|
||||||
Nur bei Eltern (Rolle Mitarbeit/Chef) sichtbar.
|
|
||||||
"""
|
|
||||||
model = Person.children.through # Das durchschnittliche Join‑Model
|
|
||||||
verbose_name = 'Kind'
|
|
||||||
verbose_name_plural = 'Kinder'
|
|
||||||
extra = 0 # Keine leeren Zeilen anzeigen
|
|
||||||
|
|
||||||
# ----------------------------------------------------------
|
# DELTE: Die Option, Kinder zuzuordnen soll zunächst nicht umgesetzt werden.
|
||||||
# **WICHTIG:** Hier wird der zu verwendende FK explizit genannt
|
# class PersonInlineChildren(admin.TabularInline):
|
||||||
# ----------------------------------------------------------
|
# """
|
||||||
fk_name = 'person' # <-- legt fest, welcher FK gemeint ist
|
# Inline für das Feld ``children`` (Schüler eines Elternteils).
|
||||||
|
# Nur bei Eltern (Rolle Mitarbeit/Chef) sichtbar.
|
||||||
def has_add_permission(self, request, obj=None):
|
# """
|
||||||
# Erlaube Hinzufügen nur für Eltern (Rolle Eltern)
|
# model = Person.children.through # Das durchschnittliche Join‑Model
|
||||||
return obj and obj.rolle in ('eltern')
|
# verbose_name = 'Kind'
|
||||||
|
# verbose_name_plural = 'Kinder'
|
||||||
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
# extra = 0 # Keine leeren Zeilen anzeigen
|
||||||
"""
|
#
|
||||||
Verhindere, dass ein Benutzer sich selbst als Kind hinzufügt.
|
# # ----------------------------------------------------------
|
||||||
"""
|
# # **WICHTIG:** Hier wird der zu verwendende FK explizit genannt
|
||||||
if db_field.name == "person":
|
# # ----------------------------------------------------------
|
||||||
# Hier handelt es sich um den FK auf die Person (das Ziel)
|
# fk_name = 'person' # <-- legt fest, welcher FK gemeint ist
|
||||||
kwargs["queryset"] = Person.objects.exclude(pk=self.instance.pk)
|
#
|
||||||
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
# def has_add_permission(self, request, obj=None):
|
||||||
|
# # Erlaube Hinzufügen nur für Eltern (Rolle Eltern)
|
||||||
|
# return obj and obj.rolle in ('eltern')
|
||||||
|
#
|
||||||
|
# def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
||||||
|
# """
|
||||||
|
# Verhindere, dass ein Benutzer sich selbst als Kind hinzufügt.
|
||||||
|
# """
|
||||||
|
# if db_field.name == "person":
|
||||||
|
# # Hier handelt es sich um den FK auf die Person (das Ziel)
|
||||||
|
# kwargs["queryset"] = Person.objects.exclude(pk=self.instance.pk)
|
||||||
|
# return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
||||||
|
|
||||||
@admin.register(Person)
|
@admin.register(Person)
|
||||||
class PersonAdmin(admin.ModelAdmin):
|
class PersonAdmin(admin.ModelAdmin):
|
||||||
@@ -37,8 +39,8 @@ class PersonAdmin(admin.ModelAdmin):
|
|||||||
search_fields = ['user__username', 'klasse']
|
search_fields = ['user__username', 'klasse']
|
||||||
ordering = ('rolle',)
|
ordering = ('rolle',)
|
||||||
|
|
||||||
# ---- Inline für Eltern (Rolle Mitarbeit/Chef) ----
|
# # ---- Inline für Eltern (Rolle Mitarbeit/Chef) ----
|
||||||
inlines = [PersonInlineChildren] # Zeigt das Kinder‑Inline nur bei passenden Rollen an
|
# inlines = [PersonInlineChildren] # Zeigt das Kinder‑Inline nur bei passenden Rollen an
|
||||||
|
|
||||||
def has_change_permission(self, request, obj=None):
|
def has_change_permission(self, request, obj=None):
|
||||||
"""
|
"""
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,65 @@
|
|||||||
|
@startuml
|
||||||
|
|
||||||
|
' Teilnehmer – Standard‑Icons
|
||||||
|
actor Student as "Student"
|
||||||
|
participant Browser as "Browser"
|
||||||
|
participant WebsiteServer as "Website Server"
|
||||||
|
database OrderDB as "Order DB"
|
||||||
|
participant OrderService as "Order Service"
|
||||||
|
participant InventoryService as "Inventory Service"
|
||||||
|
participant KitchenDisplaySystem as "Kitchen Display"
|
||||||
|
participant NotificationService as "Notification Service"
|
||||||
|
|
||||||
|
' ---------- Login ----------
|
||||||
|
Student -> Browser : Öffnet die Webseite
|
||||||
|
Browser -> WebsiteServer : GET /login
|
||||||
|
WebsiteServer --> Browser : Liefert Login‑Seite
|
||||||
|
Student -> Browser : Gibt Benutzerdaten ein und schickt Formular
|
||||||
|
Browser -> WebsiteServer : POST /login\n{username, password}
|
||||||
|
WebsiteServer --> OrderService : Authentifiziere User
|
||||||
|
OrderService --> WebsiteServer : OK (Session‑Token)
|
||||||
|
WebsiteServer --> Browser : Setzt Session‑Cookie & leitet zur Menüseite
|
||||||
|
|
||||||
|
' ---------- Menü auswählen ----------
|
||||||
|
Student -> Browser : Wählt Speisen aus dem Menü
|
||||||
|
Browser -> WebsiteServer : GET /menu\n{session}
|
||||||
|
WebsiteServer --> OrderDB : Liefert aktuelle Menüpunkte
|
||||||
|
OrderDB --> WebsiteServer : Menüdaten
|
||||||
|
WebsiteServer --> Browser : Zeigt Menü
|
||||||
|
|
||||||
|
' ---------- Artikel zum Warenkorb hinzufügen ----------
|
||||||
|
loop Für jeden gewählten Artikel
|
||||||
|
Student -> Browser : Klickt „In den Warenkorb“
|
||||||
|
Browser -> OrderService : POST /cart/add\n{session, articleID}
|
||||||
|
OrderService -> OrderDB : Update Cart (Add Item)
|
||||||
|
OrderDB --> OrderService : OK
|
||||||
|
OrderService --> Browser : Rückmeldung "Artikel hinzugefügt"
|
||||||
|
end
|
||||||
|
|
||||||
|
' ---------- Bestellung prüfen ----------
|
||||||
|
Student -> Browser : Öffnet Warenkorb und prüft Bestellübersicht
|
||||||
|
Browser -> OrderService : GET /cart\n{session}
|
||||||
|
OrderService -> OrderDB : Liefert aktuelle Cart‑Daten
|
||||||
|
OrderDB --> OrderService : Cart‑Details
|
||||||
|
OrderService --> Browser : Zeigt Cart‑Übersicht
|
||||||
|
|
||||||
|
' ---------- Checkout (ohne Bezahlung) ----------
|
||||||
|
Student -> Browser : Klicke „Zur Kasse“
|
||||||
|
Browser -> WebsiteServer : GET /checkout\n{session}
|
||||||
|
WebsiteServer --> NotificationService : Sende “Bestellung anstehend” (Optional)
|
||||||
|
NotificationService --> Browser : Bestätigungsnachricht
|
||||||
|
Student -> Browser : Gibt Zahlungsinformationen für Bezahlung vor Ort ein und schickt Formular
|
||||||
|
Browser -> OrderService : POST /checkout/confirm\n{session, paymentMethod="VorOrt"}
|
||||||
|
OrderService --> OrderDB : Speichere Order & set Status “Pending Payment”
|
||||||
|
OrderDB --> OrderService : OK
|
||||||
|
OrderService --> KitchenDisplaySystem : Sende Order‑Details (Zubereitung)
|
||||||
|
KitchenDisplaySystem --> Browser : Zeigt „Bestellung in Bearbeitung“
|
||||||
|
OrderService -> InventoryService : Update Bestand
|
||||||
|
InventoryService --> OrderService : OK
|
||||||
|
OrderService --> NotificationService : Sende “Bestellung bestätigt” (Email/SMS)
|
||||||
|
|
||||||
|
' ---------- Fertigstellung ----------
|
||||||
|
Student -> Browser : Empfängt Benachrichtigung “Ihr Essen ist fertig”
|
||||||
|
Browser -> KitchenDisplaySystem : Optional „Abholung bestätigen“
|
||||||
|
|
||||||
|
@enduml
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# Generated by Django 5.2.14 on 2026-05-15 09:28
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mensa_app', '0004_alter_gericht_time_creation_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='bestellung',
|
||||||
|
name='id',
|
||||||
|
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='gericht',
|
||||||
|
name='id',
|
||||||
|
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='gericht',
|
||||||
|
name='time_creation',
|
||||||
|
field=models.TimeField(default=datetime.datetime(2026, 5, 15, 9, 28, 5, 292454)),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='gericht',
|
||||||
|
name='time_last_change',
|
||||||
|
field=models.TimeField(default=datetime.datetime(2026, 5, 15, 9, 28, 5, 292454)),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='kategorie',
|
||||||
|
name='id',
|
||||||
|
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='menue',
|
||||||
|
name='id',
|
||||||
|
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='person',
|
||||||
|
name='id',
|
||||||
|
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='speiseplantag',
|
||||||
|
name='id',
|
||||||
|
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Bewertung',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('sterne', models.IntegerField(choices=[(1, '★☆☆☆☆'), (2, '★★☆☆☆'), (3, '★★★☆☆'), (4, '★★★★☆'), (5, '★★★★★')], default=3)),
|
||||||
|
('kommentar', models.TextField(blank=True, null=True)),
|
||||||
|
('datum', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('ist_verifiziert', models.BooleanField(default=False, help_text='Wird automatisch auf True gesetzt, wenn eine bezahlte Bestellung vorliegt.')),
|
||||||
|
('gericht', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bewertungen', to='mensa_app.gericht')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bewertungen', to='mensa_app.person')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Bewertung',
|
||||||
|
'verbose_name_plural': 'Bewertungen',
|
||||||
|
'unique_together': {('user', 'gericht')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Generated by Django 5.2.14 on 2026-06-12 09:13
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mensa_app', '0005_alter_bestellung_id_alter_gericht_id_and_more'),
|
||||||
|
('mensa_app', '0009_alter_gericht_time_creation_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
]
|
||||||
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
+50
-48
@@ -19,55 +19,57 @@ class Person(models.Model):
|
|||||||
rolle = models.CharField(max_length=20, choices=[('schueler', 'Schüler'), ('eltern', 'Eltern'), ('lehrer', 'Lehrer'), ('mitarbeiter','Mensa-Mitarbeiter'), ('chef','Mensa-Leitung')])
|
rolle = models.CharField(max_length=20, choices=[('schueler', 'Schüler'), ('eltern', 'Eltern'), ('lehrer', 'Lehrer'), ('mitarbeiter','Mensa-Mitarbeiter'), ('chef','Mensa-Leitung')])
|
||||||
klasse = models.CharField(max_length=4, blank=True, null=True) # Nur für Schüler relevant
|
klasse = models.CharField(max_length=4, blank=True, null=True) # Nur für Schüler relevant
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
# Neue Many‑to‑Many Beziehung: „Elternteil ↔ Schüler“
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
children = models.ManyToManyField(
|
|
||||||
'self',
|
|
||||||
blank=True,
|
|
||||||
related_name='parents', # → für einen Schüler: person.parents.all()
|
|
||||||
symmetrical=False, # Verhindert eine zirkuläre Rück‑Beziehung (nicht benötigt)
|
|
||||||
help_text=(
|
|
||||||
"Eltern können hier die ihnen zugewiesenen Schüler hinzufügen. Für Schüler bleibt dieses Feld leer."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# DELETE: Die Eltern-Kinder Beziehung soll zunächst nicht umgesetzt werden.
|
||||||
# Optional: Hilfsmethode, die alle Kinder zurückgibt, ggf. mit einer Sortierung
|
# # ----------------------------------------------------------------------
|
||||||
def get_children_sorted(self):
|
# # Neue Many‑to‑Many Beziehung: „Elternteil ↔ Schüler“
|
||||||
# Das `order_by('name')` ist ein Beispiel – du kannst deine eigene Sortierung verwenden.
|
# # ----------------------------------------------------------------------
|
||||||
return self.children.order_by('name')
|
# children = models.ManyToManyField(
|
||||||
|
# 'self',
|
||||||
class Meta:
|
# blank=True,
|
||||||
verbose_name_plural = "Personen"
|
# related_name='parents', # → für einen Schüler: person.parents.all()
|
||||||
|
# symmetrical=False, # Verhindert eine zirkuläre Rück‑Beziehung (nicht benötigt)
|
||||||
def __str__(self):
|
# help_text=(
|
||||||
if self.children.exists():
|
# "Eltern können hier die ihnen zugewiesenen Schüler hinzufügen. Für Schüler bleibt dieses Feld leer."
|
||||||
# Wir sammeln alle Kinder‑Namen in einem String, z. B. mit Komma getrennt.
|
# )
|
||||||
children_names = ', '.join([c.name for c in self.children.all()])
|
# )
|
||||||
return f"{self.user.username} ({children_names}) ({self.rolle})"
|
#
|
||||||
else:
|
# # ------------------------------------------------------------------
|
||||||
return f"{self.user.username} ({self.rolle})"
|
# # Optional: Hilfsmethode, die alle Kinder zurückgibt, ggf. mit einer Sortierung
|
||||||
|
# def get_children_sorted(self):
|
||||||
def clean(self):
|
# # Das `order_by('name')` ist ein Beispiel – du kannst deine eigene Sortierung verwenden.
|
||||||
"""
|
# return self.children.order_by('name')
|
||||||
Überprüfung für die ManyToMany-Beziehung ``children``:
|
#
|
||||||
- Nur Personen mit der Rolle 'mitarbeiter' oder 'chef' (Eltern) dürfen Kinder haben.
|
# class Meta:
|
||||||
- Ein Schüler darf sich selbst nicht als Kind hinzufügen.
|
# verbose_name_plural = "Personen"
|
||||||
"""
|
#
|
||||||
super().clean() # ruft die Validierungen der Elternklasse auf (hier User/Model)
|
# def __str__(self):
|
||||||
|
# if self.children.exists():
|
||||||
if self.rolle in ('mitarbeiter', 'chef', 'lehrer'):
|
# # Wir sammeln alle Kinder‑Namen in einem String, z. B. mit Komma getrennt.
|
||||||
# Nicht‑Eltern dürfen keine Kinder besitzen
|
# children_names = ', '.join([c.name for c in self.children.all()])
|
||||||
if self.children.exists():
|
# return f"{self.user.username} ({children_names}) ({self.rolle})"
|
||||||
raise ValidationError(
|
# else:
|
||||||
"Nur Benutzer mit der Rolle 'Eltern' "
|
# return f"{self.user.username} ({self.rolle})"
|
||||||
"dürfen Schüler zuordnen (children)."
|
#
|
||||||
)
|
# def clean(self):
|
||||||
else:
|
# """
|
||||||
# Eltern: Verhindere, dass sie sich selbst als Kind eintragen
|
# Überprüfung für die ManyToMany-Beziehung ``children``:
|
||||||
if self in self.children.all():
|
# - Nur Personen mit der Rolle 'mitarbeiter' oder 'chef' (Eltern) dürfen Kinder haben.
|
||||||
raise ValidationError("Ein Benutzer darf nicht gleichzeitig Elternteil und eigener Kind sein.")
|
# - Ein Schüler darf sich selbst nicht als Kind hinzufügen.
|
||||||
|
# """
|
||||||
|
# super().clean() # ruft die Validierungen der Elternklasse auf (hier User/Model)
|
||||||
|
#
|
||||||
|
# if self.rolle in ('mitarbeiter', 'chef', 'lehrer'):
|
||||||
|
# # Nicht‑Eltern dürfen keine Kinder besitzen
|
||||||
|
# if self.children.exists():
|
||||||
|
# raise ValidationError(
|
||||||
|
# "Nur Benutzer mit der Rolle 'Eltern' "
|
||||||
|
# "dürfen Schüler zuordnen (children)."
|
||||||
|
# )
|
||||||
|
# else:
|
||||||
|
# # Eltern: Verhindere, dass sie sich selbst als Kind eintragen
|
||||||
|
# if self in self.children.all():
|
||||||
|
# raise ValidationError("Ein Benutzer darf nicht gleichzeitig Elternteil und eigener Kind sein.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Bestell-Zusammenfassung</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body class="bg-light">
|
||||||
|
|
||||||
|
<div class="container mt-5">
|
||||||
|
<!-- PAGER & DATUM -->
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h2 class="mb-3">Bestell-Check für den <span class="text-success">{{ target_date|date:"d.m.Y" }}</span></h2>
|
||||||
|
<div class="btn-group">
|
||||||
|
<a href="?datum={{ prev_date }}" class="btn btn-outline-secondary {% if not prev_date %}disabled{% endif %}">« Vorheriger Tag</a>
|
||||||
|
<a href="?" class="btn btn-primary">Heute</a>
|
||||||
|
<a href="?datum={{ next_date }}" class="btn btn-outline-secondary {% if not next_date %}disabled{% endif %}">Nächster Tag »</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DASHBOARD CARDS -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card bg-white shadow border-start border-primary border-5">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="text-muted">Gesamtanzahl Bestellungen</h6>
|
||||||
|
<h3 class="mb-0">{{ total_bestellungen }}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card bg-white shadow border-start border-success border-5">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="text-muted">Erwarteter Umsatz</h6>
|
||||||
|
<h3 class="mb-0">{{ total_umsatz|floatformat:2 }} €</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DETAILS TABELLE -->
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-header bg-dark text-white">Details pro Gericht</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<table class="table table-hover mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Gericht</th>
|
||||||
|
<th>Kategorie</th>
|
||||||
|
<th class="text-center">Menge</th>
|
||||||
|
<th class="text-end">Umsatz</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for stat in summary_stats %}
|
||||||
|
<tr>
|
||||||
|
<td><strong>{{ stat.menue__gericht__name }}</strong></td>
|
||||||
|
<td><span class="badge bg-info text-dark">{{ stat.menue__gericht__kategorie__name }}</span></td>
|
||||||
|
<td class="text-center">{{ stat.anzahl }}</td>
|
||||||
|
<td class="text-end">{{ stat.umsatz|floatformat:2 }} €</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="text-center py-4 text-muted">Keine Bestellungen für diesen Tag vorhanden.</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 text-end">
|
||||||
|
<a href="{% url 'speisekarte' %}" class="btn btn-link text-decoration-none">← Zurück zur Speisekarte</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Mensa-Speisekarte</title>
|
||||||
|
<!-- Bootstrap CSS für schnelles Styling -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body class="bg-light">
|
||||||
|
|
||||||
|
<div class="container mt-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class='card-header bg-primary text-white text-center py-3'>
|
||||||
|
<h1><i class="bi bi-egg-fried"></i> Unsere Speisekarte</h1>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th>Gericht</th>
|
||||||
|
<th>Kategorie</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for gericht in alle_gerichte %}
|
||||||
|
<tr>
|
||||||
|
<td><strong>{{ gericht.name }}</strong></td>
|
||||||
|
<td><span class="badge bg-info text-dark">{{ gericht.kategorie.name }}</span></td>
|
||||||
|
<td>
|
||||||
|
{% if gericht.ist_dauerangebot %}
|
||||||
|
<span class="badge bg-success">Dauerangebot</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">Tagesangebot</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="3" class="text-center text-muted">Keine Gerichte im System gefunden.</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Mensa Speiseplan</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body class="bg-light">
|
||||||
|
|
||||||
|
<div class="container mt-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
|
||||||
|
<!-- PAGER & DATUM ANZEIGE -->
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h2 class="mb-3">Speiseplan für den <br><span class="text-primary">{{ target_date|date:"d.m.Y" }}</span></h2>
|
||||||
|
|
||||||
|
<div class="btn-group" role="group" aria-label="Datum Navigation">
|
||||||
|
<!-- Button Zurück -->
|
||||||
|
{% if prev_date %}
|
||||||
|
<a href="?datum={{ prev_date }}" class="btn btn-outline-secondary">« Vorheriger Tag</a>
|
||||||
|
{% else %}
|
||||||
|
<button class="btn btn-outline-light text-muted" disabled>« Vorheriger</button>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Button Heute (Reset) -->
|
||||||
|
<a href="?" class="btn btn-primary">Heute</a>
|
||||||
|
|
||||||
|
<!-- Button Weiter -->
|
||||||
|
{% if next_date %}
|
||||||
|
<a href="?datum={{ next_date }}" class="btn btn-outline-secondary">Nächster Tag »</a>
|
||||||
|
{% else %}
|
||||||
|
<button class="btn btn-outline-light text-muted" disabled>Nächster »</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TABELLE DER TAGESMENÜS -->
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-header bg-dark text-white">Tagesmenüs</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<table class="table table-striped mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Gericht</th>
|
||||||
|
<th>Kategorie</th>
|
||||||
|
<th>Preis</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for menue in menues_day %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ menue.gericht.name }}</td>
|
||||||
|
<td><span class="badge bg-info text-dark">{{ menue.gericht.kategorie.name }}</span></td>
|
||||||
|
<td>{{ menue.preis|floatformat:2 }} €</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="3" class="text-center py-3 text-muted">Keine Tagesmenüs für diesen Tag geplant.</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TABELLE DER DAUERANGEBOTE -->
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-header bg-success text-white">Immer verfügbar (Dauerangebote)</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
{% for gericht in dauerangebote %}
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
{{ gericht.name }}
|
||||||
|
<span class="badge bg-success rounded-pill">Verfügbar</span>
|
||||||
|
</li>
|
||||||
|
{% empty %}
|
||||||
|
<li class="list-group-item text-muted">Momentan keine Dauerangebote.</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from .views import GerichtListView
|
||||||
|
from .views import SpeiseplanView # Achte auf den neuen Klassennamen!
|
||||||
|
from .views import BestellSummaryView
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('speisekarte/', GerichtListView.as_view(), name='speisekarte'),
|
||||||
|
path('speiseplan/', SpeiseplanView.as_view(), name='speiseplan'),
|
||||||
|
path('bestellungen/summary/', BestellSummaryView.as_view(), name='bestell_summary'),
|
||||||
|
]
|
||||||
@@ -1,3 +1,101 @@
|
|||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
|
||||||
# Create your views here.
|
# Create your views here.
|
||||||
|
from django.views.generic import ListView
|
||||||
|
from .models import Gericht
|
||||||
|
|
||||||
|
class GerichtListView(ListView):
|
||||||
|
model = Gericht
|
||||||
|
template_name = 'mensa_app/gericht_liste.html' # Der Pfad zum Template
|
||||||
|
context_object_name = 'alle_gerichte' # Der Name, den wir im Template nutzen
|
||||||
|
|
||||||
|
from django.views.generic import TemplateView
|
||||||
|
from django.utils import timezone
|
||||||
|
from datetime import datetime
|
||||||
|
from .models import Menue, Gericht, SpeiseplanTag
|
||||||
|
|
||||||
|
class SpeiseplanView(TemplateView):
|
||||||
|
template_name = 'mensa_app/speiseplan.html'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
# 1. Datum aus der URL holen (z.B. ?datum=2023-10-27)
|
||||||
|
# Wenn kein Datum angegeben ist, nehmen wir heute.
|
||||||
|
date_str = self.request.GET.get('datum')
|
||||||
|
if date_str:
|
||||||
|
try:
|
||||||
|
target_date = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||||
|
except ValueError:
|
||||||
|
target_date = timezone.glob.now().date()
|
||||||
|
else:
|
||||||
|
target_date = timezone.now().date()
|
||||||
|
|
||||||
|
# 2. Menüs für diesen spezifischen Tag laden
|
||||||
|
# Wir suchen alle Menüs, deren Tag das target_date hat
|
||||||
|
context['target_date'] = target_date
|
||||||
|
context['menues_day'] = Menue.objects.filter(tag__datum=target_date)
|
||||||
|
|
||||||
|
# 3. Dauerangebote laden (unabhängig vom Tag)
|
||||||
|
context['dauerangebote'] = Gericht.objects.filter(ist_dauerangebot=True)
|
||||||
|
|
||||||
|
# 4. Pager-Logik: Vorherigen und nächsten Tag finden
|
||||||
|
# Wir suchen in der Tabelle SpeiseplanTag nach dem Tag davor/danach
|
||||||
|
prev_tag = SpeiseplanTag.objects.filter(datum__lt=target_date).order_by('-datum').first()
|
||||||
|
next_tag = SpeiseplanTag.objects.filter(datum__gt=target_date).order_by('datum').first()
|
||||||
|
|
||||||
|
context['prev_date'] = prev_tag.datum.strftime('%Y-%m-%d') if prev_tag else None
|
||||||
|
context['next_date'] = next_tag.datum.strftime('%Y-%m-%d') if next_tag else None
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
from django.views.generic import TemplateView
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.db.models import Count, Sum
|
||||||
|
from datetime import datetime
|
||||||
|
from .models import Bestellung, SpeiseplanTag, Gericht, Menue
|
||||||
|
|
||||||
|
class BestellSummaryView(TemplateView):
|
||||||
|
template_name = 'mensa_app/bestell_summary.html'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
# 1. Datum aus der URL holen (wie beim Speiseplan)
|
||||||
|
date_str = self.request.GET. get('datum')
|
||||||
|
if date_str:
|
||||||
|
try:
|
||||||
|
target_date = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||||
|
except ValueError:
|
||||||
|
target_date = timezone.now().date()
|
||||||
|
else:
|
||||||
|
target_date = timezone.now().date()
|
||||||
|
|
||||||
|
context['target_date'] = target_date
|
||||||
|
|
||||||
|
# 2. Aggregation: Bestellungen nach Gericht gruppieren
|
||||||
|
# Wir suchen alle Bestellungen, deren Menü am target_date stattfindet
|
||||||
|
summary_stats = (
|
||||||
|
Bestellung.objects.filter(menue__tag__datum=target_date)
|
||||||
|
.values('menue__gericht__name', 'menue__gericht__kategorie__name') # Gruppierung nach Name & Kategorie
|
||||||
|
.annotate(
|
||||||
|
anzahl=Count('id'), # Wie viele wurden bestellt?
|
||||||
|
umsatz=Sum('menue__preis') # Was macht das für einen Umsatz?
|
||||||
|
)
|
||||||
|
.order_by('menue__gericht__name')
|
||||||
|
)
|
||||||
|
context['summary_stats'] = summary_stats
|
||||||
|
|
||||||
|
# 3. Pager-Logik (identisch mit dem Speiseplan-View)
|
||||||
|
prev_tag = SpeiseplanTag.objects.filter(datum__lt=target_date).order_by('-datum').first()
|
||||||
|
next_tag = SpeiseplanTag.objects.filter(datum__gt=target_date).order_by('datum').first()
|
||||||
|
|
||||||
|
context['prev_date'] = prev_tag.datum.strftime('%Y-%m-%d') if prev_tag else None
|
||||||
|
context['next_date'] = next_tag.datum.strftime('%Y-%m-%d') if next_tag else None
|
||||||
|
|
||||||
|
# 4. Gesamtzahlen für die Übersicht
|
||||||
|
context['total_bestellungen'] = Bestellung.objects.filter(menue__tag__datum=target_date).count()
|
||||||
|
context['total_umsatz'] = Bestellung.objects.filter(menue__tag__datum=target_date).aggregate(Sum('menue__preis'))['menue__preis__sum'] or 0
|
||||||
|
|
||||||
|
return context
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3
-1
@@ -15,10 +15,12 @@ Including another URLconf
|
|||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
"""
|
"""
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path
|
from django.urls import path, include
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
|
path('', include('mensa_app.urls')), # Schaltet die App-URLs frei
|
||||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user