Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

注文画面の UI や確認ダイアログの追加 #65

Merged
merged 8 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 43 additions & 16 deletions app/routers/orders.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,33 @@
from typing import Annotated
from uuid import UUID, uuid4

from fastapi import APIRouter, Form, Header, HTTPException, Request, Response, status
from fastapi.responses import HTMLResponse

from .. import templates
from ..store import PlacedItemTable, PlacementTable, Product, ProductTable
from ..store.product import ProductCompact

router = APIRouter()

# NOTE: Do NOT store this data in database (the data is transient and should be kept in memory)
# NOTE: Or should this be optionally stored in database?
order_sessions: dict[int, list[Product | None]] = {}
order_sessions: dict[int, dict[UUID, Product]] = {}
last_session_id = 0


def create_new_session() -> int:
global last_session_id
last_session_id += 1
new_session_id = last_session_id
order_sessions[new_session_id] = []
order_sessions[new_session_id] = {}
return new_session_id


def compute_total_price(order_items: list[Product | None]) -> str:
def compute_total_price(order_items: dict[UUID, Product]) -> str:
total_price = 0
for item in order_items:
if item is not None:
total_price += item.price
for item in order_items.values():
total_price += item.price
return Product.to_price_str(total_price)


Expand All @@ -42,8 +43,7 @@ async def create_new_order():

@router.get("/orders/{session_id}", response_class=HTMLResponse)
async def get_order_session(request: Request, session_id: int):
order_items = order_sessions.get(session_id)
if order_items is None:
if (order_items := order_sessions.get(session_id)) is None:
raise HTTPException(status_code=404, detail=f"Session {session_id} not found")

total_price = compute_total_price(order_items)
Expand All @@ -53,20 +53,49 @@ async def get_order_session(request: Request, session_id: int):
)


@router.get("/orders/{session_id}/confirm", response_class=HTMLResponse)
async def get_order_session_to_confirm(request: Request, session_id: int):
if (order_items := order_sessions.get(session_id)) is None:
raise HTTPException(status_code=404, detail=f"Session {session_id} not found")
if len(order_items) == 0:
total_price = Product.to_price_str(0)
placement_status = "エラー:商品が選択されていません"
else:
placement_status = ""
total_price = 0
products: dict[int, ProductCompact] = {}
for item in order_items.values():
total_price += item.price
if item.product_id in products:
products[item.product_id].count += 1
else:
products[item.product_id] = ProductCompact(item.name, item.price)
return HTMLResponse(
templates.components.order_confirm(
request,
session_id,
products,
len(order_items),
Product.to_price_str(total_price),
placement_status,
)
)


@router.post("/orders/{session_id}", response_class=HTMLResponse)
async def place_order(request: Request, session_id: int):
if (order_items := order_sessions.get(session_id)) is None:
raise HTTPException(status_code=404, detail=f"Session {session_id} not found")

if len(list(filter(lambda x: x is not None, order_items))) == 0:
if len(order_items) == 0:
total_price = Product.to_price_str(0)
placement_status = "エラー:商品が選択されていません"
order_frozen = False
else:
order_sessions.pop(session_id)

total_price = compute_total_price(order_items)
product_ids = [item.product_id for item in order_items if item is not None]
product_ids = [item.product_id for item in order_items.values()]
placement_id = await PlacedItemTable.issue(product_ids)
# TODO: add a branch for out of stock error
await PlacementTable.insert(placement_id)
Expand Down Expand Up @@ -111,7 +140,7 @@ async def add_order_item(
status_code=404, detail=f"Session {session_id} not found"
)

order_items.append(product)
order_items[uuid4()] = product
return HTMLResponse(
templates.components.order_session(
request, session_id, order_items, compute_total_price(order_items)
Expand All @@ -120,13 +149,11 @@ async def add_order_item(


@router.delete("/orders/{session_id}/item/{index}", response_class=HTMLResponse)
async def delete_order_item(request: Request, session_id: int, index: int):
async def delete_order_item(request: Request, session_id: int, index: UUID):
if (order_items := order_sessions.get(session_id)) is None:
raise HTTPException(status_code=404, detail=f"Session {session_id} not found")
if order_items[index] is None:
raise HTTPException(status_code=404, detail=f"Order item {index} not found")

order_items[index] = None
order_items.pop(index)
return HTMLResponse(
templates.components.order_session(
request, session_id, order_items, compute_total_price(order_items)
Expand Down Expand Up @@ -160,7 +187,7 @@ async def clear_order_items(

return HTMLResponse(
templates.components.order_session(
request, session_id, [], Product.to_price_str(0)
request, session_id, {}, Product.to_price_str(0)
)
)

Expand Down
7 changes: 7 additions & 0 deletions app/store/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ def to_price_str(price: int) -> str:
return f"¥{price:,}"


class ProductCompact:
def __init__(self, name: str, price: int):
self.name = name
self.price = Product.to_price_str(price)
self.count = 1


class Table:
def __init__(self, database: Database):
self._db = database
Expand Down
16 changes: 14 additions & 2 deletions app/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from functools import wraps
from pathlib import Path
from typing import Any, Callable, Protocol
from uuid import UUID

import jinja2
from fastapi import Request
Expand All @@ -10,6 +11,7 @@

from .env import DEBUG
from .store import Product, placements_t
from .store.product import ProductCompact

TEMPLATES_DIR = Path("app/templates")

Expand Down Expand Up @@ -86,7 +88,7 @@ def products(products: list[Product]): ...
def orders(
session_id: int,
products: list[Product],
order_items: list[Product | None],
order_items: dict[UUID, Product],
total_price: str,
placement_status: str = "",
order_frozen: bool = False,
Expand Down Expand Up @@ -115,7 +117,7 @@ def product_editor(product: Product | None): ...
@staticmethod
def order_session(
session_id: int,
order_items: list[Product | None],
order_items: dict[UUID, Product],
total_price: str,
placement_status: str = "",
order_frozen: bool = False,
Expand All @@ -124,3 +126,13 @@ def order_session(
# @macro_template("components/incoming-placements.html")
# @staticmethod
# def incoming_placements(placements: placements_t): ...

@macro_template("components/order-confirm.html")
@staticmethod
def order_confirm(
session_id: int,
products: dict[int, ProductCompact],
count: int,
total_price: str,
placement_status: str = "",
): ...
56 changes: 56 additions & 0 deletions app/templates/components/order-confirm.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{% macro order_confirm(session_id, products, count, total_price, placement_status) %}
<div class="relative z-10" aria-labelledby="confirm-dialog" role="dialog" aria-modal="true">
<!-- background -->
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true"></div>
<div class="fixed inset-0 z-10 w-screen">
<div class="flex h-full items-end justify-center py-4 px-10 text-center">
<!-- dialog -->
<div class="flex flex-col overflow-y-auto transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all max-h-full">
<!-- text area wrapper -->
<div class="flex flex-col overflow-y-auto bg-white px-4 pb-4 pt-5">
<!-- text area -->
{% if placement_status == "" %}
<div class="flex flex-col overflow-y-auto mt-1 text-center">
<h3 class=" font-semibold leading-6 text-gray-900 text-lg">注文の確定</h3>
<div class="flex flex-col overflow-y-auto mt-5">
<ul class="flex flex-col overflow-y-auto mx-1 px-3">
{% for product in products.values() %}
<li class="flex flex-row justify-between items-start gap-x-6 text-lg">
<span class="break-words">{{ product.name }}</span>
<span class="whitespace-nowrap">{{ product.price }} x {{ product.count }}</span>
</li>
{% endfor %}
</ul>
<p class="flex flex-row mx-1 px-3 mt-5 justify-between text-lg">
<span class="break-words">計</span>
<span class="whitespace-nowrap">{{ count }} 点</span>
</p>
<p class="flex flex-row mx-1 px-3 justify-between text-lg">
<span class="break-words">合計金額</span>
<span class="whitespace-nowrap">{{ total_price }}</span>
</p>
</div>
</div>
{% else %}
{{ placement_status }}
{% endif %}
</div>
<!-- control -->
<div class="flex-none bg-gray-50 px-4 py-3">
<button
type="button"
hx-post="/orders/{{ session_id }}"
hx-target="#order-session"
class="inline-flex w-full justify-center rounded bg-blue-600 px-8 py-4 text-xl font-semibold text-white shadow-sm"
>確認</button>
<button
type="button"
onclick="htmx.swap('#order-confirm', '', {swapStyle: 'innerHTML'})"
class="mt-3 inline-flex w-full justify-center rounded bg-white px-5 py-4 text-xl font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300"
>キャンセル</button>
</div>
</div>
</div>
</div>
</div>
{% endmacro %}
47 changes: 25 additions & 22 deletions app/templates/components/order-session.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,41 @@
{# `flex-col-reverse` lets the browser to pin scroll to bottom #}
<div class="flex flex-col-reverse overflow-y-auto">
<ul class="text-lg divide-y-4">
{% for product in order_items %}
{% if product is not none %}
<li id="item-{{ loop.index0 }}" class="flex justify-between">
<div class="overflow-x-auto whitespace-nowrap sm:flex sm:flex-1 sm:justify-between p-4">
<p class="sm:flex-1">{{ product.name }}</p>
<div>{{ product.price_str() }}</div>
{% for uuid, product in order_items.items() %}
<li id="item-{{ uuid }}" class="flex justify-between">
<div class="overflow-x-auto whitespace-nowrap sm:flex sm:flex-1 sm:justify-between p-4">
<p class="sm:flex-1">{{ product.name }}</p>
<div>{{ product.price_str() }}</div>
</div>
{% if order_frozen == false %}
<div class="flex items-center">
<button
hx-delete="/orders/{{ session_id }}/item/{{ uuid }}"
hx-target="#order-session"
class="font-bold text-red-600 bg-gray-200 px-2 rounded"
>X</button>
</div>
{% if order_frozen == false %}
<div class="flex items-center">
<button
hx-delete="/orders/{{ session_id }}/item/{{ loop.index0 }}"
hx-target="#order-session"
class="font-bold text-red-600 bg-gray-200 px-2 rounded"
>X</button>
</div>
{% endif %}
</li>
{% endif %}
{% endif %}
</li>
{% endfor %}
</ul>
</div>
{% if placement_status != "" %}
<div>{{ placement_status }}</div>
{% endif %}
<div class="flex flex-row p-2 items-center">
<div class="basis-2/4">{{ placement_status }}</div>
<div class="basis-1/4 text-center">{{ total_price }}</div>
<div class="basis-1/4 text-right text-xl">{{ order_items | length }} 点</div>
<div class="basis-2/4 text-center text-xl">合計: {{ total_price }}</div>
{% if order_frozen == true %}
<button hx-post="/orders" class="basis-1/4 text-xl text-center px-2 rounded bg-gray-300">新規</button>
{% else %}
<button
hx-post="/orders/{{ session_id }}"
hx-target="#order-session"
class="basis-1/4 text-xl text-center px-2 rounded bg-gray-300"
hx-get="/orders/{{ session_id }}/confirm"
hx-target="#order-confirm"
class="basis-1/4 text-xl text-center px-2 py-2 rounded bg-gray-300 disabled:cursor-not-allowed disabled:text-gray-700 disabled:bg-gray-100"
{{ 'disabled' if order_items | length == 0 else '' }}
>確定</button>
<div id="order-confirm"></div>
{% endif %}
</div>
{% endmacro %}
Loading