From 7a27e88284cf0109f502b2355c6419f29157a9a4 Mon Sep 17 00:00:00 2001 From: toririm Date: Wed, 30 Oct 2024 15:30:17 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=E3=81=A8=E3=82=8A=E3=81=82=E3=81=88?= =?UTF-8?q?=E3=81=9A=E5=90=8C=E6=9C=9F=E3=81=A7=E3=81=8D=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/firebase-utils/converter.ts | 42 +++++++++++++ common/firebase-utils/firestore.ts | 10 ++- common/models/global.ts | 59 ++++++++++++++++++ common/repositories/global.ts | 62 +++++++++++++++++++ firestore.rules | 3 + .../functional/useSyncCahiserOrder.ts | 11 ++++ pos/app/components/pages/CashierV2.tsx | 5 +- pos/app/routes/cashier.tsx | 53 +++++++++++++++- 8 files changed, 239 insertions(+), 6 deletions(-) create mode 100644 common/models/global.ts create mode 100644 common/repositories/global.ts create mode 100644 pos/app/components/functional/useSyncCahiserOrder.ts diff --git a/common/firebase-utils/converter.ts b/common/firebase-utils/converter.ts index 427fbfe2..ec75ae8e 100644 --- a/common/firebase-utils/converter.ts +++ b/common/firebase-utils/converter.ts @@ -6,6 +6,11 @@ import { Timestamp, } from "firebase/firestore"; import _ from "lodash"; +import { + type GlobalCashierState, + MasterStateEntity, + globalStatSchema, +} from "models/global"; import type { ZodSchema } from "zod"; import type { WithId } from "../lib/typeguard"; import { ItemEntity, itemSchema } from "../models/item"; @@ -95,3 +100,40 @@ export const orderConverter: FirestoreDataConverter> = { return OrderEntity.fromOrder(convertedData); }, }; + +export const cashierStateConverter: FirestoreDataConverter = + { + toFirestore: converter(globalStatSchema).toFirestore, + fromFirestore: ( + snapshot: QueryDocumentSnapshot, + options: SnapshotOptions, + ) => { + const convertedData = converter(globalStatSchema).fromFirestore( + snapshot, + options, + ); + + if (convertedData.id === "cashier-state") { + return convertedData; + } + throw new Error("Invalid data"); + }, + }; + +export const masterStateConverter: FirestoreDataConverter = { + toFirestore: converter(globalStatSchema).toFirestore, + fromFirestore: ( + snapshot: QueryDocumentSnapshot, + options: SnapshotOptions, + ) => { + const convertedData = converter(globalStatSchema).fromFirestore( + snapshot, + options, + ); + + if (convertedData.id === "master-state") { + return MasterStateEntity.fromMasterState(convertedData); + } + throw new Error("Invalid data"); + }, +}; diff --git a/common/firebase-utils/firestore.ts b/common/firebase-utils/firestore.ts index 8f8cbfca..12ce04ac 100644 --- a/common/firebase-utils/firestore.ts +++ b/common/firebase-utils/firestore.ts @@ -1,7 +1,7 @@ -import { initializeApp } from "firebase/app"; -import { getFirestore } from "firebase/firestore"; +import { type FirebaseOptions, initializeApp } from "firebase/app"; +import { getFirestore, initializeFirestore } from "firebase/firestore"; -const firebaseConfig = { +const firebaseConfig: FirebaseOptions = { apiKey: "AIzaSyC3llKAZQOVQEFV0-0xHiseDB55YXJilHM", authDomain: "cafeore-2024.firebaseapp.com", projectId: "cafeore-2024", @@ -12,4 +12,8 @@ const firebaseConfig = { const app = initializeApp(firebaseConfig); +initializeFirestore(app, { + ignoreUndefinedProperties: true, +}); + export const prodDB = getFirestore(app); diff --git a/common/models/global.ts b/common/models/global.ts new file mode 100644 index 00000000..ebe9f1d0 --- /dev/null +++ b/common/models/global.ts @@ -0,0 +1,59 @@ +import { z } from "zod"; +import { orderSchema } from "./order"; + +export const GlobalCashierStateSchema = z.object({ + id: z.literal("cashier-state"), + edittingOrder: orderSchema, +}); + +export type GlobalCashierState = z.infer; + +const orderStatTypes = ["stop", "operational"] as const; + +export const orderStatSchema = z.object({ + createdAt: z.date(), + type: z.enum(orderStatTypes), +}); + +export type OrderStatType = (typeof orderStatTypes)[number]; +export type OrderStat = z.infer; + +export const GlobalMasterStateSchema = z.object({ + id: z.literal("master-state"), + orderStats: z.array(orderStatSchema), +}); + +export type GlobalMasterState = z.infer; + +export const globalStatSchema = z.union([ + GlobalCashierStateSchema, + GlobalMasterStateSchema, +]); + +export type GlobalStat = z.infer; + +export class MasterStateEntity implements GlobalMasterState { + constructor( + public id: "master-state", + private _orderStats: OrderStat[], + ) {} + + static fromMasterState(state: GlobalMasterState): MasterStateEntity { + return new MasterStateEntity(state.id, state.orderStats); + } + + static createNew(): MasterStateEntity { + return new MasterStateEntity("master-state", []); + } + + get orderStats() { + return this._orderStats; + } + + addOrderStat(stat: OrderStatType) { + this._orderStats.push({ + createdAt: new Date(), + type: stat, + }); + } +} diff --git a/common/repositories/global.ts b/common/repositories/global.ts new file mode 100644 index 00000000..7e8202eb --- /dev/null +++ b/common/repositories/global.ts @@ -0,0 +1,62 @@ +import { + cashierStateConverter, + masterStateConverter, +} from "firebase-utils/converter"; +import { prodDB } from "firebase-utils/firestore"; +import { type Firestore, doc, getDoc, setDoc } from "firebase/firestore"; +import type { GlobalCashierState, MasterStateEntity } from "models/global"; + +export type CashierStateRepo = { + get: () => Promise; + set: (state: GlobalCashierState) => Promise; +}; + +export type MasterStateRepo = { + get: () => Promise; + set: (state: MasterStateEntity) => Promise; +}; + +export const cashierStateRepoFactory = (db: Firestore): CashierStateRepo => { + return { + get: async () => { + const docRef = doc(db, "global", "cashier-state").withConverter( + cashierStateConverter, + ); + const docSnap = await getDoc(docRef); + const data = docSnap.data(); + if (data?.id === "cashier-state") { + return data; + } + }, + set: async (state) => { + const docRef = doc(db, "global", "cashier-state").withConverter( + cashierStateConverter, + ); + await setDoc(docRef, state); + }, + }; +}; + +export const masterStateRepoFactory = (db: Firestore): MasterStateRepo => { + return { + get: async () => { + const docRef = doc(db, "global", "master-state").withConverter( + masterStateConverter, + ); + const docSnap = await getDoc(docRef); + const data = docSnap.data(); + if (data?.id === "master-state") { + return data; + } + }, + set: async (state) => { + const docRef = doc(db, "global", "master-state").withConverter( + masterStateConverter, + ); + await setDoc(docRef, state); + }, + }; +}; + +export const cashierRepository = cashierStateRepoFactory(prodDB); +export const masterRepository = masterStateRepoFactory(prodDB); diff --git a/firestore.rules b/firestore.rules index e039e4a6..e64150ca 100644 --- a/firestore.rules +++ b/firestore.rules @@ -8,5 +8,8 @@ service cloud.firestore { match /orders/{orderId} { allow read, write: if true; } + match /global/{document=**} { + allow read, write: if true; + } } } \ No newline at end of file diff --git a/pos/app/components/functional/useSyncCahiserOrder.ts b/pos/app/components/functional/useSyncCahiserOrder.ts new file mode 100644 index 00000000..717d309f --- /dev/null +++ b/pos/app/components/functional/useSyncCahiserOrder.ts @@ -0,0 +1,11 @@ +import type { OrderEntity } from "common/models/order"; +import { useEffect } from "react"; + +export const useSyncCahiserOrder = ( + order: OrderEntity, + syncOrder: (order: OrderEntity) => void, +) => { + useEffect(() => { + syncOrder(order); + }, [order, syncOrder]); +}; diff --git a/pos/app/components/pages/CashierV2.tsx b/pos/app/components/pages/CashierV2.tsx index 9d485f31..544ba4f4 100644 --- a/pos/app/components/pages/CashierV2.tsx +++ b/pos/app/components/pages/CashierV2.tsx @@ -9,6 +9,7 @@ import { useInputStatus } from "../functional/useInputStatus"; import { useLatestOrderId } from "../functional/useLatestOrderId"; import { useOrderState } from "../functional/useOrderState"; import { usePreventNumberKeyUpDown } from "../functional/usePreventNumberKeyUpDown"; +import { useSyncCahiserOrder } from "../functional/useSyncCahiserOrder"; import { useUISession } from "../functional/useUISession"; import { AttractiveTextArea } from "../molecules/AttractiveTextArea"; import { InputHeader } from "../molecules/InputHeader"; @@ -24,6 +25,7 @@ type props = { items: WithId[] | undefined; orders: WithId[] | undefined; submitPayload: (order: OrderEntity) => void; + syncOrder: (order: OrderEntity) => void; }; /** @@ -31,7 +33,7 @@ type props = { * * データの入出力は親コンポーネントに任せる */ -const CashierV2 = ({ items, orders, submitPayload }: props) => { +const CashierV2 = ({ items, orders, submitPayload, syncOrder }: props) => { const [newOrder, newOrderDispatch] = useOrderState(); const { inputStatus, @@ -44,6 +46,7 @@ const CashierV2 = ({ items, orders, submitPayload }: props) => { const [menuOpen, setMenuOpen] = useState(false); const [UISession, renewUISession] = useUISession(); const { nextOrderId } = useLatestOrderId(orders); + useSyncCahiserOrder(newOrder, syncOrder); const printer = usePrinter(); diff --git a/pos/app/routes/cashier.tsx b/pos/app/routes/cashier.tsx index 41da136e..2d30f189 100644 --- a/pos/app/routes/cashier.tsx +++ b/pos/app/routes/cashier.tsx @@ -7,6 +7,7 @@ import { import { itemSource } from "common/data/items"; import { stringToJSONSchema } from "common/lib/custom-zod"; import { OrderEntity, orderSchema } from "common/models/order"; +import { cashierRepository } from "common/repositories/global"; import { orderRepository } from "common/repositories/order"; import { useCallback } from "react"; import { z } from "zod"; @@ -39,13 +40,37 @@ export default function Cashier() { [submit], ); + const syncOrder = useCallback( + (order: OrderEntity) => { + submit({ syncOrder: JSON.stringify(order.toOrder()) }, { method: "PUT" }); + }, + [submit], + ); + return ( - + ); } // TODO(toririm): リファクタリングするときにファイルを切り出す -export const clientAction: ClientActionFunction = async ({ request }) => { +export const clientAction: ClientActionFunction = async (args) => { + const method = args.request.method; + switch (method) { + case "POST": + return submitOrderAction(args); + case "PUT": + return syncOrderAction(args); + default: + return new Response("Method not allowed", { status: 405 }); + } +}; + +export const submitOrderAction: ClientActionFunction = async ({ request }) => { const formData = await request.formData(); const schema = z.object({ @@ -68,3 +93,27 @@ export const clientAction: ClientActionFunction = async ({ request }) => { return new Response("ok"); }; + +export const syncOrderAction: ClientActionFunction = async ({ request }) => { + const formData = await request.formData(); + + const schema = z.object({ + syncOrder: stringToJSONSchema.pipe(orderSchema), + }); + const submission = parseWithZod(formData, { + schema, + }); + if (submission.status !== "success") { + console.error(submission.error); + return submission.reply(); + } + + const { syncOrder } = submission.value; + + cashierRepository.set({ + id: "cashier-state", + edittingOrder: OrderEntity.fromOrder(syncOrder), + }); + + return new Response("ok"); +}; From cd2ebb1c74ff94991b47c7d413b8e03607c2f912 Mon Sep 17 00:00:00 2001 From: toririm Date: Wed, 30 Oct 2024 19:52:33 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=E3=82=AA=E3=83=BC=E3=83=80=E3=83=BC?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=83=83=E3=83=97=E3=81=AE=E6=83=85=E5=A0=B1?= =?UTF-8?q?=E3=82=92=E3=82=B0=E3=83=AD=E3=83=BC=E3=83=90=E3=83=AB=E3=81=AB?= =?UTF-8?q?=E5=85=B1=E6=9C=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/firebase-utils/converter.ts | 27 ++++---- common/firebase-utils/subscription.ts | 23 +++++++ common/models/global.ts | 20 +++--- common/repositories/global.ts | 8 +-- firestore.rules | 4 +- pos/app/routes/_header.master.tsx | 93 +++++++++++++++++++++++++-- 6 files changed, 139 insertions(+), 36 deletions(-) diff --git a/common/firebase-utils/converter.ts b/common/firebase-utils/converter.ts index ec75ae8e..d5d88bcc 100644 --- a/common/firebase-utils/converter.ts +++ b/common/firebase-utils/converter.ts @@ -6,13 +6,14 @@ import { Timestamp, } from "firebase/firestore"; import _ from "lodash"; +import type { ZodSchema } from "zod"; +import type { WithId } from "../lib/typeguard"; import { type GlobalCashierState, MasterStateEntity, - globalStatSchema, -} from "models/global"; -import type { ZodSchema } from "zod"; -import type { WithId } from "../lib/typeguard"; + globalCashierStateSchema, + globalMasterStateSchema, +} from "../models/global"; import { ItemEntity, itemSchema } from "../models/item"; import { OrderEntity, orderSchema } from "../models/order"; @@ -103,37 +104,31 @@ export const orderConverter: FirestoreDataConverter> = { export const cashierStateConverter: FirestoreDataConverter = { - toFirestore: converter(globalStatSchema).toFirestore, + toFirestore: converter(globalCashierStateSchema).toFirestore, fromFirestore: ( snapshot: QueryDocumentSnapshot, options: SnapshotOptions, ) => { - const convertedData = converter(globalStatSchema).fromFirestore( + const convertedData = converter(globalCashierStateSchema).fromFirestore( snapshot, options, ); - if (convertedData.id === "cashier-state") { - return convertedData; - } - throw new Error("Invalid data"); + return convertedData; }, }; export const masterStateConverter: FirestoreDataConverter = { - toFirestore: converter(globalStatSchema).toFirestore, + toFirestore: converter(globalMasterStateSchema).toFirestore, fromFirestore: ( snapshot: QueryDocumentSnapshot, options: SnapshotOptions, ) => { - const convertedData = converter(globalStatSchema).fromFirestore( + const convertedData = converter(globalMasterStateSchema).fromFirestore( snapshot, options, ); - if (convertedData.id === "master-state") { - return MasterStateEntity.fromMasterState(convertedData); - } - throw new Error("Invalid data"); + return MasterStateEntity.fromMasterState(convertedData); }, }; diff --git a/common/firebase-utils/subscription.ts b/common/firebase-utils/subscription.ts index 155ec67c..3eb589ac 100644 --- a/common/firebase-utils/subscription.ts +++ b/common/firebase-utils/subscription.ts @@ -2,6 +2,7 @@ import { type FirestoreDataConverter, type QueryConstraint, collection, + doc, onSnapshot, query, } from "firebase/firestore"; @@ -34,3 +35,25 @@ export const collectionSub = ( }; return sub; }; + +export const documentSub = ({ + converter, +}: { converter: FirestoreDataConverter }) => { + const sub: SWRSubscription = ( + [collectionName, ...keys], + { next }, + ) => { + const coll = collection(prodDB, collectionName); + const unsub = onSnapshot( + doc(coll, ...keys).withConverter(converter), + (snapshot) => { + next(null, snapshot.data()); + }, + (err) => { + next(err); + }, + ); + return unsub; + }; + return sub; +}; diff --git a/common/models/global.ts b/common/models/global.ts index ebe9f1d0..b67a7673 100644 --- a/common/models/global.ts +++ b/common/models/global.ts @@ -1,14 +1,14 @@ import { z } from "zod"; import { orderSchema } from "./order"; -export const GlobalCashierStateSchema = z.object({ +export const globalCashierStateSchema = z.object({ id: z.literal("cashier-state"), edittingOrder: orderSchema, }); -export type GlobalCashierState = z.infer; +export type GlobalCashierState = z.infer; -const orderStatTypes = ["stop", "operational"] as const; +export const orderStatTypes = ["stop", "operational"] as const; export const orderStatSchema = z.object({ createdAt: z.date(), @@ -18,16 +18,16 @@ export const orderStatSchema = z.object({ export type OrderStatType = (typeof orderStatTypes)[number]; export type OrderStat = z.infer; -export const GlobalMasterStateSchema = z.object({ +export const globalMasterStateSchema = z.object({ id: z.literal("master-state"), orderStats: z.array(orderStatSchema), }); -export type GlobalMasterState = z.infer; +export type GlobalMasterState = z.infer; export const globalStatSchema = z.union([ - GlobalCashierStateSchema, - GlobalMasterStateSchema, + globalCashierStateSchema, + globalMasterStateSchema, ]); export type GlobalStat = z.infer; @@ -43,7 +43,11 @@ export class MasterStateEntity implements GlobalMasterState { } static createNew(): MasterStateEntity { - return new MasterStateEntity("master-state", []); + const initOrderStat: OrderStat = { + createdAt: new Date(), + type: "operational", + }; + return new MasterStateEntity("master-state", [initOrderStat]); } get orderStats() { diff --git a/common/repositories/global.ts b/common/repositories/global.ts index 7e8202eb..da79d4be 100644 --- a/common/repositories/global.ts +++ b/common/repositories/global.ts @@ -1,10 +1,10 @@ +import { type Firestore, doc, getDoc, setDoc } from "firebase/firestore"; import { cashierStateConverter, masterStateConverter, -} from "firebase-utils/converter"; -import { prodDB } from "firebase-utils/firestore"; -import { type Firestore, doc, getDoc, setDoc } from "firebase/firestore"; -import type { GlobalCashierState, MasterStateEntity } from "models/global"; +} from "../firebase-utils/converter"; +import { prodDB } from "../firebase-utils/firestore"; +import type { GlobalCashierState, MasterStateEntity } from "../models/global"; export type CashierStateRepo = { get: () => Promise; diff --git a/firestore.rules b/firestore.rules index e64150ca..6f0d8b25 100644 --- a/firestore.rules +++ b/firestore.rules @@ -8,8 +8,8 @@ service cloud.firestore { match /orders/{orderId} { allow read, write: if true; } - match /global/{document=**} { + match /global/{docId} { allow read, write: if true; } } -} \ No newline at end of file +} diff --git a/pos/app/routes/_header.master.tsx b/pos/app/routes/_header.master.tsx index fa26347c..8b66a053 100644 --- a/pos/app/routes/_header.master.tsx +++ b/pos/app/routes/_header.master.tsx @@ -5,18 +5,28 @@ import { useSubmit, } from "@remix-run/react"; import { id2abbr } from "common/data/items"; -import { orderConverter } from "common/firebase-utils/converter"; -import { collectionSub } from "common/firebase-utils/subscription"; +import { + masterStateConverter, + orderConverter, +} from "common/firebase-utils/converter"; +import { collectionSub, documentSub } from "common/firebase-utils/subscription"; import { stringToJSONSchema } from "common/lib/custom-zod"; +import { + MasterStateEntity, + type OrderStatType, + orderStatTypes, +} from "common/models/global"; import { OrderEntity, orderSchema } from "common/models/order"; +import { masterRepository } from "common/repositories/global"; import { orderRepository } from "common/repositories/order"; import dayjs from "dayjs"; import { orderBy } from "firebase/firestore"; -import { useCallback } from "react"; +import { useCallback, useMemo } from "react"; import useSWRSubscription from "swr/subscription"; import { z } from "zod"; import { InputComment } from "~/components/molecules/InputComment"; import { RealtimeElapsedTime } from "~/components/molecules/RealtimeElapsedTime"; +import { Button } from "~/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; import { cn } from "~/lib/utils"; @@ -37,6 +47,22 @@ export default function FielsOfMaster() { }, [submit], ); + const { data: masterRemoStat } = useSWRSubscription( + ["global", "master-state"], + documentSub({ converter: masterStateConverter }), + ); + const masterStat = masterRemoStat ?? MasterStateEntity.createNew(); + const orderStat = useMemo(() => { + const state = masterStat.orderStats[masterStat.orderStats.length - 1]; + return state.type; + }, [masterStat]); + + const changeOrderStat = useCallback( + (status: OrderStatType) => { + submit({ status }, { method: "POST" }); + }, + [submit], + ); const { data: orders } = useSWRSubscription( "orders", @@ -54,6 +80,20 @@ export default function FielsOfMaster() {

マスター

+

提供待ちオーダー数:{unserved}

@@ -79,8 +119,8 @@ export default function FielsOfMaster() {
- {order.items.map((item) => ( -
+ {order.items.map((item, index) => ( +
{ +// TODO: ファイル分割してリファクタリングする +export const clientAction: ClientActionFunction = async (args) => { + const method = args.request.method; + switch (method) { + case "PUT": + return addComment(args); + case "POST": + return changeOrderStat(args); + default: + throw new Error(`Method ${method} is not allowed`); + } +}; + +export const addComment: ClientActionFunction = async ({ request }) => { const formData = await request.formData(); const schema = z.object({ @@ -155,3 +208,31 @@ export const clientAction: ClientActionFunction = async ({ request }) => { return new Response("ok"); }; + +export const changeOrderStat: ClientActionFunction = async ({ request }) => { + const formData = await request.formData(); + + const schema = z.object({ + status: z.enum(orderStatTypes), + }); + const submission = parseWithZod(formData, { + schema, + }); + if (submission.status !== "success") { + console.error(submission.error); + return submission.reply(); + } + + const { status } = submission.value; + + const masterStats: MasterStateEntity = + (await masterRepository.get()) ?? MasterStateEntity.createNew(); + + console.log(status); + masterStats.addOrderStat(status); + console.log(masterStats); + + await masterRepository.set(masterStats); + + return new Response("ok"); +};