Compare commits

..

3 Commits

Author SHA1 Message Date
847cac4bba UI improvements
* Replace registration table with cards on smaller devices
  * Use number inputs for registrations
2025-10-07 21:26:17 +02:00
102e03b546 Make page responsive and improve event listing 2025-10-07 21:08:41 +02:00
1d29e954b8 Work that has already been maxed will not show up in the dropdown 2025-10-07 20:42:41 +02:00
5 changed files with 116 additions and 61 deletions

View File

@@ -164,6 +164,7 @@ async def add_team_registration(request: Request, event_id: int, session: Sessio
statement = select(TeamRegistration).where(
TeamRegistration.person_name == person, TeamRegistration.work_type == work_type
)
# if the person has already registered for the same work type, just ignore
if session.exec(statement).one_or_none() is None:
registration = TeamRegistration(
person_name=person,

View File

@@ -1,8 +1,10 @@
import typing
from datetime import datetime
from typing import Literal
from sqlmodel import Field, Relationship, SQLModel, String
WorkTypes = typing.Literal["cooking", "dishes", "tables"]
class Event(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
@@ -19,20 +21,45 @@ class Event(SQLModel, table=True):
team_dishes_min: int = 3
team_dishes_max: int = 5
# Todo: Rename to "table"
team_prep_min: int = 1
team_prep_max: int = 1
registrations: list["Registration"] = Relationship()
team: list["TeamRegistration"] = Relationship()
def team_min_reached(self, work_type: WorkTypes):
threshold = {
"cooking": self.team_cooking_min,
"dishes": self.team_dishes_min,
"tables": self.team_prep_min,
}[work_type]
return sum(1 for t in self.team if t.work_type == work_type) >= threshold
def team_max_reached(self, work_type: WorkTypes):
threshold = {
"cooking": self.team_cooking_max,
"dishes": self.team_dishes_max,
"tables": self.team_prep_max,
}[work_type]
return sum(1 for t in self.team if t.work_type == work_type) >= threshold
def all_teams_min(self):
return all(
self.team_min_reached(work_type) for work_type in typing.get_args(WorkTypes)
)
def all_teams_max(self):
return all(
self.team_max_reached(work_type) for work_type in typing.get_args(WorkTypes)
)
class TeamRegistration(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
event_id: int | None = Field(default=None, foreign_key="event.id")
person_name: str = Field(nullable=False)
work_type: Literal["cooking", "dishes", "tables"] = Field(
nullable=False, sa_type=String
)
work_type: WorkTypes = Field(nullable=False, sa_type=String)
comment: str | None

View File

@@ -2,13 +2,12 @@
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Allmende Essen</title>
<link href="/static/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB">
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
<link href="/static/icons/bootstrap-icons.min.css" rel="stylesheet">
</head>
<body class="p-3 m-0 border-0 bd-example m-0 border-0">
<body class="p-3 m-0 border-0 bd-example">
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="/">
@@ -38,7 +37,6 @@
{% block content %}{% endblock %}
</div>
<script src="/static/js/bootstrap.bundle.min.js"
integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI"></script>
<script src="/static/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -18,21 +18,19 @@
<hr class="hr"/>
<p class="h3">Anmeldungen</p>
<div class="container">
<div class="row">
<div class="row m-2">
<div class="col-md-4 d-flex justify-content-center align-items-center flex-column">
<!-- Button trigger modal -->
<button type="button" class="btn btn-primary mb-2" data-bs-toggle="modal" data-bs-target="#registration">
<button type="button" class="btn btn-primary mb-2 w-100" data-bs-toggle="modal" data-bs-target="#registration">
Anmeldung hinzufügen
</button>
<button type="button" class="btn btn-primary mb-2" data-bs-toggle="modal" data-bs-target="#teamRegistration">
<button type="button" class="btn btn-primary mb-2 w-100" {% if event.all_teams_max() %}disabled{% endif %} data-bs-toggle="modal" data-bs-target="#teamRegistration">
Dienst übernehmen
</button>
{% if event.recipe_link %}
<div class="mb-3">
<a href="{{ event.recipe_link }}" class="btn btn-outline-primary" target="_blank">
<a href="{{ event.recipe_link }}" class="btn btn-outline-primary mb-2 w-100" target="_blank">
<i class="bi bi-book"></i> Original Rezept ansehen
</a>
</div>
{% endif %}
</div>
<div class="col-md-4">
@@ -41,7 +39,7 @@
<h5 class="card-title">Dienste</h5>
<div class="row">
<div class="col-md-4">
Kochen: {% if event.team | selectattr("work_type", "equalto", "cooking") | list | count >= 3 %}✅{% endif %}
Kochen: {% if event.team_min_reached("cooking") %}✅{% endif %}
</div>
<div class="col-md-8">
{{ teamEntries(event, "cooking") }}
@@ -50,7 +48,7 @@
<hr/>
<div class="row">
<div class="col-md-4">
Spülen: {% if event.team | selectattr("work_type", "equalto", "dishes") | list | count >= 3 %}✅{% endif %}
Spülen: {% if event.team_min_reached("dishes") %}✅{% endif %}
</div>
<div class="col-md-8">
{{ teamEntries(event, "dishes") }}
@@ -59,7 +57,7 @@
<hr/>
<div class="row">
<div class="col-md-4">
Tische vorbereiten: {% if event.team | selectattr("work_type", "equalto", "tables") | list | count >= 1 %}✅{% endif %}
Tische vorbereiten: {% if event.team_min_reached("tables") %}✅{% endif %}
</div>
<div class="col-md-8">
{{ teamEntries(event, "tables") }}
@@ -95,6 +93,8 @@
</div>
</div>
<!-- Desktop table view -->
<div class="d-none d-md-block">
<table class="table">
<thead>
<tr>
@@ -118,6 +118,37 @@
</tbody>
</table>
</div>
<!-- Mobile card view -->
<div class="d-md-none">
{% for reg in event.registrations %}
<div class="card mb-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-2">
<h5 class="card-title mb-0">{{ reg.household.name }}</h5>
<a href="/event/{{event.id}}/registration/{{reg.household_id}}/delete" class="text-danger">
<i class="bi bi-trash"></i>
</a>
</div>
<div class="row mt-3">
<div class="col-4 text-center">
<div class="text-muted small">Erwachsene</div>
<div class="fw-bold">{{ reg.num_adult_meals }}</div>
</div>
<div class="col-4 text-center">
<div class="text-muted small">Kinder</div>
<div class="fw-bold">{{ reg.num_children_meals }}</div>
</div>
<div class="col-4 text-center">
<div class="text-muted small">Kleinkinder</div>
<div class="fw-bold">{{ reg.num_small_children_meals }}</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Modal -->
<div class="modal fade" id="registration" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1"
aria-labelledby="staticBackdropLabel" aria-hidden="true">
@@ -143,18 +174,18 @@
<div class="row">
<div class="col">
<label for="InputAdults" class="form-label">Anzahl Erwachsene</label>
<input name="numAdults" id="InputAdults" type="text" class="form-control"
aria-label="Anzahl Erwachsene">
<input name="numAdults" id="InputAdults" type="number" class="form-control"
aria-label="Anzahl Erwachsene" min="0" step="1" inputmode="numeric">
</div>
<div class="col">
<label for="InputKids" class="form-label">Anzahl Kinder >7 </label>
<input name="numKids" id="InputKids" type="text" class="form-control"
aria-label="Anzahl Kinder >7">
<input name="numKids" id="InputKids" type="number" class="form-control"
aria-label="Anzahl Kinder >7" min="0" step="1" inputmode="numeric">
</div>
<div class="col">
<label for="InputSmallKids" class="form-label">Anzahl Kinder <7 </label>
<input name="numSmallKids" id="InputSmallKids" type="text" class="form-control"
aria-label="Anzahl Kinder <7">
<input name="numSmallKids" id="InputSmallKids" type="number" class="form-control"
aria-label="Anzahl Kinder <7" min="0" step="1" inputmode="numeric">
</div>
</div>
@@ -188,9 +219,9 @@
<div class="col-md-6">
<label for="workType" class="form-label">Dienst-Art</label>
<select id="workType" name="workType" class="form-select" aria-label="Multiple select example">
<option value="cooking">Kochen</option>
<option value="dishes">Spülen</option>
<option value="tables">Tische vorbereiten</option>
{% if not event.team_max_reached("cooking") %}<option value="cooking">Kochen</option>{% endif %}
{% if not event.team_max_reached("dishes") %}<option value="dishes">Spülen</option>{% endif %}
{% if not event.team_max_reached("tables") %}<option value="tables">Tische vorbereiten</option>{% endif %}
</select>
</div>
</div>

View File

@@ -8,22 +8,20 @@
</a>
</div>
</div>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
{% for event in events %}
<div class="row">
<div class="col">
<div class="card p-3 m-3">
<div class="card h-100 shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title">{{ event.title }}</h5>
<h5 class="text-muted"><i class="bi bi-calendar"></i> {{ event.event_time.strftime('%A, %d.%m.%Y')
<h5 class="card-title mb-1">{{ event.title }}</h5>
<p class="text-muted mb-3"><i class="bi bi-calendar"></i> {{ event.event_time.strftime('%A, %d.%m.%Y')
}}
</h5>
</div>
</p>
<p class="card-text">{{ event.description }}</p>
<a href="event/{{ event.id }}" class="btn {% if event.registration_deadline > now %}btn-primary{% else %}btn-secondary{% endif %}">{% if event.registration_deadline > now %}Zur Anmeldung{% else %}Details ansehen{% endif %}</a>
</div>
<a href="event/{{ event.id }}" class="btn btn-sm {% if event.registration_deadline > now %}btn-primary{% else %}btn-secondary{% endif %}">{% if event.registration_deadline > now %}Zur Anmeldung{% else %}Details ansehen{% endif %}</a>
</div>
</div>
</div>
{% endfor %}
</div>
{% endblock %}