diff --git a/app/components/pages/CashierV2.tsx b/app/components/pages/CashierV2.tsx new file mode 100644 index 00000000..e6294bc2 --- /dev/null +++ b/app/components/pages/CashierV2.tsx @@ -0,0 +1,275 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { Input } from "~/components/ui/input"; +import type { WithId } from "~/lib/typeguard"; +import { type ItemEntity, type2label } from "~/models/item"; +import { OrderEntity } from "~/models/order"; +import { DiscountInput } from "../organisms/DiscountInput"; +import { OrderAlertDialog } from "../organisms/OrderAlertDialog"; +import { OrderItemView } from "../organisms/OrderItemView"; +import { Button } from "../ui/button"; + +const keys = ["a", "s", "d", "f", "g", "h", "j", "k", "l", ";"]; + +const InputStatus = [ + "discount", + "items", + "received", + "description", + "submit", +] as const; + +type props = { + items: WithId[] | undefined; + orders: WithId[] | undefined; + submitPayload: (order: OrderEntity) => void; +}; + +const CashierV2 = ({ items, orders, submitPayload }: props) => { + const [orderItems, setOrderItems] = useState[]>([]); + const [received, setReceived] = useState(""); + const [discountOrderId, setDiscountOrderId] = useState(""); + const [description, setDescription] = useState(""); + const [inputStatus, setInputStatus] = + useState<(typeof InputStatus)[number]>("discount"); + const [dialogOpen, setDialogOpen] = useState(false); + const [itemFocus, setItemFocus] = useState(0); + + const discountOrderIdNum = Number(discountOrderId); + const discountOrder = orders?.find( + (order) => order.orderId === discountOrderIdNum, + ); + const lastPurchasedCups = discountOrder?._getCoffeeCount() ?? 0; + + const curOrderId = + orders?.reduce((acc, cur) => Math.max(acc, cur.orderId), 0) ?? 0; + const nextOrderId = curOrderId + 1; + const newOrder = OrderEntity.createNew({ orderId: nextOrderId }); + const receivedNum = Number(received); + newOrder.items = orderItems; + newOrder.received = receivedNum; + if (description !== "") { + newOrder.description = description; + } + if (discountOrder) { + newOrder.applyDiscount(discountOrder); + } + const charge = newOrder.received - newOrder.billingAmount; + const chargeView: string | number = charge < 0 ? "不足しています" : charge; + + const receivedDOM = useRef(null); + const descriptionDOM = useRef(null); + const discountInputDOM = useRef(null); + + const proceedItemFocus = useCallback(() => { + setItemFocus((prev) => (prev + 1) % orderItems.length); + }, [orderItems]); + + const prevousItemFocus = useCallback(() => { + setItemFocus((prev) => (prev - 1 + orderItems.length) % orderItems.length); + }, [orderItems]); + + useEffect(() => { + const handler = (event: KeyboardEvent) => { + if (inputStatus !== "items") { + return; + } + if (event.key === "ArrowUp") { + prevousItemFocus(); + } + if (event.key === "ArrowDown") { + proceedItemFocus(); + } + }; + window.addEventListener("keydown", handler); + return () => { + window.removeEventListener("keydown", handler); + }; + }, [proceedItemFocus, prevousItemFocus, inputStatus]); + + const proceedStatus = useCallback(() => { + const idx = InputStatus.indexOf(inputStatus); + setInputStatus(InputStatus[(idx + 1) % InputStatus.length]); + }, [inputStatus]); + + const prevousStatus = useCallback(() => { + const idx = InputStatus.indexOf(inputStatus); + setInputStatus( + InputStatus[(idx - 1 + InputStatus.length) % InputStatus.length], + ); + }, [inputStatus]); + + const submitOrder = useCallback(() => { + if (charge < 0) { + return; + } + if (orderItems.length === 0) { + return; + } + submitPayload(newOrder); + setOrderItems([]); + setReceived(""); + setDiscountOrderId(""); + setDescription(""); + setInputStatus("discount"); + }, [charge, newOrder, orderItems, submitPayload]); + + const moveFocus = useCallback(() => { + switch (inputStatus) { + case "discount": + setDialogOpen(false); + discountInputDOM.current?.focus(); + setItemFocus(-1); + break; + case "items": + break; + case "received": + setItemFocus(-1); + receivedDOM.current?.focus(); + break; + case "description": + descriptionDOM.current?.focus(); + setDialogOpen(false); + break; + case "submit": + setDialogOpen(true); + break; + } + }, [inputStatus]); + + useEffect(moveFocus); + + const keyEventHandlers = useMemo(() => { + return { + ArrowRight: proceedStatus, + ArrowLeft: prevousStatus, + Escape: () => { + setInputStatus("discount"); + setDialogOpen(false); + setOrderItems([]); + setReceived(""); + setDiscountOrderId(""); + setDescription(""); + }, + }; + }, [proceedStatus, prevousStatus]); + + useEffect(() => { + const handlers = items?.map((item, idx) => { + const handler = (event: KeyboardEvent) => { + if (inputStatus !== "items") { + return; + } + if (event.key === keys[idx]) { + setOrderItems((prevItems) => [...prevItems, structuredClone(item)]); + } + }; + return handler; + }); + for (const handler of handlers ?? []) { + window.addEventListener("keydown", handler); + } + + return () => { + for (const handler of handlers ?? []) { + window.removeEventListener("keydown", handler); + } + }; + }, [items, inputStatus]); + + useEffect(() => { + const handler = (event: KeyboardEvent) => { + const key = event.key; + for (const [keyName, keyHandler] of Object.entries(keyEventHandlers)) { + if (key === keyName) { + keyHandler(); + } + } + }; + window.addEventListener("keydown", handler); + return () => { + window.removeEventListener("keydown", handler); + }; + }, [keyEventHandlers]); + + return ( + <> +
+
+ {items?.map((item) => ( +
+

{item.name}

+

{item.price}

+

{type2label[item.type]}

+ +
+ ))} +
+
+

操作

+

入力ステータスを移動して一つ一つの項目を入力していきます

+
    +
  • 入力ステータスを移動 ←・→
  • +
  • 注文をクリア: Esc
  • +
+ + +

{`No. ${nextOrderId}`}

+
+

合計金額

+

{newOrder.billingAmount}

+
+ setDiscountOrderId(value)} + disabled={inputStatus !== "discount"} + discountOrder={discountOrder} + lastPurchasedCups={lastPurchasedCups} + /> + setReceived(e.target.value)} + placeholder="お預かり金額を入力" + disabled={inputStatus !== "received"} + ref={receivedDOM} + /> + + setDescription(e.target.value)} + placeholder="備考" + disabled={inputStatus !== "description"} + ref={descriptionDOM} + /> +
+
+

入力ステータス: {inputStatus}

+ +
+
+ + + ); +}; + +export { CashierV2 }; diff --git a/app/routes/cashier-v2.tsx b/app/routes/cashier-v2.tsx index ef7ab96a..e9927903 100644 --- a/app/routes/cashier-v2.tsx +++ b/app/routes/cashier-v2.tsx @@ -1,31 +1,15 @@ import { parseWithZod } from "@conform-to/zod"; import { type ClientActionFunction, useSubmit } from "@remix-run/react"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback } from "react"; import useSWRSubscription from "swr/subscription"; import { z } from "zod"; -import { DiscountInput } from "~/components/organisms/DiscountInput"; -import { OrderAlertDialog } from "~/components/organisms/OrderAlertDialog"; -import { OrderItemView } from "~/components/organisms/OrderItemView"; -import { Button } from "~/components/ui/button"; -import { Input } from "~/components/ui/input"; +import { CashierV2 } from "~/components/pages/CashierV2"; import { itemConverter, orderConverter } from "~/firebase/converter"; import { collectionSub } from "~/firebase/subscription"; import { stringToJSONSchema } from "~/lib/custom-zod"; -import type { WithId } from "~/lib/typeguard"; -import { type ItemEntity, type2label } from "~/models/item"; import { OrderEntity, orderSchema } from "~/models/order"; import { orderRepository } from "~/repositories/order"; -const keys = ["a", "s", "d", "f", "g", "h", "j", "k", "l", ";"]; - -const InputStatus = [ - "discount", - "items", - "received", - "description", - "submit", -] as const; - export default function Cashier() { const { data: items } = useSWRSubscription( "items", @@ -36,253 +20,19 @@ export default function Cashier() { collectionSub({ converter: orderConverter }), ); const submit = useSubmit(); - const [orderItems, setOrderItems] = useState[]>([]); - const [received, setReceived] = useState(""); - const [discountOrderId, setDiscountOrderId] = useState(""); - const [description, setDescription] = useState(""); - const [inputStatus, setInputStatus] = - useState<(typeof InputStatus)[number]>("discount"); - const [dialogOpen, setDialogOpen] = useState(false); - const [itemFocus, setItemFocus] = useState(0); - const discountOrderIdNum = Number(discountOrderId); - const discountOrder = orders?.find( - (order) => order.orderId === discountOrderIdNum, + const submitPayload = useCallback( + (newOrder: OrderEntity) => { + submit( + { newOrder: JSON.stringify(newOrder.toOrder()) }, + { method: "POST" }, + ); + }, + [submit], ); - const lastPurchasedCups = discountOrder?._getCoffeeCount() ?? 0; - - const curOrderId = - orders?.reduce((acc, cur) => Math.max(acc, cur.orderId), 0) ?? 0; - const nextOrderId = curOrderId + 1; - const newOrder = OrderEntity.createNew({ orderId: nextOrderId }); - const receivedNum = Number(received); - newOrder.items = orderItems; - newOrder.received = receivedNum; - if (description !== "") { - newOrder.description = description; - } - if (discountOrder) { - newOrder.applyDiscount(discountOrder); - } - const charge = newOrder.received - newOrder.billingAmount; - const chargeView: string | number = charge < 0 ? "不足しています" : charge; - - const receivedDOM = useRef(null); - const descriptionDOM = useRef(null); - const discountInputDOM = useRef(null); - - const proceedItemFocus = useCallback(() => { - setItemFocus((prev) => (prev + 1) % orderItems.length); - }, [orderItems]); - - const prevousItemFocus = useCallback(() => { - setItemFocus((prev) => (prev - 1 + orderItems.length) % orderItems.length); - }, [orderItems]); - - useEffect(() => { - const handler = (event: KeyboardEvent) => { - if (inputStatus !== "items") { - return; - } - if (event.key === "ArrowUp") { - prevousItemFocus(); - } - if (event.key === "ArrowDown") { - proceedItemFocus(); - } - }; - window.addEventListener("keydown", handler); - return () => { - window.removeEventListener("keydown", handler); - }; - }, [proceedItemFocus, prevousItemFocus, inputStatus]); - - const proceedStatus = useCallback(() => { - const idx = InputStatus.indexOf(inputStatus); - setInputStatus(InputStatus[(idx + 1) % InputStatus.length]); - }, [inputStatus]); - - const prevousStatus = useCallback(() => { - const idx = InputStatus.indexOf(inputStatus); - setInputStatus( - InputStatus[(idx - 1 + InputStatus.length) % InputStatus.length], - ); - }, [inputStatus]); - - const submitOrder = useCallback(() => { - if (charge < 0) { - return; - } - if (orderItems.length === 0) { - return; - } - submit( - { newOrder: JSON.stringify(newOrder.toOrder()) }, - { method: "POST" }, - ); - setOrderItems([]); - setReceived(""); - setDiscountOrderId(""); - setDescription(""); - setInputStatus("discount"); - }, [charge, newOrder, orderItems, submit]); - - const moveFocus = useCallback(() => { - switch (inputStatus) { - case "discount": - setDialogOpen(false); - discountInputDOM.current?.focus(); - setItemFocus(-1); - break; - case "items": - break; - case "received": - setItemFocus(-1); - receivedDOM.current?.focus(); - break; - case "description": - descriptionDOM.current?.focus(); - setDialogOpen(false); - break; - case "submit": - setDialogOpen(true); - break; - } - }, [inputStatus]); - - useEffect(moveFocus); - - const keyEventHandlers = useMemo(() => { - return { - ArrowRight: proceedStatus, - ArrowLeft: prevousStatus, - Escape: () => { - setInputStatus("discount"); - setDialogOpen(false); - setOrderItems([]); - setReceived(""); - setDiscountOrderId(""); - setDescription(""); - }, - }; - }, [proceedStatus, prevousStatus]); - - useEffect(() => { - const handlers = items?.map((item, idx) => { - const handler = (event: KeyboardEvent) => { - if (inputStatus !== "items") { - return; - } - if (event.key === keys[idx]) { - setOrderItems((prevItems) => [...prevItems, structuredClone(item)]); - } - }; - return handler; - }); - for (const handler of handlers ?? []) { - window.addEventListener("keydown", handler); - } - - return () => { - for (const handler of handlers ?? []) { - window.removeEventListener("keydown", handler); - } - }; - }, [items, inputStatus]); - - useEffect(() => { - const handler = (event: KeyboardEvent) => { - const key = event.key; - for (const [keyName, keyHandler] of Object.entries(keyEventHandlers)) { - if (key === keyName) { - keyHandler(); - } - } - }; - window.addEventListener("keydown", handler); - return () => { - window.removeEventListener("keydown", handler); - }; - }, [keyEventHandlers]); return ( - <> -
-
- {items?.map((item) => ( -
-

{item.name}

-

{item.price}

-

{type2label[item.type]}

- -
- ))} -
-
-

操作

-

入力ステータスを移動して一つ一つの項目を入力していきます

-
    -
  • 入力ステータスを移動 ←・→
  • -
  • 注文をクリア: Esc
  • -
- - -

{`No. ${nextOrderId}`}

-
-

合計金額

-

{newOrder.billingAmount}

-
- setDiscountOrderId(value)} - disabled={inputStatus !== "discount"} - discountOrder={discountOrder} - lastPurchasedCups={lastPurchasedCups} - /> - setReceived(e.target.value)} - placeholder="お預かり金額を入力" - disabled={inputStatus !== "received"} - ref={receivedDOM} - /> - - setDescription(e.target.value)} - placeholder="備考" - disabled={inputStatus !== "description"} - ref={descriptionDOM} - /> -
-
-

入力ステータス: {inputStatus}

- -
-
- - + ); }