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

/cashier-v2 リファクタリング #174

Merged
merged 14 commits into from
Sep 30, 2024
28 changes: 28 additions & 0 deletions app/components/molecules/ThreeDigitsInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { REGEXP_ONLY_DIGITS } from "input-otp";
import {
type ComponentPropsWithoutRef,
type ElementRef,
forwardRef,
} from "react";
import { InputOTP, InputOTPGroup, InputOTPSlot } from "../ui/input-otp";

// 3桁の数字を入力するためのコンポーネント
const ThreeDigitsInput = forwardRef<
ElementRef<typeof InputOTP>,
Omit<
ComponentPropsWithoutRef<typeof InputOTP>,
"maxLength" | "pattern" | "render"
>
>(({ ...props }, ref) => {
return (
<InputOTP ref={ref} maxLength={3} pattern={REGEXP_ONLY_DIGITS} {...props}>
<InputOTPGroup>
<InputOTPSlot index={0} className="font-mono text-3xl" />
<InputOTPSlot index={1} className="font-mono text-3xl" />
<InputOTPSlot index={2} className="font-mono text-3xl" />
</InputOTPGroup>
</InputOTP>
);
});

export { ThreeDigitsInput };
30 changes: 30 additions & 0 deletions app/components/organisms/DiscountInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {
type ComponentPropsWithoutRef,
type ElementRef,
forwardRef,
} from "react";
import type { WithId } from "~/lib/typeguard";
import type { OrderEntity } from "~/models/order";
import { ThreeDigitsInput } from "../molecules/ThreeDigitsInput";

// 割引券番号を入力するためのコンポーネント
const DiscountInput = forwardRef<
ElementRef<typeof ThreeDigitsInput>,
ComponentPropsWithoutRef<typeof ThreeDigitsInput> & {
discountOrder: WithId<OrderEntity> | undefined;
lastPurchasedCups: number;
}
>(({ discountOrder, lastPurchasedCups, ...props }, ref) => {
return (
<div>
<p>割引券番号</p>
<ThreeDigitsInput ref={ref} {...props} />
<p>
{discountOrder === undefined ? "見つかりません" : null}
{discountOrder && `有効杯数: ${lastPurchasedCups}`}
</p>
</div>
);
});

export { DiscountInput };
96 changes: 96 additions & 0 deletions app/components/organisms/ItemAssign.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import {
type Dispatch,
type SetStateAction,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import type { WithId } from "~/lib/typeguard";
import { cn } from "~/lib/utils";
import { type ItemEntity, type2label } from "~/models/item";
import { Input } from "../ui/input";

type props = {
item: WithId<ItemEntity>;
idx: number;
setOrderItems: Dispatch<SetStateAction<WithId<ItemEntity>[]>>;
focus: boolean;
};

const ItemAssign = ({ item, idx, setOrderItems, focus }: props) => {
const [edit, setEdit] = useState(false);
const [assignee, setAssinee] = useState<string | null>(null);

const assignInputRef = useRef<HTMLInputElement>(null);

const closeAssignInput = useCallback(() => {
setOrderItems((prevItems) => {
const newItems = [...prevItems];
newItems[idx].assignee = assignee;
return newItems;
});
setEdit(false);
}, [idx, assignee, setOrderItems]);

// edit の状態に応じて assign 入力欄を開くか閉じる
const change = useCallback(() => {
if (edit) {
closeAssignInput();
} else {
setEdit(true);
}
}, [edit, closeAssignInput]);

// focus が変化したときに assign 入力欄を閉じる
useEffect(() => {
if (!focus) {
closeAssignInput();
}
}, [focus, closeAssignInput]);

// Enter が押されたときに assign 入力欄を開く
useEffect(() => {
const handler = (event: KeyboardEvent) => {
if (event.key === "Enter") {
change();
}
};
if (focus) {
window.addEventListener("keydown", handler);
}
return () => {
window.removeEventListener("keydown", handler);
};
}, [focus, change]);

// edit が true に変化したとき assign 入力欄にフォーカスする
useEffect(() => {
if (edit) {
assignInputRef.current?.focus();
}
}, [edit]);

return (
<div className={cn("grid grid-cols-2", focus && "bg-orange-500")}>
<p className="font-bold text-lg">{idx + 1}</p>
<div>
<p>{item.name}</p>
<p>{item.price}</p>
<p>{type2label[item.type]}</p>
{edit ? (
<Input
ref={assignInputRef}
value={assignee ?? ""}
onChange={(e) => setAssinee(e.target.value || null)}
placeholder="指名"
/>
) : (
<p>{item.assignee ?? "指名なし"}</p>
)}
</div>
</div>
);
};

export { ItemAssign };
70 changes: 70 additions & 0 deletions app/components/organisms/OrderAlertDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { type ComponentPropsWithoutRef, forwardRef } from "react";
import { type2label } from "~/models/item";
import type { OrderEntity } from "~/models/order";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "../ui/alert-dialog";

// 確定前にオーダーの内容を表示するダイアログ
const OrderAlertDialog = forwardRef<
null,
ComponentPropsWithoutRef<typeof AlertDialog> & {
order: OrderEntity;
chargeView: number | string;
onConfirm: () => void;
}
>(({ order, chargeView, onConfirm, ...props }) => {
return (
<AlertDialog {...props}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>オーダーを確定しますか?</AlertDialogTitle>
<AlertDialogDescription>
以下の内容で提出します
</AlertDialogDescription>
{order.items.map((item, idx) => (
<AlertDialogDescription key={`${idx}-${item.id}`}>
{`${idx + 1} ―― ${item.name} ¥${item.price} ${type2label[item.type]}`}
</AlertDialogDescription>
))}
<AlertDialogDescription>
合計金額: &yen;{order.total}
</AlertDialogDescription>
{order.discountInfo.previousOrderId !== null && (
<AlertDialogDescription>
割引: -&yen;{order.discountInfo.discount}
</AlertDialogDescription>
)}
<AlertDialogDescription>
支払金額: &yen;{order.billingAmount}
</AlertDialogDescription>
<AlertDialogDescription>
お預かり金額: &yen;{order.received}
</AlertDialogDescription>
<AlertDialogDescription>
お釣り: &yen;{chargeView}
</AlertDialogDescription>
<AlertDialogDescription>
備考: {order.description}
</AlertDialogDescription>
<AlertDialogDescription>
Tabで選択し、Enterで確定
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>キャンセル</AlertDialogCancel>
<AlertDialogAction onClick={onConfirm}>確定</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
});

export { OrderAlertDialog };
51 changes: 51 additions & 0 deletions app/components/organisms/OrderItemView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { WithId } from "~/lib/typeguard";
import type { ItemEntity } from "~/models/item";
import type { OrderEntity } from "~/models/order";
import { ItemAssign } from "./ItemAssign";

type props = {
order: OrderEntity;
setOrderItems: React.Dispatch<React.SetStateAction<WithId<ItemEntity>[]>>;
inputStatus: "items" | "discount" | "received" | "description" | "submit";
itemFocus: number;
setItemFocus: React.Dispatch<React.SetStateAction<number>>;
discountOrder: boolean;
};

// オーダーのアイテムや割引情報を表示するコンポーネント
const OrderItemView = ({
inputStatus,
discountOrder,
setOrderItems,
itemFocus,
order,
}: props) => {
return (
<>
{inputStatus === "items" && (
<>
<p>商品を追加: キーボードの a, s, d, f, g, h, j, k, l, ;</p>
<p>↑・↓でアイテムのフォーカスを移動</p>
<p>Enterで指名の入力欄を開く</p>
</>
)}
{order.items.map((item, idx) => (
<ItemAssign
key={`${idx}-${item.id}`}
item={item}
idx={idx}
setOrderItems={setOrderItems}
focus={idx === itemFocus}
/>
))}
{discountOrder && (
<div className="grid grid-cols-2">
<p className="font-bold text-lg">割引</p>
<div>-&yen;{order.discountInfo.discount}</div>
</div>
)}
</>
);
};

export { OrderItemView };
Loading