prerelease/0.6.0 #1
@@ -0,0 +1,53 @@
|
|||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from rio import Component, Rectangle, Column, Spacer, Text, Row, TextInput, FlowContainer
|
||||||
|
from rio.event import on_populate
|
||||||
|
|
||||||
|
from elm.components import ElmButton, CateringItemBox
|
||||||
|
from elm.types.CateringTypes import CateringMenuItem, CateringMenuItemCategory
|
||||||
|
|
||||||
|
ITEM_CATEGORY_BY_DISPLAY_CATEGORY: dict[Literal["Frühstück", "Hauptspeisen", "Snacks & Dessert", "Softdrinks", "Alkohol"], list[CateringMenuItemCategory]] = {
|
||||||
|
"Frühstück": [CateringMenuItemCategory.BREAKFAST],
|
||||||
|
"Hauptspeisen": [CateringMenuItemCategory.MAIN_COURSE],
|
||||||
|
"Snacks & Dessert": [CateringMenuItemCategory.SNACK, CateringMenuItemCategory.DESSERT],
|
||||||
|
"Softdrinks": [CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC],
|
||||||
|
"Alkohol": [CateringMenuItemCategory.BEVERAGE_ALCOHOLIC, CateringMenuItemCategory.BEVERAGE_COCKTAIL, CateringMenuItemCategory.BEVERAGE_SHOT]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CateringCategoryDisplay(Component):
|
||||||
|
active_category: Literal["Frühstück", "Hauptspeisen", "Snacks & Dessert", "Softdrinks", "Alkohol"]
|
||||||
|
catering_menu_items: list[CateringMenuItem] = []
|
||||||
|
|
||||||
|
@on_populate
|
||||||
|
async def on_populate(self) -> None:
|
||||||
|
self.catering_menu_items = await CateringMenuItem.find(
|
||||||
|
{
|
||||||
|
"category": {
|
||||||
|
"$in": ITEM_CATEGORY_BY_DISPLAY_CATEGORY[self.active_category]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).to_list()
|
||||||
|
def build(self) -> Component:
|
||||||
|
if len(self.catering_menu_items) <= 0:
|
||||||
|
return Spacer()
|
||||||
|
|
||||||
|
return Rectangle(
|
||||||
|
content=Column(
|
||||||
|
Rectangle(
|
||||||
|
content=Rectangle(
|
||||||
|
content=Text(self.active_category, margin=0.5, selectable=False, overflow="wrap"),
|
||||||
|
fill=self.session.theme.header_box_background_color,
|
||||||
|
margin=0.4
|
||||||
|
),
|
||||||
|
stroke_width=0.1,
|
||||||
|
stroke_color=self.session.theme.box_border_color,
|
||||||
|
),
|
||||||
|
# Items here
|
||||||
|
Column(*[CateringItemBox(i, margin=0.5, grow_y=True) for i in self.catering_menu_items]),
|
||||||
|
Spacer()
|
||||||
|
),
|
||||||
|
fill=self.session.theme.box_color,
|
||||||
|
stroke_width=0.1,
|
||||||
|
stroke_color=self.session.theme.box_border_color
|
||||||
|
)
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from rio import Component, Rectangle, Column, Text, Row, Separator, Color, Checkbox, FlowContainer, IconButton, Icon, Spacer
|
||||||
|
|
||||||
|
from elm.services import AccountingService
|
||||||
|
from elm.components import ElmButton
|
||||||
|
from elm.types.CateringTypes import CateringMenuItem, CateringModificationKey
|
||||||
|
|
||||||
|
|
||||||
|
class CateringItemBox(Component):
|
||||||
|
item: CateringMenuItem
|
||||||
|
|
||||||
|
def make_money_string(self, money: Decimal) -> str:
|
||||||
|
return self.session[AccountingService].make_euro_string_from_decimal(money)
|
||||||
|
|
||||||
|
def build(self) -> Component:
|
||||||
|
base_mods = []
|
||||||
|
extra_mods = []
|
||||||
|
if self.item.active:
|
||||||
|
for modifier_group in self.item.modifier_groups:
|
||||||
|
if modifier_group.key == CateringModificationKey.BASE:
|
||||||
|
base_mods.append(Text("Basis:", fill=self.session.theme.secondary_color, font_size=0.8, margin_top=0.5))
|
||||||
|
container = FlowContainer(spacing=2.5)
|
||||||
|
for option in modifier_group.options:
|
||||||
|
container.children.append(Row(Checkbox(is_on=option.default_selected), Text(option.label), spacing=0.6))
|
||||||
|
base_mods.append(container)
|
||||||
|
if modifier_group.key == CateringModificationKey.EXTRA:
|
||||||
|
extra_mods.append(Text("Extras:", fill=self.session.theme.secondary_color, font_size=0.8, margin_top=0.8))
|
||||||
|
container = FlowContainer(spacing=2.5)
|
||||||
|
for option in modifier_group.options:
|
||||||
|
text = f"{option.label}"
|
||||||
|
if option.price_delta > Decimal("0"):
|
||||||
|
text += f" (+ {self.make_money_string(option.price_delta)})"
|
||||||
|
container.children.append(Row(Checkbox(is_on=option.default_selected), Text(text), spacing=0.6))
|
||||||
|
extra_mods.append(container)
|
||||||
|
|
||||||
|
return Rectangle(
|
||||||
|
content=Column(
|
||||||
|
Row(
|
||||||
|
Column(
|
||||||
|
Row(
|
||||||
|
Text(text=self.item.name, overflow="nowrap", justify="left", font_size=1.1, margin_right=0.8, font_weight="bold", strikethrough=not self.item.active),
|
||||||
|
Text(text=self.make_money_string(self.item.base_price), overflow="ellipsize", justify="left", font_size=0.8, grow_x=True, fill=self.session.theme.primary_color, align_y=1.2)
|
||||||
|
),
|
||||||
|
Text(self.item.description, font_size=0.7, margin_left=2),
|
||||||
|
*base_mods,
|
||||||
|
*extra_mods,
|
||||||
|
spacing=0.5,
|
||||||
|
margin=0.5,
|
||||||
|
grow_x=True
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
Spacer(),
|
||||||
|
Rectangle(
|
||||||
|
content=Icon(icon="material/add_shopping_cart", min_width=2, min_height=2, margin=1),
|
||||||
|
hover_fill=Color.TRANSPARENT if not self.item.active else self.session.theme.hud_color,
|
||||||
|
cursor="not-allowed" if not self.item.active else "pointer",
|
||||||
|
transition_time=0.2
|
||||||
|
),
|
||||||
|
Spacer()
|
||||||
|
)
|
||||||
|
|
||||||
|
),
|
||||||
|
Separator(color=self.session.theme.box_border_color),
|
||||||
|
spacing=0.5,
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -9,3 +9,5 @@ from .PersonalInfoBox import PersonalInfoBox
|
|||||||
from .BuyTicketBox import BuyTicketBox
|
from .BuyTicketBox import BuyTicketBox
|
||||||
from .SeatingPlanPixels import *
|
from .SeatingPlanPixels import *
|
||||||
from .SeatingPlan import *
|
from .SeatingPlan import *
|
||||||
|
from .CateringItemBox import CateringItemBox
|
||||||
|
from .CateringCategoryDisplay import CateringCategoryDisplay
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
from copy import copy
|
||||||
|
from typing import Any, Optional, Literal
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from rio import Component, Column, Row, Text, Spacer, page, Color, Rectangle, TextInput, GuardEvent, SwitcherBar, SwitcherBarChangeEvent
|
||||||
|
|
||||||
|
from elm.types import UserSession, User
|
||||||
|
from elm.types.CateringTypes import *
|
||||||
|
from elm.services import UserService, LocalData, LocalDataService, ConfigurationService
|
||||||
|
from elm.components import ElmButton, CateringCategoryDisplay
|
||||||
|
|
||||||
|
|
||||||
|
@page(name="Catering", url_segment="catering")
|
||||||
|
class CateringPage(Component):
|
||||||
|
active_category: Literal["Frühstück", "Hauptspeisen", "Snacks & Dessert", "Softdrinks", "Alkohol"] = "Hauptspeisen"
|
||||||
|
|
||||||
|
async def on_switcher_bar_change(self, event: SwitcherBarChangeEvent) -> None:
|
||||||
|
self.active_category = event.value
|
||||||
|
print(event)
|
||||||
|
|
||||||
|
def build(self) -> Component:
|
||||||
|
return Row(
|
||||||
|
Column(
|
||||||
|
Rectangle(
|
||||||
|
content=Column(
|
||||||
|
Rectangle(
|
||||||
|
content=SwitcherBar("Frühstück", "Hauptspeisen", "Snacks & Dessert", "Softdrinks", "Alkohol", margin=0.5, selected_value=self.bind().active_category, on_change=self.on_switcher_bar_change),
|
||||||
|
stroke_width=0.1,
|
||||||
|
stroke_color=self.session.theme.box_border_color,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
fill=self.session.theme.box_color,
|
||||||
|
stroke_width=0.1,
|
||||||
|
stroke_color=self.session.theme.box_border_color
|
||||||
|
),
|
||||||
|
CateringCategoryDisplay(active_category=self.active_category, grow_y=True),
|
||||||
|
grow_x=True,
|
||||||
|
spacing=1
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
Rectangle(
|
||||||
|
content=Column(
|
||||||
|
Rectangle(
|
||||||
|
content=Rectangle(
|
||||||
|
content=Text("Warenkorb", margin=0.5, selectable=False, overflow="wrap"),
|
||||||
|
fill=self.session.theme.header_box_background_color,
|
||||||
|
margin=0.4
|
||||||
|
),
|
||||||
|
stroke_width=0.1,
|
||||||
|
stroke_color=self.session.theme.box_border_color,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
fill=self.session.theme.box_color,
|
||||||
|
stroke_width=0.1,
|
||||||
|
stroke_color=self.session.theme.box_border_color
|
||||||
|
),
|
||||||
|
Rectangle(
|
||||||
|
content=Column(
|
||||||
|
Text("ToDo", margin=1),
|
||||||
|
Spacer()
|
||||||
|
),
|
||||||
|
fill=self.session.theme.box_color,
|
||||||
|
stroke_width=0.1,
|
||||||
|
stroke_color=self.session.theme.box_border_color,
|
||||||
|
grow_y=True
|
||||||
|
),
|
||||||
|
spacing=1,
|
||||||
|
min_width=18
|
||||||
|
),
|
||||||
|
spacing=1,
|
||||||
|
margin=1
|
||||||
|
)
|
||||||
@@ -4,7 +4,7 @@ from beanie import init_beanie
|
|||||||
from pymongo import AsyncMongoClient
|
from pymongo import AsyncMongoClient
|
||||||
from pymongo.asynchronous.collection import AsyncCollection
|
from pymongo.asynchronous.collection import AsyncCollection
|
||||||
|
|
||||||
from elm.types import User, Transaction, Ticket, Seat
|
from elm.types import User, Transaction, Ticket, Seat, CateringTypes
|
||||||
from elm.types.ConfigurationTypes import DatabaseConfiguration
|
from elm.types.ConfigurationTypes import DatabaseConfiguration
|
||||||
|
|
||||||
logger = logging.getLogger(__name__.split(".")[-1])
|
logger = logging.getLogger(__name__.split(".")[-1])
|
||||||
@@ -33,5 +33,5 @@ class DatabaseService:
|
|||||||
self._users: AsyncCollection = self._database["users"]
|
self._users: AsyncCollection = self._database["users"]
|
||||||
await init_beanie(
|
await init_beanie(
|
||||||
database=self._database,
|
database=self._database,
|
||||||
document_models=[User, Transaction, Ticket, Seat]
|
document_models=[User, Transaction, Ticket, Seat, CateringTypes.CateringMenuItem, CateringTypes.CateringOrder]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime, UTC
|
||||||
|
from decimal import Decimal
|
||||||
|
from enum import StrEnum
|
||||||
|
from typing import Optional, Annotated
|
||||||
|
|
||||||
|
from beanie import Document, PydanticObjectId, Indexed
|
||||||
|
from bson import Decimal128
|
||||||
|
from pydantic import BaseModel, Field, field_validator, ConfigDict
|
||||||
|
|
||||||
|
class CateringMenuItemCategory(StrEnum):
|
||||||
|
MAIN_COURSE = "MAIN_COURSE"
|
||||||
|
DESSERT = "DESSERT"
|
||||||
|
BEVERAGE_NON_ALCOHOLIC = "BEVERAGE_NON_ALCOHOLIC"
|
||||||
|
BEVERAGE_ALCOHOLIC = "BEVERAGE_ALCOHOLIC"
|
||||||
|
BEVERAGE_COCKTAIL = "BEVERAGE_COCKTAIL"
|
||||||
|
BEVERAGE_SHOT = "BEVERAGE_SHOT"
|
||||||
|
BREAKFAST = "BREAKFAST"
|
||||||
|
SNACK = "SNACK"
|
||||||
|
NON_FOOD = "NON_FOOD"
|
||||||
|
|
||||||
|
class CateringOrderStatus(StrEnum):
|
||||||
|
RECEIVED = "RECEIVED"
|
||||||
|
DELAYED = "DELAYED"
|
||||||
|
READY_FOR_PICKUP = "READY_FOR_PICKUP"
|
||||||
|
EN_ROUTE = "EN_ROUTE"
|
||||||
|
COMPLETED = "COMPLETED"
|
||||||
|
CANCELED = "CANCELED"
|
||||||
|
|
||||||
|
class CateringModificationKey(StrEnum):
|
||||||
|
BASE = "base" # For base ingredients that can be deselected, like butter on bread
|
||||||
|
EXTRA = "extra" # For ingredients that can be added, like ketchup on fries
|
||||||
|
|
||||||
|
class MongoDecimalModel(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
arbitrary_types_allowed=True,
|
||||||
|
json_encoders={
|
||||||
|
Decimal: lambda v: str(v)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@field_validator("*", mode="before", check_fields=False)
|
||||||
|
@classmethod
|
||||||
|
def convert_decimal128(cls, v):
|
||||||
|
if isinstance(v, Decimal128):
|
||||||
|
return v.to_decimal()
|
||||||
|
|
||||||
|
return v
|
||||||
|
|
||||||
|
class CateringModifierOption(MongoDecimalModel):
|
||||||
|
key: str
|
||||||
|
label: str
|
||||||
|
|
||||||
|
default_selected: bool = False
|
||||||
|
|
||||||
|
price_delta: Decimal = Decimal("0.00")
|
||||||
|
|
||||||
|
class CateringModifierGroup(MongoDecimalModel):
|
||||||
|
key: CateringModificationKey
|
||||||
|
label: str
|
||||||
|
|
||||||
|
# True = checkbox group
|
||||||
|
# False = radio button group
|
||||||
|
multi_select: bool = True
|
||||||
|
|
||||||
|
min_selected: int = 0
|
||||||
|
max_selected: Optional[int] = None
|
||||||
|
|
||||||
|
options: list[CateringModifierOption] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class CateringMenuItem(MongoDecimalModel, Document):
|
||||||
|
name: Annotated[str, Indexed(unique=True)]
|
||||||
|
|
||||||
|
category: CateringMenuItemCategory
|
||||||
|
|
||||||
|
description: Optional[str] = None
|
||||||
|
|
||||||
|
base_price: Decimal = Decimal("0.00")
|
||||||
|
|
||||||
|
modifier_groups: list[CateringModifierGroup] = Field(default_factory=list)
|
||||||
|
|
||||||
|
active: bool = True
|
||||||
|
|
||||||
|
created_at: datetime = Field(
|
||||||
|
default_factory=lambda: datetime.now(UTC)
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_at: datetime = Field(
|
||||||
|
default_factory=lambda: datetime.now(UTC)
|
||||||
|
)
|
||||||
|
|
||||||
|
class Settings:
|
||||||
|
name = "catering_menu_items"
|
||||||
|
|
||||||
|
class CateringSelectedModifier(MongoDecimalModel):
|
||||||
|
group_key: str
|
||||||
|
option_key: str
|
||||||
|
|
||||||
|
label: str
|
||||||
|
|
||||||
|
selected: bool
|
||||||
|
|
||||||
|
price_delta: Decimal = Decimal("0.00")
|
||||||
|
|
||||||
|
|
||||||
|
class CateringOrderedItem(MongoDecimalModel):
|
||||||
|
menu_item_id: PydanticObjectId
|
||||||
|
|
||||||
|
# Snapshot fields
|
||||||
|
name: str
|
||||||
|
|
||||||
|
quantity: int = 1
|
||||||
|
|
||||||
|
base_price: Decimal
|
||||||
|
|
||||||
|
selected_modifiers: list[CateringSelectedModifier] = Field(
|
||||||
|
default_factory=list
|
||||||
|
)
|
||||||
|
|
||||||
|
# Final calculated price INCLUDING modifiers
|
||||||
|
final_unit_price: Decimal
|
||||||
|
|
||||||
|
notes: Optional[str] = None
|
||||||
|
|
||||||
|
class CateringOrder(Document):
|
||||||
|
customer_id: PydanticObjectId
|
||||||
|
|
||||||
|
items: list[CateringOrderedItem] = Field(default_factory=list)
|
||||||
|
|
||||||
|
status: CateringOrderStatus = CateringOrderStatus.RECEIVED
|
||||||
|
|
||||||
|
created_at: datetime = Field(
|
||||||
|
default_factory=lambda: datetime.now(UTC)
|
||||||
|
)
|
||||||
|
|
||||||
|
class Settings:
|
||||||
|
name = "catering_orders"
|
||||||
@@ -4,7 +4,7 @@ from typing import Annotated
|
|||||||
|
|
||||||
from beanie import Document, Indexed
|
from beanie import Document, Indexed
|
||||||
from bson import Decimal128
|
from bson import Decimal128
|
||||||
from pydantic import field_validator
|
from pydantic import field_validator, Field
|
||||||
|
|
||||||
|
|
||||||
class Transaction(Document):
|
class Transaction(Document):
|
||||||
@@ -12,7 +12,9 @@ class Transaction(Document):
|
|||||||
value: Decimal
|
value: Decimal
|
||||||
is_debit: bool
|
is_debit: bool
|
||||||
title: str
|
title: str
|
||||||
transaction_date: datetime = datetime.now(UTC)
|
transaction_date: datetime = Field(
|
||||||
|
default_factory=lambda: datetime.now(UTC)
|
||||||
|
)
|
||||||
|
|
||||||
class Settings:
|
class Settings:
|
||||||
name = "transactions"
|
name = "transactions"
|
||||||
@@ -23,3 +25,4 @@ class Transaction(Document):
|
|||||||
if isinstance(v, Decimal128):
|
if isinstance(v, Decimal128):
|
||||||
return v.to_decimal()
|
return v.to_decimal()
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from datetime import date, datetime
|
from datetime import date, datetime, UTC
|
||||||
from typing import Optional, Annotated
|
from typing import Optional, Annotated
|
||||||
|
|
||||||
from beanie import Document, Indexed
|
from beanie import Document, Indexed
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
|
||||||
class User(Document):
|
class User(Document):
|
||||||
@@ -15,7 +16,9 @@ class User(Document):
|
|||||||
user_birth_day: Optional[date] = None
|
user_birth_day: Optional[date] = None
|
||||||
is_active: bool = True
|
is_active: bool = True
|
||||||
is_team_member: bool = False
|
is_team_member: bool = False
|
||||||
created_at: datetime = datetime.now()
|
created_at: datetime = Field(
|
||||||
|
default_factory=lambda: datetime.now(UTC)
|
||||||
|
)
|
||||||
|
|
||||||
class Settings:
|
class Settings:
|
||||||
name = "users"
|
name = "users"
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ from .ConfigurationTypes import *
|
|||||||
from .Transaction import Transaction
|
from .Transaction import Transaction
|
||||||
from .Ticket import Ticket, TicketState
|
from .Ticket import Ticket, TicketState
|
||||||
from .Seat import Seat
|
from .Seat import Seat
|
||||||
|
from .ConfigurationTypes import *
|
||||||
|
|||||||
Reference in New Issue
Block a user