Move meal manager into it's own package
This commit is contained in:
0
src/meal_manager/__init__.py
Normal file
0
src/meal_manager/__init__.py
Normal file
43
src/meal_manager/alembic.ini
Normal file
43
src/meal_manager/alembic.ini
Normal file
@@ -0,0 +1,43 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
|
||||
# database URL. This is consumed by the user-maintained env.py script only.
|
||||
# other means of configuring database URLs may be customized within the env.py
|
||||
# file.
|
||||
sqlalchemy.url = sqlite:///database.db
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARNING
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARNING
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
1
src/meal_manager/alembic/README
Normal file
1
src/meal_manager/alembic/README
Normal file
@@ -0,0 +1 @@
|
||||
pyproject configuration, based on the generic configuration.
|
||||
76
src/meal_manager/alembic/env.py
Normal file
76
src/meal_manager/alembic/env.py
Normal file
@@ -0,0 +1,76 @@
|
||||
from logging.config import fileConfig
|
||||
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
from meal_manager.models import Base
|
||||
|
||||
target_metadata = Base.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(connection=connection, target_metadata=target_metadata)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
28
src/meal_manager/alembic/script.py.mako
Normal file
28
src/meal_manager/alembic/script.py.mako
Normal file
@@ -0,0 +1,28 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
${downgrades if downgrades else "pass"}
|
||||
@@ -0,0 +1,110 @@
|
||||
"""inital revision
|
||||
|
||||
Revision ID: 299a83240036
|
||||
Revises:
|
||||
Create Date: 2025-10-12 20:46:13.452705
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "299a83240036"
|
||||
down_revision: Union[str, Sequence[str], None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"event",
|
||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("title", sa.String(), nullable=False),
|
||||
sa.Column("event_time", sa.DateTime(), nullable=False),
|
||||
sa.Column("registration_deadline", sa.DateTime(), nullable=False),
|
||||
sa.Column("description", sa.String(), nullable=False),
|
||||
sa.Column("recipe_link", sa.String(), nullable=False),
|
||||
sa.Column("team_cooking_min", sa.Integer(), nullable=False),
|
||||
sa.Column("team_cooking_max", sa.Integer(), nullable=False),
|
||||
sa.Column("team_dishes_min", sa.Integer(), nullable=False),
|
||||
sa.Column("team_dishes_max", sa.Integer(), nullable=False),
|
||||
sa.Column("team_prep_min", sa.Integer(), nullable=False),
|
||||
sa.Column("team_prep_max", sa.Integer(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_table(
|
||||
"household",
|
||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("name", sa.String(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_table(
|
||||
"registration",
|
||||
sa.Column("event_id", sa.Integer(), nullable=False),
|
||||
sa.Column("household_id", sa.Integer(), nullable=False),
|
||||
sa.Column("num_adult_meals", sa.Integer(), nullable=False),
|
||||
sa.Column("num_children_meals", sa.Integer(), nullable=False),
|
||||
sa.Column("num_small_children_meals", sa.Integer(), nullable=False),
|
||||
sa.Column("comment", sa.String(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["event_id"],
|
||||
["event.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["household_id"],
|
||||
["household.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("event_id", "household_id"),
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"subscription",
|
||||
sa.Column("household_id", sa.Integer(), nullable=False),
|
||||
sa.Column("num_adult_meals", sa.Integer(), nullable=False),
|
||||
sa.Column("num_children_meals", sa.Integer(), nullable=False),
|
||||
sa.Column("num_small_children_meals", sa.Integer(), nullable=False),
|
||||
sa.Column("comment", sa.String(), nullable=True),
|
||||
sa.Column("last_modified", sa.DateTime(), nullable=False),
|
||||
sa.Column("monday", sa.Boolean(), nullable=False),
|
||||
sa.Column("tuesday", sa.Boolean(), nullable=False),
|
||||
sa.Column("wednesday", sa.Boolean(), nullable=False),
|
||||
sa.Column("thursday", sa.Boolean(), nullable=False),
|
||||
sa.Column("friday", sa.Boolean(), nullable=False),
|
||||
sa.Column("saturday", sa.Boolean(), nullable=False),
|
||||
sa.Column("sunday", sa.Boolean(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["household_id"],
|
||||
["household.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("household_id"),
|
||||
)
|
||||
op.create_table(
|
||||
"team_registration",
|
||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("event_id", sa.Integer(), nullable=False),
|
||||
sa.Column("person_name", sa.String(), nullable=False),
|
||||
sa.Column("work_type", sa.Text(), nullable=False),
|
||||
sa.Column("comment", sa.String(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["event_id"],
|
||||
["event.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table("team_registration")
|
||||
op.drop_table("subscription")
|
||||
op.drop_table("registration")
|
||||
op.drop_table("household")
|
||||
op.drop_table("event")
|
||||
# ### end Alembic commands ###
|
||||
289
src/meal_manager/main.py
Normal file
289
src/meal_manager/main.py
Normal file
@@ -0,0 +1,289 @@
|
||||
import locale
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Annotated
|
||||
|
||||
import starlette.status as status
|
||||
from fastapi import Depends, FastAPI, Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy import create_engine, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from meal_manager.models import (
|
||||
Base,
|
||||
Event,
|
||||
Household,
|
||||
Registration,
|
||||
Subscription,
|
||||
TeamRegistration,
|
||||
)
|
||||
|
||||
sqlite_file_name = "database.db"
|
||||
sqlite_url = f"sqlite:///{sqlite_file_name}"
|
||||
|
||||
connect_args = {"check_same_thread": False}
|
||||
engine = create_engine(sqlite_url, connect_args=connect_args)
|
||||
|
||||
locale.setlocale(locale.LC_ALL, "de_DE.UTF-8")
|
||||
|
||||
|
||||
def get_session():
|
||||
with Session(engine) as session:
|
||||
yield session
|
||||
|
||||
|
||||
def create_db_and_tables():
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def on_startup(app_: FastAPI):
|
||||
create_db_and_tables()
|
||||
yield
|
||||
|
||||
|
||||
app = FastAPI(lifespan=on_startup)
|
||||
app.mount("/static", StaticFiles(directory="src/meal_manager/static"), name="static")
|
||||
|
||||
templates = Jinja2Templates(directory="src/meal_manager/templates")
|
||||
|
||||
SessionDep = Annotated[Session, Depends(get_session)]
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def index(request: Request, session: SessionDep):
|
||||
"""Displays coming events and a button to register new ones"""
|
||||
now = datetime.now()
|
||||
# TODO: Once we refactored to use SQLAlchemy directly, we can probably do a nicer filtering on the date alone
|
||||
statement = (
|
||||
select(Event)
|
||||
.order_by(Event.event_time)
|
||||
.where(Event.event_time >= now - timedelta(days=1))
|
||||
)
|
||||
events = session.scalars(statement)
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name="index.html",
|
||||
context={"events": events, "current_page": "home", "now": now},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/past_events")
|
||||
async def past_events(request: Request, session: SessionDep):
|
||||
now = datetime.now()
|
||||
# TODO: Once we refactored to use SQLAlchemy directly, we can probably do a nicer filtering on the date alone
|
||||
statement = (
|
||||
select(Event)
|
||||
.order_by(Event.event_time)
|
||||
.where(Event.event_time < now - timedelta(days=1))
|
||||
)
|
||||
events = session.scalars(statement)
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name="index.html",
|
||||
context={"events": events, "current_page": "past", "now": now},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/subscribe")
|
||||
async def subscribe(request: Request, session: SessionDep):
|
||||
statement = select(Household)
|
||||
households = session.scalars(statement)
|
||||
|
||||
subscriptions = session.scalars(select(Subscription))
|
||||
|
||||
# filter out households with existing subscriptions
|
||||
households = [
|
||||
h for h in households if h.id not in [sub.household_id for sub in subscriptions]
|
||||
]
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name="subscribe.html",
|
||||
context={"households": households, "subscriptions": subscriptions},
|
||||
)
|
||||
|
||||
|
||||
@app.post("/subscribe")
|
||||
async def add_subscribe(request: Request, session: SessionDep):
|
||||
form_data = await request.form()
|
||||
# TODO: Make this return a nicer error message
|
||||
try:
|
||||
num_adult_meals = int(form_data["numAdults"]) if form_data["numAdults"] else 0
|
||||
num_children_meals = int(form_data["numKids"]) if form_data["numKids"] else 0
|
||||
num_small_children_meals = (
|
||||
int(form_data["numSmallKids"]) if form_data["numSmallKids"] else 0
|
||||
)
|
||||
except ValueError:
|
||||
raise ValueError("All number fields must be integers")
|
||||
|
||||
subscription = Subscription(
|
||||
household_id=form_data["household"],
|
||||
num_adult_meals=num_adult_meals,
|
||||
num_children_meals=num_children_meals,
|
||||
num_small_children_meals=num_small_children_meals,
|
||||
)
|
||||
|
||||
selected_days = form_data.getlist("days")
|
||||
if selected_days:
|
||||
subscription.monday = "1" in selected_days
|
||||
subscription.tuesday = "2" in selected_days
|
||||
subscription.wednesday = "3" in selected_days
|
||||
subscription.thursday = "4" in selected_days
|
||||
subscription.friday = "5" in selected_days
|
||||
subscription.saturday = "6" in selected_days
|
||||
subscription.sunday = "7" in selected_days
|
||||
|
||||
session.add(subscription)
|
||||
session.commit()
|
||||
|
||||
return RedirectResponse(url="/subscribe", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
|
||||
@app.get("/subscribe/{household_id}/delete")
|
||||
async def delete_subscription(request: Request, session: SessionDep, household_id: int):
|
||||
|
||||
statement = select(Subscription).where(Subscription.household_id == household_id)
|
||||
sub = session.scalars(statement).one()
|
||||
|
||||
session.delete(sub)
|
||||
session.commit()
|
||||
|
||||
return RedirectResponse(url="/subscribe", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
|
||||
@app.get("/event/add")
|
||||
async def add_event_form(request: Request, session: SessionDep):
|
||||
return templates.TemplateResponse(request=request, name="add_event.html")
|
||||
|
||||
|
||||
@app.post("/event/add")
|
||||
async def add_event(request: Request, session: SessionDep):
|
||||
form_data = await request.form()
|
||||
|
||||
event_time = datetime.fromisoformat(form_data["eventTime"])
|
||||
registration_deadline = form_data.get("registrationDeadline")
|
||||
if not registration_deadline:
|
||||
# Find the last Sunday before event_time
|
||||
deadline = event_time
|
||||
while deadline.weekday() != 6: # 6 represents Sunday
|
||||
deadline = deadline.replace(day=deadline.day - 1)
|
||||
registration_deadline = deadline.replace(
|
||||
hour=19, minute=30, second=0, microsecond=0
|
||||
)
|
||||
else:
|
||||
registration_deadline = datetime.fromisoformat(registration_deadline)
|
||||
|
||||
event = Event(
|
||||
title=form_data["eventName"],
|
||||
event_time=event_time,
|
||||
registration_deadline=registration_deadline,
|
||||
description=form_data.get("eventDescription"),
|
||||
recipe_link=form_data.get("recipeLink"),
|
||||
)
|
||||
session.add(event)
|
||||
session.commit()
|
||||
return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
|
||||
@app.get("/event/{event_id}")
|
||||
async def read_event(request: Request, event_id: int, session: SessionDep):
|
||||
statement = select(Event).where(Event.id == event_id)
|
||||
event = session.scalars(statement).one()
|
||||
|
||||
statement = select(Household)
|
||||
households = session.scalars(statement)
|
||||
|
||||
# filter out households with existing registrations
|
||||
households = [
|
||||
h
|
||||
for h in households
|
||||
if h.id not in [reg.household_id for reg in event.registrations]
|
||||
]
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name="event.html",
|
||||
context={"event": event, "households": households, "now": datetime.now()},
|
||||
)
|
||||
|
||||
|
||||
@app.post("/event/{event_id}/register")
|
||||
async def add_registration(request: Request, event_id: int, session: SessionDep):
|
||||
form_data = await request.form()
|
||||
|
||||
# TODO: Make this return a nicer error message
|
||||
try:
|
||||
num_adult_meals = int(form_data["numAdults"]) if form_data["numAdults"] else 0
|
||||
num_children_meals = int(form_data["numKids"]) if form_data["numKids"] else 0
|
||||
num_small_children_meals = (
|
||||
int(form_data["numSmallKids"]) if form_data["numSmallKids"] else 0
|
||||
)
|
||||
except ValueError:
|
||||
raise ValueError("All number fields must be integers")
|
||||
|
||||
registration = Registration(
|
||||
household_id=form_data["household"],
|
||||
event_id=event_id,
|
||||
num_adult_meals=num_adult_meals,
|
||||
num_children_meals=num_children_meals,
|
||||
num_small_children_meals=num_small_children_meals,
|
||||
comment=form_data["comment"],
|
||||
)
|
||||
session.add(registration)
|
||||
session.commit()
|
||||
return RedirectResponse(url=f"/event/{event_id}", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
|
||||
@app.get("/event/{event_id}/registration/{household_id}/delete")
|
||||
async def delete_registration(
|
||||
request: Request, event_id: int, household_id: int, session: SessionDep
|
||||
):
|
||||
"""
|
||||
Deletes a registration record for a specific household at a given event. This endpoint
|
||||
handles the removal of the registration, commits the change to the database, and
|
||||
redirects the user to the event page.
|
||||
"""
|
||||
statement = select(Registration).where(
|
||||
Registration.household_id == household_id, Registration.event_id == event_id
|
||||
)
|
||||
session.delete(session.scalars(statement).one())
|
||||
session.commit()
|
||||
return RedirectResponse(url=f"/event/{event_id}", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
|
||||
@app.post("/event/{event_id}/register_team")
|
||||
async def add_team_registration(request: Request, event_id: int, session: SessionDep):
|
||||
form_data = await request.form()
|
||||
|
||||
person = form_data["personName"].strip()
|
||||
work_type = form_data["workType"]
|
||||
|
||||
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.scalars(statement).one_or_none() is None:
|
||||
registration = TeamRegistration(
|
||||
person_name=person,
|
||||
event_id=event_id,
|
||||
work_type=form_data["workType"],
|
||||
)
|
||||
session.add(registration)
|
||||
session.commit()
|
||||
return RedirectResponse(url=f"/event/{event_id}", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
|
||||
@app.get("/event/{event_id}/register_team/{entry_id}/delete")
|
||||
async def delete_team_registration(
|
||||
request: Request,
|
||||
event_id: int,
|
||||
entry_id: int,
|
||||
session: SessionDep,
|
||||
):
|
||||
statement = select(TeamRegistration).where(TeamRegistration.id == entry_id)
|
||||
session.delete(session.scalars(statement).one())
|
||||
session.commit()
|
||||
return RedirectResponse(url=f"/event/{event_id}", status_code=status.HTTP_302_FOUND)
|
||||
143
src/meal_manager/models.py
Normal file
143
src/meal_manager/models.py
Normal file
@@ -0,0 +1,143 @@
|
||||
import typing
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||
from sqlalchemy.types import Text
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
WorkTypes = typing.Literal["cooking", "dishes", "tables"]
|
||||
|
||||
|
||||
class Event(Base):
|
||||
__tablename__ = "event"
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
title: Mapped[str] = mapped_column(nullable=False)
|
||||
event_time: Mapped[datetime] = mapped_column(nullable=False)
|
||||
registration_deadline: Mapped[datetime] = mapped_column(nullable=False)
|
||||
description: Mapped[str] = mapped_column()
|
||||
recipe_link: Mapped[str] = mapped_column()
|
||||
|
||||
# Min and max number of people needed for cooking, doing the dishes and preparing the tables
|
||||
team_cooking_min: Mapped[int] = mapped_column(default=3, nullable=False)
|
||||
team_cooking_max: Mapped[int] = mapped_column(default=5, nullable=False)
|
||||
|
||||
team_dishes_min: Mapped[int] = mapped_column(default=3, nullable=False)
|
||||
team_dishes_max: Mapped[int] = mapped_column(default=5, nullable=False)
|
||||
|
||||
# Todo: Rename to "table"
|
||||
team_prep_min: Mapped[int] = mapped_column(default=1, nullable=False)
|
||||
team_prep_max: Mapped[int] = mapped_column(default=1, nullable=False)
|
||||
|
||||
registrations: Mapped[list["Registration"]] = relationship("Registration")
|
||||
team: Mapped[list["TeamRegistration"]] = relationship("TeamRegistration")
|
||||
|
||||
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(Base):
|
||||
__tablename__ = "team_registration"
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
event_id: Mapped[int] = mapped_column(ForeignKey("event.id"))
|
||||
person_name: Mapped[str] = mapped_column(nullable=False)
|
||||
work_type: Mapped[WorkTypes] = mapped_column(Text, nullable=False)
|
||||
comment: Mapped[str | None] = mapped_column()
|
||||
|
||||
|
||||
class Household(Base):
|
||||
__tablename__ = "household"
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
name: Mapped[str] = mapped_column(nullable=False)
|
||||
|
||||
|
||||
class Registration(Base):
|
||||
__tablename__ = "registration"
|
||||
event_id: Mapped[int] = mapped_column(ForeignKey("event.id"), primary_key=True)
|
||||
household_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("household.id"), primary_key=True
|
||||
)
|
||||
num_adult_meals: Mapped[int] = mapped_column(nullable=False)
|
||||
num_children_meals: Mapped[int] = mapped_column(nullable=False)
|
||||
num_small_children_meals: Mapped[int] = mapped_column(nullable=False)
|
||||
comment: Mapped[str | None] = mapped_column()
|
||||
|
||||
household: Mapped["Household"] = relationship()
|
||||
|
||||
|
||||
class Subscription(Base):
|
||||
__tablename__ = "subscription"
|
||||
household_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("household.id"), primary_key=True
|
||||
)
|
||||
num_adult_meals: Mapped[int] = mapped_column(nullable=False)
|
||||
num_children_meals: Mapped[int] = mapped_column(nullable=False)
|
||||
num_small_children_meals: Mapped[int] = mapped_column(nullable=False)
|
||||
comment: Mapped[str | None] = mapped_column()
|
||||
|
||||
last_modified: Mapped[datetime] = mapped_column(
|
||||
default=datetime.now, nullable=False
|
||||
)
|
||||
|
||||
monday: Mapped[bool] = mapped_column(default=True, nullable=False)
|
||||
tuesday: Mapped[bool] = mapped_column(default=True, nullable=False)
|
||||
wednesday: Mapped[bool] = mapped_column(default=True, nullable=False)
|
||||
thursday: Mapped[bool] = mapped_column(default=True, nullable=False)
|
||||
friday: Mapped[bool] = mapped_column(default=True, nullable=False)
|
||||
saturday: Mapped[bool] = mapped_column(default=True, nullable=False)
|
||||
sunday: Mapped[bool] = mapped_column(default=True, nullable=False)
|
||||
|
||||
household: Mapped["Household"] = relationship()
|
||||
|
||||
def day_string_de(self) -> str:
|
||||
"""
|
||||
Generates a string representation of selected days in German short form.
|
||||
"""
|
||||
result = []
|
||||
|
||||
if self.monday:
|
||||
result.append("Mo")
|
||||
if self.tuesday:
|
||||
result.append("Di")
|
||||
if self.wednesday:
|
||||
result.append("Mi")
|
||||
if self.thursday:
|
||||
result.append("Do")
|
||||
if self.friday:
|
||||
result.append("Fr")
|
||||
if self.saturday:
|
||||
result.append("Sa")
|
||||
if self.sunday:
|
||||
result.append("So")
|
||||
|
||||
if len(result) < 7:
|
||||
return ", ".join(result)
|
||||
else:
|
||||
return "Alle"
|
||||
29
src/meal_manager/static/css/allmende.css
Normal file
29
src/meal_manager/static/css/allmende.css
Normal file
@@ -0,0 +1,29 @@
|
||||
/* theme.css */
|
||||
/* Green Bootstrap Theme */
|
||||
|
||||
:root {
|
||||
--bs-primary: #198754;
|
||||
--bs-primary-rgb: 25, 135, 84;
|
||||
--bs-success: #198754;
|
||||
--bs-success-rgb: 25, 135, 84;
|
||||
--bs-link-color: var(--bs-primary);
|
||||
--bs-link-hover-color: #146c43;
|
||||
}
|
||||
|
||||
/* Explicit fallback overrides for older versions (<=5.2) */
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #198754;
|
||||
border-color: #198754;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
color: #fff;
|
||||
background-color: #157347;
|
||||
border-color: #146c43;
|
||||
}
|
||||
.btn-primary:focus,
|
||||
.btn-primary:active {
|
||||
color: #fff;
|
||||
background-color: #146c43;
|
||||
border-color: #125c39;
|
||||
}
|
||||
4085
src/meal_manager/static/css/bootstrap-grid.css
vendored
Normal file
4085
src/meal_manager/static/css/bootstrap-grid.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
src/meal_manager/static/css/bootstrap-grid.css.map
Normal file
1
src/meal_manager/static/css/bootstrap-grid.css.map
Normal file
File diff suppressed because one or more lines are too long
6
src/meal_manager/static/css/bootstrap-grid.min.css
vendored
Normal file
6
src/meal_manager/static/css/bootstrap-grid.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
src/meal_manager/static/css/bootstrap-grid.min.css.map
Normal file
1
src/meal_manager/static/css/bootstrap-grid.min.css.map
Normal file
File diff suppressed because one or more lines are too long
4084
src/meal_manager/static/css/bootstrap-grid.rtl.css
vendored
Normal file
4084
src/meal_manager/static/css/bootstrap-grid.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
src/meal_manager/static/css/bootstrap-grid.rtl.css.map
Normal file
1
src/meal_manager/static/css/bootstrap-grid.rtl.css.map
Normal file
File diff suppressed because one or more lines are too long
6
src/meal_manager/static/css/bootstrap-grid.rtl.min.css
vendored
Normal file
6
src/meal_manager/static/css/bootstrap-grid.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
601
src/meal_manager/static/css/bootstrap-reboot.css
vendored
Normal file
601
src/meal_manager/static/css/bootstrap-reboot.css
vendored
Normal file
@@ -0,0 +1,601 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.3.8 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2025 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root,
|
||||
[data-bs-theme=light] {
|
||||
--bs-blue: #0d6efd;
|
||||
--bs-indigo: #6610f2;
|
||||
--bs-purple: #6f42c1;
|
||||
--bs-pink: #d63384;
|
||||
--bs-red: #dc3545;
|
||||
--bs-orange: #fd7e14;
|
||||
--bs-yellow: #ffc107;
|
||||
--bs-green: #198754;
|
||||
--bs-teal: #20c997;
|
||||
--bs-cyan: #0dcaf0;
|
||||
--bs-black: #000;
|
||||
--bs-white: #fff;
|
||||
--bs-gray: #6c757d;
|
||||
--bs-gray-dark: #343a40;
|
||||
--bs-gray-100: #f8f9fa;
|
||||
--bs-gray-200: #e9ecef;
|
||||
--bs-gray-300: #dee2e6;
|
||||
--bs-gray-400: #ced4da;
|
||||
--bs-gray-500: #adb5bd;
|
||||
--bs-gray-600: #6c757d;
|
||||
--bs-gray-700: #495057;
|
||||
--bs-gray-800: #343a40;
|
||||
--bs-gray-900: #212529;
|
||||
--bs-primary: #0d6efd;
|
||||
--bs-secondary: #6c757d;
|
||||
--bs-success: #198754;
|
||||
--bs-info: #0dcaf0;
|
||||
--bs-warning: #ffc107;
|
||||
--bs-danger: #dc3545;
|
||||
--bs-light: #f8f9fa;
|
||||
--bs-dark: #212529;
|
||||
--bs-primary-rgb: 13, 110, 253;
|
||||
--bs-secondary-rgb: 108, 117, 125;
|
||||
--bs-success-rgb: 25, 135, 84;
|
||||
--bs-info-rgb: 13, 202, 240;
|
||||
--bs-warning-rgb: 255, 193, 7;
|
||||
--bs-danger-rgb: 220, 53, 69;
|
||||
--bs-light-rgb: 248, 249, 250;
|
||||
--bs-dark-rgb: 33, 37, 41;
|
||||
--bs-primary-text-emphasis: #052c65;
|
||||
--bs-secondary-text-emphasis: #2b2f32;
|
||||
--bs-success-text-emphasis: #0a3622;
|
||||
--bs-info-text-emphasis: #055160;
|
||||
--bs-warning-text-emphasis: #664d03;
|
||||
--bs-danger-text-emphasis: #58151c;
|
||||
--bs-light-text-emphasis: #495057;
|
||||
--bs-dark-text-emphasis: #495057;
|
||||
--bs-primary-bg-subtle: #cfe2ff;
|
||||
--bs-secondary-bg-subtle: #e2e3e5;
|
||||
--bs-success-bg-subtle: #d1e7dd;
|
||||
--bs-info-bg-subtle: #cff4fc;
|
||||
--bs-warning-bg-subtle: #fff3cd;
|
||||
--bs-danger-bg-subtle: #f8d7da;
|
||||
--bs-light-bg-subtle: #fcfcfd;
|
||||
--bs-dark-bg-subtle: #ced4da;
|
||||
--bs-primary-border-subtle: #9ec5fe;
|
||||
--bs-secondary-border-subtle: #c4c8cb;
|
||||
--bs-success-border-subtle: #a3cfbb;
|
||||
--bs-info-border-subtle: #9eeaf9;
|
||||
--bs-warning-border-subtle: #ffe69c;
|
||||
--bs-danger-border-subtle: #f1aeb5;
|
||||
--bs-light-border-subtle: #e9ecef;
|
||||
--bs-dark-border-subtle: #adb5bd;
|
||||
--bs-white-rgb: 255, 255, 255;
|
||||
--bs-black-rgb: 0, 0, 0;
|
||||
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||
--bs-body-font-size: 1rem;
|
||||
--bs-body-font-weight: 400;
|
||||
--bs-body-line-height: 1.5;
|
||||
--bs-body-color: #212529;
|
||||
--bs-body-color-rgb: 33, 37, 41;
|
||||
--bs-body-bg: #fff;
|
||||
--bs-body-bg-rgb: 255, 255, 255;
|
||||
--bs-emphasis-color: #000;
|
||||
--bs-emphasis-color-rgb: 0, 0, 0;
|
||||
--bs-secondary-color: rgba(33, 37, 41, 0.75);
|
||||
--bs-secondary-color-rgb: 33, 37, 41;
|
||||
--bs-secondary-bg: #e9ecef;
|
||||
--bs-secondary-bg-rgb: 233, 236, 239;
|
||||
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
|
||||
--bs-tertiary-color-rgb: 33, 37, 41;
|
||||
--bs-tertiary-bg: #f8f9fa;
|
||||
--bs-tertiary-bg-rgb: 248, 249, 250;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #0d6efd;
|
||||
--bs-link-color-rgb: 13, 110, 253;
|
||||
--bs-link-decoration: underline;
|
||||
--bs-link-hover-color: #0a58ca;
|
||||
--bs-link-hover-color-rgb: 10, 88, 202;
|
||||
--bs-code-color: #d63384;
|
||||
--bs-highlight-color: #212529;
|
||||
--bs-highlight-bg: #fff3cd;
|
||||
--bs-border-width: 1px;
|
||||
--bs-border-style: solid;
|
||||
--bs-border-color: #dee2e6;
|
||||
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
|
||||
--bs-border-radius: 0.375rem;
|
||||
--bs-border-radius-sm: 0.25rem;
|
||||
--bs-border-radius-lg: 0.5rem;
|
||||
--bs-border-radius-xl: 1rem;
|
||||
--bs-border-radius-xxl: 2rem;
|
||||
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
|
||||
--bs-border-radius-pill: 50rem;
|
||||
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
|
||||
--bs-focus-ring-width: 0.25rem;
|
||||
--bs-focus-ring-opacity: 0.25;
|
||||
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
|
||||
--bs-form-valid-color: #198754;
|
||||
--bs-form-valid-border-color: #198754;
|
||||
--bs-form-invalid-color: #dc3545;
|
||||
--bs-form-invalid-border-color: #dc3545;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] {
|
||||
color-scheme: dark;
|
||||
--bs-body-color: #dee2e6;
|
||||
--bs-body-color-rgb: 222, 226, 230;
|
||||
--bs-body-bg: #212529;
|
||||
--bs-body-bg-rgb: 33, 37, 41;
|
||||
--bs-emphasis-color: #fff;
|
||||
--bs-emphasis-color-rgb: 255, 255, 255;
|
||||
--bs-secondary-color: rgba(222, 226, 230, 0.75);
|
||||
--bs-secondary-color-rgb: 222, 226, 230;
|
||||
--bs-secondary-bg: #343a40;
|
||||
--bs-secondary-bg-rgb: 52, 58, 64;
|
||||
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
|
||||
--bs-tertiary-color-rgb: 222, 226, 230;
|
||||
--bs-tertiary-bg: #2b3035;
|
||||
--bs-tertiary-bg-rgb: 43, 48, 53;
|
||||
--bs-primary-text-emphasis: #6ea8fe;
|
||||
--bs-secondary-text-emphasis: #a7acb1;
|
||||
--bs-success-text-emphasis: #75b798;
|
||||
--bs-info-text-emphasis: #6edff6;
|
||||
--bs-warning-text-emphasis: #ffda6a;
|
||||
--bs-danger-text-emphasis: #ea868f;
|
||||
--bs-light-text-emphasis: #f8f9fa;
|
||||
--bs-dark-text-emphasis: #dee2e6;
|
||||
--bs-primary-bg-subtle: #031633;
|
||||
--bs-secondary-bg-subtle: #161719;
|
||||
--bs-success-bg-subtle: #051b11;
|
||||
--bs-info-bg-subtle: #032830;
|
||||
--bs-warning-bg-subtle: #332701;
|
||||
--bs-danger-bg-subtle: #2c0b0e;
|
||||
--bs-light-bg-subtle: #343a40;
|
||||
--bs-dark-bg-subtle: #1a1d20;
|
||||
--bs-primary-border-subtle: #084298;
|
||||
--bs-secondary-border-subtle: #41464b;
|
||||
--bs-success-border-subtle: #0f5132;
|
||||
--bs-info-border-subtle: #087990;
|
||||
--bs-warning-border-subtle: #997404;
|
||||
--bs-danger-border-subtle: #842029;
|
||||
--bs-light-border-subtle: #495057;
|
||||
--bs-dark-border-subtle: #343a40;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #6ea8fe;
|
||||
--bs-link-hover-color: #8bb9fe;
|
||||
--bs-link-color-rgb: 110, 168, 254;
|
||||
--bs-link-hover-color-rgb: 139, 185, 254;
|
||||
--bs-code-color: #e685b5;
|
||||
--bs-highlight-color: #dee2e6;
|
||||
--bs-highlight-bg: #664d03;
|
||||
--bs-border-color: #495057;
|
||||
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
|
||||
--bs-form-valid-color: #75b798;
|
||||
--bs-form-valid-border-color: #75b798;
|
||||
--bs-form-invalid-color: #ea868f;
|
||||
--bs-form-invalid-border-color: #ea868f;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family);
|
||||
font-size: var(--bs-body-font-size);
|
||||
font-weight: var(--bs-body-font-weight);
|
||||
line-height: var(--bs-body-line-height);
|
||||
color: var(--bs-body-color);
|
||||
text-align: var(--bs-body-text-align);
|
||||
background-color: var(--bs-body-bg);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
border: 0;
|
||||
border-top: var(--bs-border-width) solid;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
color: var(--bs-heading-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.1875em;
|
||||
color: var(--bs-highlight-color);
|
||||
background-color: var(--bs-highlight-bg);
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: var(--bs-font-monospace);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-code-color);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.1875rem 0.375rem;
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-body-bg);
|
||||
background-color: var(--bs-body-color);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: left;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: inherit;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
[type=search]::-webkit-search-cancel-button {
|
||||
cursor: pointer;
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
/* rtl:raw:
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
*/
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
||||
1
src/meal_manager/static/css/bootstrap-reboot.css.map
Normal file
1
src/meal_manager/static/css/bootstrap-reboot.css.map
Normal file
File diff suppressed because one or more lines are too long
6
src/meal_manager/static/css/bootstrap-reboot.min.css
vendored
Normal file
6
src/meal_manager/static/css/bootstrap-reboot.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
src/meal_manager/static/css/bootstrap-reboot.min.css.map
Normal file
1
src/meal_manager/static/css/bootstrap-reboot.min.css.map
Normal file
File diff suppressed because one or more lines are too long
598
src/meal_manager/static/css/bootstrap-reboot.rtl.css
vendored
Normal file
598
src/meal_manager/static/css/bootstrap-reboot.rtl.css
vendored
Normal file
@@ -0,0 +1,598 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.3.8 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2025 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root,
|
||||
[data-bs-theme=light] {
|
||||
--bs-blue: #0d6efd;
|
||||
--bs-indigo: #6610f2;
|
||||
--bs-purple: #6f42c1;
|
||||
--bs-pink: #d63384;
|
||||
--bs-red: #dc3545;
|
||||
--bs-orange: #fd7e14;
|
||||
--bs-yellow: #ffc107;
|
||||
--bs-green: #198754;
|
||||
--bs-teal: #20c997;
|
||||
--bs-cyan: #0dcaf0;
|
||||
--bs-black: #000;
|
||||
--bs-white: #fff;
|
||||
--bs-gray: #6c757d;
|
||||
--bs-gray-dark: #343a40;
|
||||
--bs-gray-100: #f8f9fa;
|
||||
--bs-gray-200: #e9ecef;
|
||||
--bs-gray-300: #dee2e6;
|
||||
--bs-gray-400: #ced4da;
|
||||
--bs-gray-500: #adb5bd;
|
||||
--bs-gray-600: #6c757d;
|
||||
--bs-gray-700: #495057;
|
||||
--bs-gray-800: #343a40;
|
||||
--bs-gray-900: #212529;
|
||||
--bs-primary: #0d6efd;
|
||||
--bs-secondary: #6c757d;
|
||||
--bs-success: #198754;
|
||||
--bs-info: #0dcaf0;
|
||||
--bs-warning: #ffc107;
|
||||
--bs-danger: #dc3545;
|
||||
--bs-light: #f8f9fa;
|
||||
--bs-dark: #212529;
|
||||
--bs-primary-rgb: 13, 110, 253;
|
||||
--bs-secondary-rgb: 108, 117, 125;
|
||||
--bs-success-rgb: 25, 135, 84;
|
||||
--bs-info-rgb: 13, 202, 240;
|
||||
--bs-warning-rgb: 255, 193, 7;
|
||||
--bs-danger-rgb: 220, 53, 69;
|
||||
--bs-light-rgb: 248, 249, 250;
|
||||
--bs-dark-rgb: 33, 37, 41;
|
||||
--bs-primary-text-emphasis: #052c65;
|
||||
--bs-secondary-text-emphasis: #2b2f32;
|
||||
--bs-success-text-emphasis: #0a3622;
|
||||
--bs-info-text-emphasis: #055160;
|
||||
--bs-warning-text-emphasis: #664d03;
|
||||
--bs-danger-text-emphasis: #58151c;
|
||||
--bs-light-text-emphasis: #495057;
|
||||
--bs-dark-text-emphasis: #495057;
|
||||
--bs-primary-bg-subtle: #cfe2ff;
|
||||
--bs-secondary-bg-subtle: #e2e3e5;
|
||||
--bs-success-bg-subtle: #d1e7dd;
|
||||
--bs-info-bg-subtle: #cff4fc;
|
||||
--bs-warning-bg-subtle: #fff3cd;
|
||||
--bs-danger-bg-subtle: #f8d7da;
|
||||
--bs-light-bg-subtle: #fcfcfd;
|
||||
--bs-dark-bg-subtle: #ced4da;
|
||||
--bs-primary-border-subtle: #9ec5fe;
|
||||
--bs-secondary-border-subtle: #c4c8cb;
|
||||
--bs-success-border-subtle: #a3cfbb;
|
||||
--bs-info-border-subtle: #9eeaf9;
|
||||
--bs-warning-border-subtle: #ffe69c;
|
||||
--bs-danger-border-subtle: #f1aeb5;
|
||||
--bs-light-border-subtle: #e9ecef;
|
||||
--bs-dark-border-subtle: #adb5bd;
|
||||
--bs-white-rgb: 255, 255, 255;
|
||||
--bs-black-rgb: 0, 0, 0;
|
||||
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||
--bs-body-font-size: 1rem;
|
||||
--bs-body-font-weight: 400;
|
||||
--bs-body-line-height: 1.5;
|
||||
--bs-body-color: #212529;
|
||||
--bs-body-color-rgb: 33, 37, 41;
|
||||
--bs-body-bg: #fff;
|
||||
--bs-body-bg-rgb: 255, 255, 255;
|
||||
--bs-emphasis-color: #000;
|
||||
--bs-emphasis-color-rgb: 0, 0, 0;
|
||||
--bs-secondary-color: rgba(33, 37, 41, 0.75);
|
||||
--bs-secondary-color-rgb: 33, 37, 41;
|
||||
--bs-secondary-bg: #e9ecef;
|
||||
--bs-secondary-bg-rgb: 233, 236, 239;
|
||||
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
|
||||
--bs-tertiary-color-rgb: 33, 37, 41;
|
||||
--bs-tertiary-bg: #f8f9fa;
|
||||
--bs-tertiary-bg-rgb: 248, 249, 250;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #0d6efd;
|
||||
--bs-link-color-rgb: 13, 110, 253;
|
||||
--bs-link-decoration: underline;
|
||||
--bs-link-hover-color: #0a58ca;
|
||||
--bs-link-hover-color-rgb: 10, 88, 202;
|
||||
--bs-code-color: #d63384;
|
||||
--bs-highlight-color: #212529;
|
||||
--bs-highlight-bg: #fff3cd;
|
||||
--bs-border-width: 1px;
|
||||
--bs-border-style: solid;
|
||||
--bs-border-color: #dee2e6;
|
||||
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
|
||||
--bs-border-radius: 0.375rem;
|
||||
--bs-border-radius-sm: 0.25rem;
|
||||
--bs-border-radius-lg: 0.5rem;
|
||||
--bs-border-radius-xl: 1rem;
|
||||
--bs-border-radius-xxl: 2rem;
|
||||
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
|
||||
--bs-border-radius-pill: 50rem;
|
||||
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
|
||||
--bs-focus-ring-width: 0.25rem;
|
||||
--bs-focus-ring-opacity: 0.25;
|
||||
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
|
||||
--bs-form-valid-color: #198754;
|
||||
--bs-form-valid-border-color: #198754;
|
||||
--bs-form-invalid-color: #dc3545;
|
||||
--bs-form-invalid-border-color: #dc3545;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] {
|
||||
color-scheme: dark;
|
||||
--bs-body-color: #dee2e6;
|
||||
--bs-body-color-rgb: 222, 226, 230;
|
||||
--bs-body-bg: #212529;
|
||||
--bs-body-bg-rgb: 33, 37, 41;
|
||||
--bs-emphasis-color: #fff;
|
||||
--bs-emphasis-color-rgb: 255, 255, 255;
|
||||
--bs-secondary-color: rgba(222, 226, 230, 0.75);
|
||||
--bs-secondary-color-rgb: 222, 226, 230;
|
||||
--bs-secondary-bg: #343a40;
|
||||
--bs-secondary-bg-rgb: 52, 58, 64;
|
||||
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
|
||||
--bs-tertiary-color-rgb: 222, 226, 230;
|
||||
--bs-tertiary-bg: #2b3035;
|
||||
--bs-tertiary-bg-rgb: 43, 48, 53;
|
||||
--bs-primary-text-emphasis: #6ea8fe;
|
||||
--bs-secondary-text-emphasis: #a7acb1;
|
||||
--bs-success-text-emphasis: #75b798;
|
||||
--bs-info-text-emphasis: #6edff6;
|
||||
--bs-warning-text-emphasis: #ffda6a;
|
||||
--bs-danger-text-emphasis: #ea868f;
|
||||
--bs-light-text-emphasis: #f8f9fa;
|
||||
--bs-dark-text-emphasis: #dee2e6;
|
||||
--bs-primary-bg-subtle: #031633;
|
||||
--bs-secondary-bg-subtle: #161719;
|
||||
--bs-success-bg-subtle: #051b11;
|
||||
--bs-info-bg-subtle: #032830;
|
||||
--bs-warning-bg-subtle: #332701;
|
||||
--bs-danger-bg-subtle: #2c0b0e;
|
||||
--bs-light-bg-subtle: #343a40;
|
||||
--bs-dark-bg-subtle: #1a1d20;
|
||||
--bs-primary-border-subtle: #084298;
|
||||
--bs-secondary-border-subtle: #41464b;
|
||||
--bs-success-border-subtle: #0f5132;
|
||||
--bs-info-border-subtle: #087990;
|
||||
--bs-warning-border-subtle: #997404;
|
||||
--bs-danger-border-subtle: #842029;
|
||||
--bs-light-border-subtle: #495057;
|
||||
--bs-dark-border-subtle: #343a40;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #6ea8fe;
|
||||
--bs-link-hover-color: #8bb9fe;
|
||||
--bs-link-color-rgb: 110, 168, 254;
|
||||
--bs-link-hover-color-rgb: 139, 185, 254;
|
||||
--bs-code-color: #e685b5;
|
||||
--bs-highlight-color: #dee2e6;
|
||||
--bs-highlight-bg: #664d03;
|
||||
--bs-border-color: #495057;
|
||||
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
|
||||
--bs-form-valid-color: #75b798;
|
||||
--bs-form-valid-border-color: #75b798;
|
||||
--bs-form-invalid-color: #ea868f;
|
||||
--bs-form-invalid-border-color: #ea868f;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family);
|
||||
font-size: var(--bs-body-font-size);
|
||||
font-weight: var(--bs-body-font-weight);
|
||||
line-height: var(--bs-body-line-height);
|
||||
color: var(--bs-body-color);
|
||||
text-align: var(--bs-body-text-align);
|
||||
background-color: var(--bs-body-bg);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
border: 0;
|
||||
border-top: var(--bs-border-width) solid;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
color: var(--bs-heading-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.1875em;
|
||||
color: var(--bs-highlight-color);
|
||||
background-color: var(--bs-highlight-bg);
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: var(--bs-font-monospace);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-code-color);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.1875rem 0.375rem;
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-body-bg);
|
||||
background-color: var(--bs-body-color);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: right;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: inherit;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: right;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
[type=search]::-webkit-search-cancel-button {
|
||||
cursor: pointer;
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
|
||||
1
src/meal_manager/static/css/bootstrap-reboot.rtl.css.map
Normal file
1
src/meal_manager/static/css/bootstrap-reboot.rtl.css.map
Normal file
File diff suppressed because one or more lines are too long
6
src/meal_manager/static/css/bootstrap-reboot.rtl.min.css
vendored
Normal file
6
src/meal_manager/static/css/bootstrap-reboot.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
5406
src/meal_manager/static/css/bootstrap-utilities.css
vendored
Normal file
5406
src/meal_manager/static/css/bootstrap-utilities.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
src/meal_manager/static/css/bootstrap-utilities.css.map
Normal file
1
src/meal_manager/static/css/bootstrap-utilities.css.map
Normal file
File diff suppressed because one or more lines are too long
6
src/meal_manager/static/css/bootstrap-utilities.min.css
vendored
Normal file
6
src/meal_manager/static/css/bootstrap-utilities.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
5397
src/meal_manager/static/css/bootstrap-utilities.rtl.css
vendored
Normal file
5397
src/meal_manager/static/css/bootstrap-utilities.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
6
src/meal_manager/static/css/bootstrap-utilities.rtl.min.css
vendored
Normal file
6
src/meal_manager/static/css/bootstrap-utilities.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
12048
src/meal_manager/static/css/bootstrap.css
vendored
Normal file
12048
src/meal_manager/static/css/bootstrap.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
src/meal_manager/static/css/bootstrap.css.map
Normal file
1
src/meal_manager/static/css/bootstrap.css.map
Normal file
File diff suppressed because one or more lines are too long
6
src/meal_manager/static/css/bootstrap.min.css
vendored
Normal file
6
src/meal_manager/static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
src/meal_manager/static/css/bootstrap.min.css.map
Normal file
1
src/meal_manager/static/css/bootstrap.min.css.map
Normal file
File diff suppressed because one or more lines are too long
12021
src/meal_manager/static/css/bootstrap.rtl.css
vendored
Normal file
12021
src/meal_manager/static/css/bootstrap.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
src/meal_manager/static/css/bootstrap.rtl.css.map
Normal file
1
src/meal_manager/static/css/bootstrap.rtl.css.map
Normal file
File diff suppressed because one or more lines are too long
6
src/meal_manager/static/css/bootstrap.rtl.min.css
vendored
Normal file
6
src/meal_manager/static/css/bootstrap.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
src/meal_manager/static/css/bootstrap.rtl.min.css.map
Normal file
1
src/meal_manager/static/css/bootstrap.rtl.min.css.map
Normal file
File diff suppressed because one or more lines are too long
2106
src/meal_manager/static/icons/bootstrap-icons.css
vendored
Normal file
2106
src/meal_manager/static/icons/bootstrap-icons.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2080
src/meal_manager/static/icons/bootstrap-icons.json
Normal file
2080
src/meal_manager/static/icons/bootstrap-icons.json
Normal file
File diff suppressed because it is too large
Load Diff
5
src/meal_manager/static/icons/bootstrap-icons.min.css
vendored
Normal file
5
src/meal_manager/static/icons/bootstrap-icons.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
2118
src/meal_manager/static/icons/bootstrap-icons.scss
vendored
Normal file
2118
src/meal_manager/static/icons/bootstrap-icons.scss
vendored
Normal file
File diff suppressed because it is too large
Load Diff
BIN
src/meal_manager/static/icons/fonts/bootstrap-icons.woff
Normal file
BIN
src/meal_manager/static/icons/fonts/bootstrap-icons.woff
Normal file
Binary file not shown.
BIN
src/meal_manager/static/icons/fonts/bootstrap-icons.woff2
Normal file
BIN
src/meal_manager/static/icons/fonts/bootstrap-icons.woff2
Normal file
Binary file not shown.
BIN
src/meal_manager/static/img/Logo.png
Normal file
BIN
src/meal_manager/static/img/Logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
6312
src/meal_manager/static/js/bootstrap.bundle.js
vendored
Normal file
6312
src/meal_manager/static/js/bootstrap.bundle.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
src/meal_manager/static/js/bootstrap.bundle.js.map
Normal file
1
src/meal_manager/static/js/bootstrap.bundle.js.map
Normal file
File diff suppressed because one or more lines are too long
7
src/meal_manager/static/js/bootstrap.bundle.min.js
vendored
Normal file
7
src/meal_manager/static/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
src/meal_manager/static/js/bootstrap.bundle.min.js.map
Normal file
1
src/meal_manager/static/js/bootstrap.bundle.min.js.map
Normal file
File diff suppressed because one or more lines are too long
4447
src/meal_manager/static/js/bootstrap.esm.js
vendored
Normal file
4447
src/meal_manager/static/js/bootstrap.esm.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
src/meal_manager/static/js/bootstrap.esm.js.map
Normal file
1
src/meal_manager/static/js/bootstrap.esm.js.map
Normal file
File diff suppressed because one or more lines are too long
7
src/meal_manager/static/js/bootstrap.esm.min.js
vendored
Normal file
7
src/meal_manager/static/js/bootstrap.esm.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
src/meal_manager/static/js/bootstrap.esm.min.js.map
Normal file
1
src/meal_manager/static/js/bootstrap.esm.min.js.map
Normal file
File diff suppressed because one or more lines are too long
4494
src/meal_manager/static/js/bootstrap.js
vendored
Normal file
4494
src/meal_manager/static/js/bootstrap.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
src/meal_manager/static/js/bootstrap.js.map
Normal file
1
src/meal_manager/static/js/bootstrap.js.map
Normal file
File diff suppressed because one or more lines are too long
7
src/meal_manager/static/js/bootstrap.min.js
vendored
Normal file
7
src/meal_manager/static/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
src/meal_manager/static/js/bootstrap.min.js.map
Normal file
1
src/meal_manager/static/js/bootstrap.min.js.map
Normal file
File diff suppressed because one or more lines are too long
38
src/meal_manager/templates/add_event.html
Normal file
38
src/meal_manager/templates/add_event.html
Normal file
@@ -0,0 +1,38 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center mt-4">
|
||||
<div class="col-md-8">
|
||||
<h2>Neues Event erstellen</h2>
|
||||
<form method="post" action="/event/add">
|
||||
<div class="mb-3">
|
||||
<label for="eventName" class="form-label">Event Name</label>
|
||||
<input type="text" class="form-control" id="eventName" name="eventName" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="eventTime" class="form-label">Datum und Uhrzeit</label>
|
||||
<input type="datetime-local" class="form-control" id="eventTime" name="eventTime" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="registrationDeadline" class="form-label">Anmeldungs-Deadline</label>
|
||||
<input type="datetime-local" class="form-control" id="registrationDeadline" name="registrationDeadline">
|
||||
<small class="form-text text-muted">Leer lassen für Sonntag Abend vor dem Event</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="recipeLink" class="form-label">Rezept-Link</label>
|
||||
<input type="text" class="form-control" id="recipeLink" name="recipeLink">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="eventDescription" class="form-label">Beschreibung</label>
|
||||
<textarea class="form-control" id="eventDescription" name="eventDescription" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Event erstellen</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
43
src/meal_manager/templates/base.html
Normal file
43
src/meal_manager/templates/base.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<!doctype html>
|
||||
<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">
|
||||
<link href="/static/icons/bootstrap-icons.min.css" rel="stylesheet">
|
||||
<link href="/static/css/allmende.css" rel="stylesheet">
|
||||
</head>
|
||||
<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="/">
|
||||
<img src="/static/img/Logo.png" alt="Logo" height="30" class="d-inline-block align-text-top">
|
||||
Allmende Essen
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
|
||||
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if current_page == 'home' %}active{% endif %}" {% if current_page == 'home' %}aria-current="page"{% endif %} href="/">Kommende</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if current_page == 'past' %}active{% endif %}" {% if current_page == 'past' %}aria-current="page"{% endif %} href="/past_events">Vergangene</a>
|
||||
</li>
|
||||
<!-- <li class="nav-item">-->
|
||||
<!-- <a class="nav-link {% if current_page == 'preise' %}active{% endif %}" {% if current_page == 'preise' %}aria-current="page"{% endif %} href="/preise">Preise</a>-->
|
||||
<!-- </li>-->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<script src="/static/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
257
src/meal_manager/templates/event.html
Normal file
257
src/meal_manager/templates/event.html
Normal file
@@ -0,0 +1,257 @@
|
||||
{% macro teamEntries(event, work_type) -%}
|
||||
{% for entry in event.team | selectattr("work_type", "equalto", work_type)%}
|
||||
<div class="d-inline-flex align-items-center bg-light border rounded-pill px-3 py-1 m-1">
|
||||
<span class="me-2">{{ entry.person_name }}</span>
|
||||
<button type="button" class="btn btn-sm p-0 border-0 bg-transparent text-muted">
|
||||
<a href="/event/{{event.id}}/register_team/{{entry.id}}/delete">
|
||||
<i class="bi bi-trash"></i>
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<p class="h1">{{ event.title }}</p>
|
||||
<p class="text-muted">{{ event.event_time.strftime('%A, %d.%m.%Y') }}</p>
|
||||
<p>{{ event.description }}</p>
|
||||
<hr class="hr"/>
|
||||
<div class="container">
|
||||
<p class="h3">Anmeldungen</p>
|
||||
<div class="row m-2">
|
||||
<div class="col-md-4 d-flex justify-content-center align-items-center flex-column">
|
||||
<p class="text-muted w-100 mb-2">Anmeldung schließt {{ event.registration_deadline.strftime('%A, %d.%m.%Y, %H:%M Uhr') }}</p>
|
||||
<!-- Button trigger modal -->
|
||||
<button type="button" class="btn btn-primary mb-2 w-100" {% if event.registration_deadline < now %}disabled{% endif%} data-bs-toggle="modal" data-bs-target="#registration">
|
||||
Anmeldung hinzufügen
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary mb-2 w-100" {% if event.all_teams_max() or event.event_time < now %}disabled{% endif %} data-bs-toggle="modal" data-bs-target="#teamRegistration">
|
||||
Dienst übernehmen
|
||||
</button>
|
||||
{% if event.recipe_link %}
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Dienste</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
Kochen: {% if event.team_min_reached("cooking") %}✅{% endif %}
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
{{ teamEntries(event, "cooking") }}
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
Spülen: {% if event.team_min_reached("dishes") %}✅{% endif %}
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
{{ teamEntries(event, "dishes") }}
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
Tische vorbereiten: {% if event.team_min_reached("tables") %}✅{% endif %}
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
{{ teamEntries(event, "tables") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Zusammenfassung</h5>
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<td>Erwachsene:</td>
|
||||
<td>{{ event.registrations | sum(attribute="num_adult_meals") }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kinder:</td>
|
||||
<td>{{ event.registrations | sum(attribute="num_children_meals") }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kleinkinder:</td>
|
||||
<td>{{ event.registrations | sum(attribute="num_small_children_meals") }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Gesamt:</th>
|
||||
<th>{{ event.registrations | sum(attribute="num_adult_meals") + event.registrations | sum(attribute="num_children_meals") +event.registrations | sum(attribute="num_small_children_meals")}} </th>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop table view -->
|
||||
<div class="d-none d-md-block">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Haushalt</th>
|
||||
<th scope="col">Erwachsene</th>
|
||||
<th scope="col">Kinder</th>
|
||||
<th scope="col">Kleinkinder</th>
|
||||
<th scope="col">Kommentar</th>
|
||||
<th scope="col">Löschen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for reg in event.registrations %}
|
||||
<tr>
|
||||
<td>{{ reg.household.name }}</td>
|
||||
<td>{{ reg.num_adult_meals }}</td>
|
||||
<td>{{ reg.num_children_meals }}</td>
|
||||
<td>{{ reg.num_small_children_meals }}</td>
|
||||
<td>{{ reg.comment }}</td>
|
||||
<td><a href="/event/{{event.id}}/registration/{{reg.household_id}}/delete"><i class="bi bi-trash"></i></a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</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-3 text-center">
|
||||
<div class="text-muted small">Erwachsene</div>
|
||||
<div class="fw-bold">{{ reg.num_adult_meals }}</div>
|
||||
</div>
|
||||
<div class="col-3 text-center">
|
||||
<div class="text-muted small">Kinder</div>
|
||||
<div class="fw-bold">{{ reg.num_children_meals }}</div>
|
||||
</div>
|
||||
<div class="col-3 text-center">
|
||||
<div class="text-muted small">Kleinkinder</div>
|
||||
<div class="fw-bold">{{ reg.num_small_children_meals }}</div>
|
||||
</div>
|
||||
<div class="col-3 text-center">
|
||||
<div class="text-muted small">Kommentar</div>
|
||||
<div class="small">{{ reg.comment }}</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">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="staticBackdropLabel">Anmeldung hinzufügen</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="/event/{{event.id}}/register" method="POST">
|
||||
<div class="modal-body">
|
||||
<p>Wenn dein Haushalt nicht auswählbar ist, existiert schon eine Anmeldung. Wenn du die Anmeldung ändern willst, lösche die bestehende Anmeldung und lege eine neue an.</p>
|
||||
<div class="mb-3">
|
||||
<select name="household" class="form-select" aria-label="Multiple select example" required>
|
||||
<option value="" disabled selected hidden>Wer?</option>
|
||||
{% for household in households %}
|
||||
<option value="{{household.id}}">{{household.name}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label for="InputAdults" class="form-label">Anzahl Erwachsene</label>
|
||||
<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="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="number" class="form-control"
|
||||
aria-label="Anzahl Kinder <7" min="0" step="1" inputmode="numeric">
|
||||
</div>
|
||||
<div class="mb-3 mt-3">
|
||||
<label for="InputComment" class="form-label">Kommentar</label>
|
||||
<input name="comment" id="InputComment" class="form-control"
|
||||
aria-label="Kommentar">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">Anmelden</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Team Modal -->
|
||||
<div class="modal fade" id="teamRegistration" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1"
|
||||
aria-labelledby="teamRegistrationLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="teamRegistrationLabel">Dienst übernehmen</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="/event/{{event.id}}/register_team" method="POST">
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label for="personName" class="form-label">Name</label>
|
||||
<input name="personName" id="personName" type="text" class="form-control"
|
||||
aria-label="Name" list="people">
|
||||
</div>
|
||||
<datalist id="people">
|
||||
{% for household in households %}
|
||||
{% for person in household.name.split(",") %}
|
||||
<option value="{{ person.strip() }}">
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</datalist>
|
||||
<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">
|
||||
{% 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>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">Anmelden</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
43
src/meal_manager/templates/index.html
Normal file
43
src/meal_manager/templates/index.html
Normal file
@@ -0,0 +1,43 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="row mt-4 mb-3">
|
||||
<div class="col d-flex justify-content-between align-items-center">
|
||||
<h2>{% if current_page == "home" %}Kommende{% else %}Vergangene{% endif %} Kochabende</h2>
|
||||
<a href="/event/add" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Neues Event erstellen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div class="card bg-success-subtle text-success-emphasis border-0 shadow-sm text-center">
|
||||
<div class="card-body py-4">
|
||||
<i class="bi bi-calendar-heart fs-3 mb-2"></i>
|
||||
<h5 class="card-title mb-2">Nie wieder die Anmeldung vergessen</h5>
|
||||
<p class="card-text small mb-3">
|
||||
Die Dauerhafte Anmeldung gilt für alle kommenden Kochabende.
|
||||
</p>
|
||||
<a href="/subscribe" class="btn btn-light btn-sm fw-semibold px-3">
|
||||
Jetzt dauerhaft Anmelden
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
|
||||
|
||||
|
||||
{% for event in events %}
|
||||
<div class="col">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<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')
|
||||
}}
|
||||
</p>
|
||||
<p class="card-text">{{ event.description }}</p>
|
||||
<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 %}
|
||||
140
src/meal_manager/templates/subscribe.html
Normal file
140
src/meal_manager/templates/subscribe.html
Normal file
@@ -0,0 +1,140 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mt-4">
|
||||
<!-- Left column: subscription form -->
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col">
|
||||
<div class="card shadow-sm mt-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h1 class="fs-5 mb-0">Dauerhafte Anmeldung zu allen Kochabenden</h1>
|
||||
</div>
|
||||
|
||||
<form action="/subscribe" method="POST">
|
||||
<div class="card-body">
|
||||
<p>
|
||||
Mit einer dauerhaften Anmeldung kannst du dich/euch für alle zukünftigen Kochabende
|
||||
anmelden. Es ist möglich
|
||||
diese Anmeldung auf bestimmte Wochentage zu beschränken.
|
||||
</p>
|
||||
<p>
|
||||
Dauerhafte Anmeldungen werden eine Woche vor einem Kochabend als Anmeldungen für diesen
|
||||
Abend eingetragen. Danach
|
||||
können sie auch noch gelöscht bzw. bearbeitet werden.
|
||||
</p>
|
||||
|
||||
<!-- Info box about 7-day limitation -->
|
||||
<div class="alert alert-info" role="alert">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
<strong>Hinweis:</strong> Neu angelegte dauerhafte Anmeldungen werden erst nach einer Woche aktiv. Für Kochabende, die in weniger als 7 Tagen stattfinden, musst du dich noch separat anmelden.
|
||||
</div>
|
||||
|
||||
<!-- Household selection -->
|
||||
<div class="mb-3">
|
||||
<select name="household" class="form-select" required>
|
||||
<option value="" disabled selected hidden>Wer?</option>
|
||||
{% for household in households %}
|
||||
<option value="{{household.id}}">{{household.name}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<p class="text-muted">
|
||||
Wenn dein Haushalt hier nicht auswählbar ist, besteht bereits eine dauerhafte Anmeldung.
|
||||
Um Änderungen vorzunehmen, lösche die bestehende Anmeldung und lege eine neue an.
|
||||
</p>
|
||||
|
||||
<!-- Person counts -->
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col">
|
||||
<label for="InputAdults" class="form-label">Anzahl Erwachsene</label>
|
||||
<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="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="number" class="form-control"
|
||||
aria-label="Anzahl Kinder <7" min="0" step="1" inputmode="numeric">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Days of the week -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Wochentage auswählen (optional)</label>
|
||||
<p class="text-muted small mb-2">
|
||||
Wenn du nur an bestimmten Tagen teilnehmen möchtest, wähle sie hier aus.
|
||||
</p>
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
{% set days = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag",
|
||||
"Sonntag"] %}
|
||||
{% for day in days %}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="days" value="{{loop.index}}"
|
||||
id="day-{{loop.index}}">
|
||||
<label class="form-check-label" for="day-{{loop.index}}">
|
||||
{{day}}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="card-footer d-flex justify-content-end gap-2">
|
||||
<button type="submit" class="btn btn-primary">Dauerhaft anmelden</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right column: existing registrations -->
|
||||
<div class="col-12 col-lg-6 mt-4 mt-lg-0">
|
||||
<p class="h4 m-2">Bestehende dauerhafte Anmeldungen</p>
|
||||
{% if subscriptions | length == 0 %}
|
||||
<p class="m-2">Es gibt noch keine dauerhaften Anmeldungen</p>
|
||||
{% else %}
|
||||
{% for sub in subscriptions %}
|
||||
<div class="card mb-2">
|
||||
<div class="card-body py-2 px-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<h6 class="mb-0">{{ sub.household.name }}</h6>
|
||||
<a href="/subscribe/{{sub.household.id}}/delete" class="text-danger">
|
||||
<i class="bi bi-trash"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="row g-2">
|
||||
<div class="col-3 text-center">
|
||||
<div class="text-muted" style="font-size: 0.7rem;">Erwachsene</div>
|
||||
<div class="fw-bold small">{{ sub.num_adult_meals }}</div>
|
||||
</div>
|
||||
<div class="col-3 text-center">
|
||||
<div class="text-muted" style="font-size: 0.7rem;">Kinder</div>
|
||||
<div class="fw-bold small">{{ sub.num_children_meals }}</div>
|
||||
</div>
|
||||
<div class="col-3 text-center">
|
||||
<div class="text-muted" style="font-size: 0.7rem;">Kleinkinder</div>
|
||||
<div class="fw-bold small">{{ sub.num_small_children_meals }}</div>
|
||||
</div>
|
||||
<div class="col-3 text-center">
|
||||
<div class="text-muted" style="font-size: 0.7rem;">Tage</div>
|
||||
<div class="fw-bold small">{{ sub.day_string_de() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user