Compare commits
58
Commits
e532a95f0b
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
757cf40358 | ||
|
|
6fc315a5f2 | ||
|
|
440f9e5696 | ||
|
|
d68806aaf8 | ||
|
|
3efde72524 | ||
|
|
edd34a00a7 | ||
|
|
ac2aa770fb | ||
|
|
d987c71010 | ||
|
|
1dae53f90a | ||
|
|
7cf286850c | ||
|
|
a6ac32d783 | ||
|
|
c124b0415a | ||
|
|
815939cbc6 | ||
|
|
7f72baf97d | ||
|
|
8481032e30 | ||
|
|
917fc275f9 | ||
|
|
a8e655a810 | ||
|
|
7f3728b5bf | ||
|
|
3d736769e0 | ||
|
|
b0ef917cfc | ||
|
|
05a91bb5c3 | ||
|
|
0da1f47573 | ||
|
|
de158d7c36 | ||
|
|
04a0e4a708 | ||
|
|
5bc57f0200 | ||
|
|
c539ef84cd | ||
|
|
aa8d40bc4e | ||
|
|
6a81b305bc | ||
|
|
6bd63b53c6 | ||
|
|
8653418778 | ||
|
|
2f37b6bba1 | ||
|
|
315491dd5b | ||
|
|
c5ed038bca | ||
|
|
f4f3c26e1e | ||
|
|
5f375a0966 | ||
|
|
8d7f99848c | ||
|
|
f819b0cd46 | ||
|
|
a2e97c293d | ||
|
|
86d1a32dd8 | ||
|
|
370e77f1a6 | ||
|
|
a716080f7d | ||
|
|
439932c168 | ||
|
|
c8d1721358 | ||
|
|
18a7d2030e | ||
|
|
927f6ff56e | ||
|
|
d66e09f515 | ||
|
|
e98b0d86d2 | ||
|
|
79bd64ce8c | ||
|
|
5059f40d4c | ||
|
|
cacfce8eae | ||
|
|
0d8f8f8148 | ||
|
|
ff8fc388c0 | ||
|
|
ea459e72b3 | ||
|
|
b7fdbd4779 | ||
|
|
506319ce4f | ||
|
|
9c3ad51686 | ||
|
|
d291312139 | ||
|
|
456c3acda5 |
@@ -0,0 +1,148 @@
|
|||||||
|
---
|
||||||
|
name: run-ghost-kitchen
|
||||||
|
description: Build, run, and drive Ghost Kitchen (TanStack Start ordering app). Use when asked to start the app, run its tests, seed its database, take a screenshot of its UI, or interact with the running app (menu, basket, sign-in/sign-up).
|
||||||
|
---
|
||||||
|
|
||||||
|
Ghost Kitchen is a TanStack Start web app (Vite dev server on port 3000,
|
||||||
|
Postgres via Docker, Better Auth). There is no `chromium-cli` binary in
|
||||||
|
this environment — drive it with the `mcp__claude-in-chrome__*` tools
|
||||||
|
(load them via `ToolSearch` if deferred; see the `claude-in-chrome`
|
||||||
|
skill). All paths below are relative to the repo root.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Docker (for Postgres) and Node — both already present in this
|
||||||
|
environment. No OS packages were needed.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
One-time, per fresh checkout / fresh Postgres volume:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d # starts Postgres on localhost:5433
|
||||||
|
cp .env.example .env.local
|
||||||
|
```
|
||||||
|
|
||||||
|
`BETTER_AUTH_SECRET` in `.env.example` ships empty — Better Auth needs a
|
||||||
|
real value or auth routes fail. Generate one and fill it in:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
|
||||||
|
# paste the output as BETTER_AUTH_SECRET= in .env.local
|
||||||
|
```
|
||||||
|
|
||||||
|
Then apply migrations and seed data:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run db:migrate # this project's Drizzle migrations
|
||||||
|
npm run auth:migrate # Better Auth's own tables (users, sessions, ...)
|
||||||
|
npm run db:seed # sample Brand, menu, Machines — SKIP if DB already seeded, see Gotchas
|
||||||
|
```
|
||||||
|
|
||||||
|
Against a genuinely empty database, `auth:migrate` prompts interactively
|
||||||
|
("Are you sure you want to run these migrations?"). Non-interactively,
|
||||||
|
answer it with `yes | npm run auth:migrate`.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
No separate build step for local dev (Vite serves TS directly). For a
|
||||||
|
production bundle: `npm run build`.
|
||||||
|
|
||||||
|
## Run (agent path)
|
||||||
|
|
||||||
|
Start the dev server in the background and wait for it to actually serve
|
||||||
|
(don't `sleep` — poll the port):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev &
|
||||||
|
timeout 30 bash -c 'until curl -sf http://localhost:3000 >/dev/null; do sleep 1; done'
|
||||||
|
```
|
||||||
|
|
||||||
|
Stop it with `lsof -ti:3000 -sTCP:LISTEN | xargs -r kill` before
|
||||||
|
relaunching, or the next run hits `EADDRINUSE`. Check for an
|
||||||
|
already-running instance first (`lsof -i :3000 -sTCP:LISTEN`) — in this
|
||||||
|
project's dev container a server is often already up from a prior
|
||||||
|
session; reuse it instead of starting a duplicate.
|
||||||
|
|
||||||
|
Drive it with the `mcp__claude-in-chrome__*` tools:
|
||||||
|
|
||||||
|
```
|
||||||
|
tabs_context_mcp { createIfEmpty: true }
|
||||||
|
navigate { url: "http://localhost:3000", tabId: <tab> }
|
||||||
|
computer { action: "screenshot", tabId: <tab> }
|
||||||
|
```
|
||||||
|
|
||||||
|
One representative flow that proves the app works end-to-end (menu ->
|
||||||
|
basket -> sign-up):
|
||||||
|
|
||||||
|
```
|
||||||
|
# Menu page: bump a quantity stepper
|
||||||
|
find { query: "increment button for Pizza", tabId: <tab> }
|
||||||
|
computer { action: "left_click", coordinate: [<x>,<y>], tabId: <tab> }
|
||||||
|
|
||||||
|
# Basket icon (top-right) -> /basket shows line items + total
|
||||||
|
computer { action: "left_click", coordinate: [1338, 29], tabId: <tab> } # icon position at 1400px width
|
||||||
|
|
||||||
|
# Sign up (/signup): Name, Email, Password fields -> "Create account"
|
||||||
|
navigate { url: "http://localhost:3000/signup", tabId: <tab> }
|
||||||
|
# fill Name/Email/Password via computer left_click + type, then click "Create account"
|
||||||
|
```
|
||||||
|
|
||||||
|
Close tabs you created when done (`tabs_close_mcp`).
|
||||||
|
|
||||||
|
## Run (human path)
|
||||||
|
|
||||||
|
`npm run dev` -> serves on http://localhost:3000, Ctrl-C to stop. Useless
|
||||||
|
headless without a browser to point at it.
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm test # vitest run — 27 tests, 2 files, ~3s
|
||||||
|
npm run typecheck
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- **`.env.local` doesn't exist by default** and isn't created by any
|
||||||
|
script — `cp .env.example .env.local` is a required manual step, and
|
||||||
|
`BETTER_AUTH_SECRET` inside it ships blank. Leaving it blank breaks
|
||||||
|
sign-in/sign-up silently at the Better Auth layer.
|
||||||
|
- **`npm run db:seed` is not idempotent.** Re-running it against an
|
||||||
|
already-seeded DB throws `DrizzleQueryError: duplicate key value
|
||||||
|
violates unique constraint "machine_tags_name_idx"` and exits 1. If
|
||||||
|
Postgres already has data (e.g. a long-lived dev container), skip
|
||||||
|
seeding rather than treating the error as a setup failure.
|
||||||
|
- **Port 3000 is often already occupied** by a dev server started
|
||||||
|
outside the current session (e.g. the user's own terminal). Check
|
||||||
|
`lsof -i :3000 -sTCP:LISTEN` before launching — if something already
|
||||||
|
answers `curl -sf http://localhost:3000`, drive that instead of
|
||||||
|
fighting over the port.
|
||||||
|
- **Session/basket state persists across browser tab reloads** (basket
|
||||||
|
contents and signed-in user both survived a fresh tab + navigate in
|
||||||
|
testing) — a new tab hitting `localhost:3000` may already show a
|
||||||
|
logged-in user or a non-empty basket from a prior run in the same
|
||||||
|
container. Not a bug; just don't assume a fresh tab means fresh state.
|
||||||
|
- **`[TanStack Devtools] Failed to load: TypeError: Failed to fetch
|
||||||
|
dynamically imported module ...`** shows up in the console on first
|
||||||
|
load. It's a stale Vite dep-cache issue with the devtools panel, not
|
||||||
|
an app error — harmless, ignore it (verified via
|
||||||
|
`read_console_messages`, the rest of the app functions normally).
|
||||||
|
- **Adding a `NOT NULL` column via `db:generate` against a populated
|
||||||
|
local DB** makes `drizzle-kit generate` prompt interactively for a
|
||||||
|
default value — awkward to script. Reset the Postgres volume first
|
||||||
|
(`docker compose down -v && docker compose up -d`, then recreate the
|
||||||
|
DB, migrate, `yes | npm run auth:migrate`, reseed) so the column is
|
||||||
|
added to an empty table and no prompt appears.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **`Error: Port 3000 is already in use` on `npm run dev`**: another
|
||||||
|
process already holds it — see the port gotcha above. Either reuse it
|
||||||
|
(`curl` returns 200) or `lsof -ti:3000 -sTCP:LISTEN | xargs -r kill`
|
||||||
|
first if you own that process.
|
||||||
|
- **`duplicate key value violates unique constraint` from `db:seed`**:
|
||||||
|
DB already seeded — skip the seed step, don't debug it.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"projectName": "ghost-kitchen-scaffold",
|
||||||
|
"mode": "file-router",
|
||||||
|
"typescript": true,
|
||||||
|
"tailwind": false,
|
||||||
|
"packageManager": "npm",
|
||||||
|
"git": false,
|
||||||
|
"install": true,
|
||||||
|
"intent": true,
|
||||||
|
"addOnOptions": {
|
||||||
|
"drizzle": {
|
||||||
|
"database": "postgresql"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"projectPreset": "default",
|
||||||
|
"includeExamples": false,
|
||||||
|
"routerOnly": false,
|
||||||
|
"version": 1,
|
||||||
|
"framework": "react",
|
||||||
|
"chosenAddOns": [
|
||||||
|
"drizzle",
|
||||||
|
"better-auth",
|
||||||
|
"eslint"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
DATABASE_URL=postgres://postgres:postgres@localhost:5433/ghost_kitchen
|
||||||
|
BETTER_AUTH_URL=http://localhost:3000
|
||||||
|
BETTER_AUTH_SECRET=
|
||||||
|
|
||||||
|
# ISO 4217 currency code this Kitchen charges and displays prices in, and
|
||||||
|
# the BCP 47 locale used to format it (decimal separator, digit grouping).
|
||||||
|
# Default to EUR / de-DE (this deployment's location) when unset.
|
||||||
|
VITE_CURRENCY=EUR
|
||||||
|
VITE_LOCALE=de-DE
|
||||||
|
|
||||||
|
# Optional: enables "Continue with Google" / "Continue with Apple" on the
|
||||||
|
# sign-in and sign-up pages. Leave unset to fall back to email/password only.
|
||||||
|
GOOGLE_CLIENT_ID=
|
||||||
|
GOOGLE_CLIENT_SECRET=
|
||||||
|
APPLE_CLIENT_ID=
|
||||||
|
APPLE_CLIENT_SECRET=
|
||||||
|
|
||||||
|
# Used by `npm run db:seed-admin` to create/promote one admin account
|
||||||
|
# (can view every order at /admin/orders). Not read by the app itself.
|
||||||
|
ADMIN_EMAIL=
|
||||||
|
ADMIN_PASSWORD=
|
||||||
|
ADMIN_NAME=
|
||||||
+12
-1
@@ -1,2 +1,13 @@
|
|||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
.env
|
.env
|
||||||
.env.local
|
.nitro
|
||||||
|
.tanstack
|
||||||
|
.wrangler
|
||||||
|
.output
|
||||||
|
.vinxi
|
||||||
|
__unconfig*
|
||||||
|
todos.json
|
||||||
|
|||||||
Generated
+4
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectTasksOptions" suppressed-tasks="SCSS" />
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package-lock.json
|
||||||
|
pnpm-lock.yaml
|
||||||
|
yarn.lock
|
||||||
Vendored
+11
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"files.watcherExclude": {
|
||||||
|
"**/routeTree.gen.ts": true
|
||||||
|
},
|
||||||
|
"search.exclude": {
|
||||||
|
"**/routeTree.gen.ts": true
|
||||||
|
},
|
||||||
|
"files.readonlyInclude": {
|
||||||
|
"**/routeTree.gen.ts": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,17 +12,17 @@ Guidance for Claude Code when working in this repository.
|
|||||||
- Vite
|
- Vite
|
||||||
- SCSS Modules
|
- SCSS Modules
|
||||||
- Drizzle ORM + PostgreSQL
|
- Drizzle ORM + PostgreSQL
|
||||||
- [Auth: Better Auth / Clerk / custom / none yet]
|
- Better Auth
|
||||||
- [Package manager: pnpm / npm / bun]
|
- npm
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm dev # start dev server (http://localhost:3000)
|
npm run dev # start dev server (http://localhost:3000)
|
||||||
pnpm build # production build
|
npm run build # production build
|
||||||
pnpm typecheck # tsc --noEmit
|
npm run typecheck # tsc --noEmit
|
||||||
pnpm lint # eslint
|
npm run lint # eslint
|
||||||
pnpm test # [test runner, e.g. vitest]
|
npm test # vitest run
|
||||||
```
|
```
|
||||||
|
|
||||||
Always run `typecheck` and `lint` before considering a task done. If `build`
|
Always run `typecheck` and `lint` before considering a task done. If `build`
|
||||||
@@ -38,18 +38,24 @@ src/
|
|||||||
├── routeTree.gen.ts # AUTO-GENERATED — never edit by hand
|
├── routeTree.gen.ts # AUTO-GENERATED — never edit by hand
|
||||||
├── router.tsx # router instance/config
|
├── router.tsx # router instance/config
|
||||||
├── components/
|
├── components/
|
||||||
│ ├── ui/ # base primitives (buttons, inputs, etc.)
|
│ ├── ui/ # base primitives (buttons, inputs, etc.) — not
|
||||||
│ └── <domain>/ # feature-specific components, e.g. posts/, auth/
|
│ │ populated yet; add it when a shared primitive
|
||||||
|
│ │ is needed
|
||||||
|
│ └── <domain>/ # feature-specific components, e.g. ordering/, orders/
|
||||||
|
├── domain/ # framework-free business logic, e.g. ordering.ts
|
||||||
|
├── server/ # server functions (createServerFn) callable from routes
|
||||||
|
├── integrations/ # third-party integration glue, e.g. better-auth/
|
||||||
├── lib/ # shared utilities, API clients, helpers
|
├── lib/ # shared utilities, API clients, helpers
|
||||||
├── db/
|
├── db/
|
||||||
│ ├── schema.ts # Drizzle schema definitions
|
│ ├── schema.ts # Drizzle schema definitions
|
||||||
│ ├── index.ts # Drizzle client instance
|
│ ├── index.ts # Drizzle client instance
|
||||||
│ └── migrations/ # generated SQL migrations — never hand-edit
|
│ └── seed.ts # sample data for local dev / tests
|
||||||
├── styles/
|
├── styles/
|
||||||
│ ├── globals.scss # global styles, resets
|
│ ├── globals.scss # global styles, resets
|
||||||
│ └── _variables.scss # shared SCSS variables/mixins (partial)
|
│ └── _variables.scss # shared SCSS variables/mixins (partial)
|
||||||
public/ # static assets
|
public/ # static assets
|
||||||
drizzle.config.ts # Drizzle Kit config (schema path, out dir, dialect)
|
drizzle.config.ts # Drizzle Kit config (schema path, out dir, dialect)
|
||||||
|
drizzle/ # generated SQL migrations — never hand-edit
|
||||||
```
|
```
|
||||||
|
|
||||||
### Routing conventions
|
### Routing conventions
|
||||||
@@ -72,7 +78,8 @@ drizzle.config.ts # Drizzle Kit config (schema path, out dir, dialect)
|
|||||||
- Class names in `.module.scss` files are camelCase (e.g. `.cardHeader`, not
|
- Class names in `.module.scss` files are camelCase (e.g. `.cardHeader`, not
|
||||||
`.card-header`) so they read naturally as `styles.cardHeader`.
|
`.card-header`) so they read naturally as `styles.cardHeader`.
|
||||||
- Shared tokens (colors, spacing, breakpoints) live in `src/styles/_variables.scss`
|
- Shared tokens (colors, spacing, breakpoints) live in `src/styles/_variables.scss`
|
||||||
and are pulled in with `@use '~/styles/variables' as *;` — don't hardcode
|
and are pulled in with a relative `@use '../styles/variables' as *;` (Sass
|
||||||
|
doesn't resolve the `#/`/`@/` TS path aliases) — don't hardcode
|
||||||
colors/spacing that already exist as a variable.
|
colors/spacing that already exist as a variable.
|
||||||
- `src/styles/globals.scss` is for resets and truly global rules only
|
- `src/styles/globals.scss` is for resets and truly global rules only
|
||||||
(imported once in `__root.tsx`); component-specific styles always go in a
|
(imported once in `__root.tsx`); component-specific styles always go in a
|
||||||
@@ -93,7 +100,10 @@ drizzle.config.ts # Drizzle Kit config (schema path, out dir, dialect)
|
|||||||
the `function` keyword — e.g. `const getUser = async (id: string) => {...}`
|
the `function` keyword — e.g. `const getUser = async (id: string) => {...}`
|
||||||
and `export const Button = (props: ButtonProps) => {...}`.
|
and `export const Button = (props: ButtonProps) => {...}`.
|
||||||
- [Naming conventions: PascalCase components, camelCase functions/vars, etc.]
|
- [Naming conventions: PascalCase components, camelCase functions/vars, etc.]
|
||||||
- [Import order / path aliases, e.g. `~/` or `@/` for `src/`]
|
- Path aliases `#/*` and `@/*` both resolve to `src/*` (see `tsconfig.json`
|
||||||
|
and `package.json`'s `imports` field) — use either for TS/TSX imports.
|
||||||
|
SCSS files can't use these aliases; use relative `@use`/`@import` paths
|
||||||
|
there instead.
|
||||||
- Co-locate a component's test file next to it unless it's a shared
|
- Co-locate a component's test file next to it unless it's a shared
|
||||||
primitive in `components/ui/` (see "Styling" for `.module.scss` placement).
|
primitive in `components/ui/` (see "Styling" for `.module.scss` placement).
|
||||||
- No comments unless they're unavoidable for understanding — e.g. explaining
|
- No comments unless they're unavoidable for understanding — e.g. explaining
|
||||||
@@ -116,14 +126,17 @@ drizzle.config.ts # Drizzle Kit config (schema path, out dir, dialect)
|
|||||||
- After changing `schema.ts`, generate a migration rather than editing the
|
- After changing `schema.ts`, generate a migration rather than editing the
|
||||||
database by hand:
|
database by hand:
|
||||||
```bash
|
```bash
|
||||||
pnpm drizzle-kit generate # create a migration from schema changes
|
npm run db:generate # create a migration from schema changes
|
||||||
pnpm drizzle-kit migrate # apply pending migrations
|
npm run db:migrate # apply pending migrations
|
||||||
```
|
```
|
||||||
- Never hand-edit files under `db/migrations/` — they're generated and
|
- Never hand-edit files under `drizzle/` (the migrations output directory,
|
||||||
should be treated as a history, not touched retroactively.
|
configured in `drizzle.config.ts`) — they're generated and should be
|
||||||
- [Connection details: env var name for `DATABASE_URL`, whether using a
|
treated as a history, not touched retroactively.
|
||||||
connection pool like `postgres.js` or `node-postgres`, local dev DB setup
|
- Connection string comes from `DATABASE_URL` (see `.env.example`), read via
|
||||||
e.g. Docker Compose]
|
`drizzle-orm/node-postgres`. Local dev DB is a `docker-compose.yml`
|
||||||
|
Postgres container — see the README's "Local development database"
|
||||||
|
section. `.env.local` is picked up automatically by both the dev server
|
||||||
|
and `drizzle-kit`; no manual sourcing needed.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
@@ -134,7 +147,7 @@ drizzle.config.ts # Drizzle Kit config (schema path, out dir, dialect)
|
|||||||
|
|
||||||
## Things to avoid
|
## Things to avoid
|
||||||
|
|
||||||
- Don't hand-edit `routeTree.gen.ts` or files under `db/migrations/`.
|
- Don't hand-edit `routeTree.gen.ts` or files under `drizzle/`.
|
||||||
- Don't run destructive Drizzle commands (`drizzle-kit push` against a
|
- Don't run destructive Drizzle commands (`drizzle-kit push` against a
|
||||||
populated DB, or anything that drops columns/tables) without confirming
|
populated DB, or anything that drops columns/tables) without confirming
|
||||||
first — prefer `generate` + `migrate` so changes are reviewable.
|
first — prefer `generate` + `migrate` so changes are reviewable.
|
||||||
|
|||||||
+20
-2
@@ -16,16 +16,30 @@ _Avoid_: Store, location (as a separate multi-tenant concept — this project ha
|
|||||||
The single restaurant concept (menu, name, branding) served by one Kitchen. One Brand per Kitchen — not a separate entity shared across multiple kitchens.
|
The single restaurant concept (menu, name, branding) served by one Kitchen. One Brand per Kitchen — not a separate entity shared across multiple kitchens.
|
||||||
|
|
||||||
**Order**:
|
**Order**:
|
||||||
A customer's placed, paid request, containing one or more Order Items. An Order is complete only once every Order Item within it has been picked up.
|
A customer's placed, paid request, containing one or more Order Items. An Order is complete once every Order Item within it has reached a terminal state — picked up, cancelled, or discarded. (A fully-cancelled Order is still "complete": there is no separate status for that outcome.)
|
||||||
|
|
||||||
|
**Charge**:
|
||||||
|
The single payment transaction covering an Order's full total, created once at placement time. Can be partially refunded multiple times over the Order's life — once per Order Item that gets cancelled — rather than each Order Item carrying its own separate payment.
|
||||||
|
_Avoid_: Payment (too generic — this project always means one Charge per Order, not a collection of per-item payments)
|
||||||
|
|
||||||
**Order Item**:
|
**Order Item**:
|
||||||
A single menu item within an Order. Progresses through its own Preparation Steps independently of the other items in the same Order, and can be picked up separately from them. Cancellable with refund any time before its first Preparation Step begins; locked once that step starts.
|
A single menu item within an Order. Progresses through its own Preparation Steps independently of the other items in the same Order, and can be picked up separately from them. Cancellable with refund any time its current Preparation Step has not yet been claimed by a Machine — including the gap between one Step finishing and the next Step being claimed. Locked as soon as a Step is claimed by a Machine (in progress). Separately, the Kitchen itself can cancel an Order Item at any point before it's ready (e.g. a Machine fault), even mid-Step, also with a refund.
|
||||||
_Avoid_: Line item (implies a billing-only concept; this also carries preparation/pickup state)
|
_Avoid_: Line item (implies a billing-only concept; this also carries preparation/pickup state)
|
||||||
|
|
||||||
|
**Guest Access Token**:
|
||||||
|
A bearer credential issued to a guest customer (one without an account) at Order placement time. Since a guest has no session, this token is the only proof of ownership accepted on later calls against that Order (status lookup, cancellation, pickup).
|
||||||
|
_Avoid_: Session (guests never get a session; this is a different, bearer-credential mechanism)
|
||||||
|
|
||||||
|
**Recipe**:
|
||||||
|
The ordered list of Preparation Steps required to prepare a Menu Item. A Menu Item's Recipe can be edited later by an operator; an Order Item freezes a copy of the Recipe at the moment it's ordered, so a later Recipe edit never reshapes a Step list a customer already paid for.
|
||||||
|
|
||||||
**Preparation Step**:
|
**Preparation Step**:
|
||||||
One stage of preparing an Order Item, requiring a Machine carrying a specific Machine Tag, with an estimated duration. An Order Item consists of one or more Steps performed in sequence — the next Step cannot begin until the previous one finishes. The customer can see their Order Item's full Step list, current queue position, and approximate time to completion.
|
One stage of preparing an Order Item, requiring a Machine carrying a specific Machine Tag, with an estimated duration. An Order Item consists of one or more Steps performed in sequence — the next Step cannot begin until the previous one finishes. The customer can see their Order Item's full Step list, current queue position, and approximate time to completion.
|
||||||
_Avoid_: Stage (use Step consistently)
|
_Avoid_: Stage (use Step consistently)
|
||||||
|
|
||||||
|
**Delay**:
|
||||||
|
Extra time a Machine reports on top of a claimed Preparation Step's estimated duration. Delays add to the Order Item's time to completion; the customer only sees the updated time, never a delay notice.
|
||||||
|
|
||||||
**Machine**:
|
**Machine**:
|
||||||
A physical cooking hardware unit at a Kitchen (e.g. an oven, a grill), carrying one or more Machine Tags. A Kitchen may have multiple Machines carrying the same Tag, working in parallel.
|
A physical cooking hardware unit at a Kitchen (e.g. an oven, a grill), carrying one or more Machine Tags. A Kitchen may have multiple Machines carrying the same Tag, working in parallel.
|
||||||
|
|
||||||
@@ -35,5 +49,9 @@ A capability label on a Machine (e.g. "oven", "grill", "assembly") describing wh
|
|||||||
**Queue**:
|
**Queue**:
|
||||||
The set of Preparation Steps awaiting a Machine carrying a given Machine Tag, ordered by turn. When such a Machine becomes idle, it takes the next eligible Step from that Tag's Queue.
|
The set of Preparation Steps awaiting a Machine carrying a given Machine Tag, ordered by turn. When such a Machine becomes idle, it takes the next eligible Step from that Tag's Queue.
|
||||||
|
|
||||||
|
**Pickup Box**:
|
||||||
|
A numbered locker at a Kitchen that a ready Order Item is placed in for the customer to collect. Each ready Order Item gets the lowest-numbered free box; if none is free, it waits for the next one to free up. The customer opens the box from their Order and closes it again after taking the item out — closing the box is what marks the Order Item picked up and frees the box. The customer has 15 minutes from when the item goes into the box (the pickup window); after that the item is discarded and the box freed, with no refund. A box that is open when the window runs out isn't emptied, since its customer is taking the item out.
|
||||||
|
_Avoid_: Counter, shelf (the box is a lockable compartment the customer operates themselves)
|
||||||
|
|
||||||
**Operator**:
|
**Operator**:
|
||||||
The human role attached to a Kitchen, responsible only for physically restocking ingredients and repairing machines. Never involved in cooking or fulfilling orders.
|
The human role attached to a Kitchen, responsible only for physically restocking ingredients and repairing machines. Never involved in cooking or fulfilling orders.
|
||||||
|
|||||||
@@ -0,0 +1,264 @@
|
|||||||
|
Welcome to your new TanStack Start app!
|
||||||
|
|
||||||
|
# Getting Started
|
||||||
|
|
||||||
|
To run this application:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
This requires a running Postgres database — see
|
||||||
|
[Local development database](#local-development-database) below before your
|
||||||
|
first run.
|
||||||
|
|
||||||
|
# Building For Production
|
||||||
|
|
||||||
|
To build this application for production:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
|
||||||
|
This project uses SCSS Modules. Every component that needs styles gets a
|
||||||
|
co-located `Component.module.scss` next to `Component.tsx`; shared tokens
|
||||||
|
(colors, spacing, breakpoints) live in `src/styles/_variables.scss`. See
|
||||||
|
`CLAUDE.md` for the full styling conventions.
|
||||||
|
|
||||||
|
## Linting & Formatting
|
||||||
|
|
||||||
|
|
||||||
|
This project uses [eslint](https://eslint.org/) and [prettier](https://prettier.io/) for linting and formatting. Eslint is configured using [tanstack/eslint-config](https://tanstack.com/config/latest/docs/eslint). The following scripts are available:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run lint
|
||||||
|
npm run format
|
||||||
|
npm run check
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Local development database
|
||||||
|
|
||||||
|
This project requires a Postgres database — both for its own data (Drizzle,
|
||||||
|
see `src/db/schema.ts`) and for Better Auth's own tables (users, sessions,
|
||||||
|
etc). A `docker-compose.yml` at the repo root starts one:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d # starts Postgres on localhost:5433
|
||||||
|
cp .env.example .env.local # sets DATABASE_URL to match
|
||||||
|
npm run db:migrate # applies this project's Drizzle migrations
|
||||||
|
npm run auth:migrate # applies Better Auth's own tables
|
||||||
|
npm run db:seed # seeds a sample Brand, menu, and Machines
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
`DATABASE_URL` in `.env.local` is picked up automatically by both the dev
|
||||||
|
server and the `drizzle-kit` commands above — no other wiring needed.
|
||||||
|
|
||||||
|
After changing `src/db/schema.ts`, generate and apply a new migration rather
|
||||||
|
than editing the database by hand:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run db:generate
|
||||||
|
npm run db:migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setting up Better Auth
|
||||||
|
|
||||||
|
Generate and set the `BETTER_AUTH_SECRET` environment variable in your
|
||||||
|
`.env.local`, and make sure `BETTER_AUTH_URL` matches the URL the app is
|
||||||
|
actually served from (`http://localhost:3000` in local dev):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx @better-auth/cli secret
|
||||||
|
```
|
||||||
|
|
||||||
|
Account creation and sign-in live at `/signup` and `/login`, supporting both
|
||||||
|
email/password and "Continue with Google" / "Continue with Apple" SSO. The
|
||||||
|
SSO buttons only appear once the corresponding provider's credentials are
|
||||||
|
set in `.env.local`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GOOGLE_CLIENT_ID=...
|
||||||
|
GOOGLE_CLIENT_SECRET=...
|
||||||
|
APPLE_CLIENT_ID=...
|
||||||
|
APPLE_CLIENT_SECRET=...
|
||||||
|
```
|
||||||
|
|
||||||
|
Create OAuth credentials in the [Google Cloud Console](https://console.cloud.google.com/apis/credentials)
|
||||||
|
and the [Apple Developer portal](https://developer.apple.com/account/resources/identifiers/list/serviceId)
|
||||||
|
respectively, with the redirect URI `{BETTER_AUTH_URL}/api/auth/callback/google`
|
||||||
|
(swap `google` for `apple`). See the
|
||||||
|
[Better Auth documentation](https://www.better-auth.com) for more on
|
||||||
|
configuring authentication, and its
|
||||||
|
[Google](https://www.better-auth.com/docs/authentication/google) and
|
||||||
|
[Apple](https://www.better-auth.com/docs/authentication/apple) provider
|
||||||
|
guides for provider-specific setup details.
|
||||||
|
|
||||||
|
For Google, `scripts/setup-google-sso.sh` walks you through the Google Cloud
|
||||||
|
Console steps interactively and writes the resulting `GOOGLE_CLIENT_ID` /
|
||||||
|
`GOOGLE_CLIENT_SECRET` into `.env` for you:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/setup-google-sso.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Apple's Sign in with Apple doesn't support `localhost` redirect URIs — you
|
||||||
|
need a real HTTPS domain (a deployment or a tunnel like ngrok) registered on
|
||||||
|
the Services ID before it's testable, so there's no equivalent script for it
|
||||||
|
yet.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Routing
|
||||||
|
|
||||||
|
This project uses [TanStack Router](https://tanstack.com/router) with file-based routing. Routes are managed as files in `src/routes`.
|
||||||
|
|
||||||
|
### Adding A Route
|
||||||
|
|
||||||
|
To add a new route to your application just add a new file in the `./src/routes` directory.
|
||||||
|
|
||||||
|
TanStack will automatically generate the content of the route file for you.
|
||||||
|
|
||||||
|
Now that you have two routes you can use a `Link` component to navigate between them.
|
||||||
|
|
||||||
|
### Adding Links
|
||||||
|
|
||||||
|
To use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/react-router`.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
```
|
||||||
|
|
||||||
|
Then anywhere in your JSX you can use it like so:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Link to="/about">About</Link>
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create a link that will navigate to the `/about` route.
|
||||||
|
|
||||||
|
More information on the `Link` component can be found in the [Link documentation](https://tanstack.com/router/v1/docs/framework/react/api/router/linkComponent).
|
||||||
|
|
||||||
|
### Using A Layout
|
||||||
|
|
||||||
|
In the File Based Routing setup the layout is located in `src/routes/__root.tsx`. Anything you add to the root route will appear in all the routes. The route content will appear in the JSX where you render `{children}` in the `shellComponent`.
|
||||||
|
|
||||||
|
Here is an example layout that includes a header:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
export const Route = createRootRoute({
|
||||||
|
head: () => ({
|
||||||
|
meta: [
|
||||||
|
{ charSet: 'utf-8' },
|
||||||
|
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||||
|
{ title: 'My App' },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
shellComponent: ({ children }) => (
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<HeadContent />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<nav>
|
||||||
|
<Link to="/">Home</Link>
|
||||||
|
<Link to="/about">About</Link>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
{children}
|
||||||
|
<Scripts />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
More information on layouts can be found in the [Layouts documentation](https://tanstack.com/router/latest/docs/framework/react/guide/routing-concepts#layouts).
|
||||||
|
|
||||||
|
## Server Functions
|
||||||
|
|
||||||
|
TanStack Start provides server functions that allow you to write server-side code that seamlessly integrates with your client components.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createServerFn } from '@tanstack/react-start'
|
||||||
|
|
||||||
|
const getServerTime = createServerFn({
|
||||||
|
method: 'GET',
|
||||||
|
}).handler(async () => {
|
||||||
|
return new Date().toISOString()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Use in a component
|
||||||
|
function MyComponent() {
|
||||||
|
const [time, setTime] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getServerTime().then(setTime)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return <div>Server time: {time}</div>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Routes
|
||||||
|
|
||||||
|
You can create API routes by using the `server` property in your route definitions:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
import { json } from '@tanstack/react-start'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/api/hello')({
|
||||||
|
server: {
|
||||||
|
handlers: {
|
||||||
|
GET: () => json({ message: 'Hello, World!' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Fetching
|
||||||
|
|
||||||
|
There are multiple ways to fetch data in your application. You can use TanStack Query to fetch data from a server. But you can also use the `loader` functionality built into TanStack Router to load the data for a route before it's rendered.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/people')({
|
||||||
|
loader: async () => {
|
||||||
|
const response = await fetch('https://swapi.dev/api/people')
|
||||||
|
return response.json()
|
||||||
|
},
|
||||||
|
component: PeopleComponent,
|
||||||
|
})
|
||||||
|
|
||||||
|
function PeopleComponent() {
|
||||||
|
const data = Route.useLoaderData()
|
||||||
|
return (
|
||||||
|
<ul>
|
||||||
|
{data.results.map((person) => (
|
||||||
|
<li key={person.name}>{person.name}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Loaders simplify your data fetching logic dramatically. Check out more information in the [Loader documentation](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#loader-parameters).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Learn More
|
||||||
|
|
||||||
|
You can learn more about all of the offerings from TanStack in the [TanStack documentation](https://tanstack.com).
|
||||||
|
|
||||||
|
For TanStack Start specific documentation, visit [TanStack Start](https://tanstack.com/start).
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_DB: ghost_kitchen
|
||||||
|
ports:
|
||||||
|
- '5433:5432'
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
# Order Items are independent, not the Order
|
# Order Items are independent, not the Order
|
||||||
|
|
||||||
Preparation, queuing, pickup, and cancellation are all tracked per Order Item, not per Order. An Order is just a bundle that is "complete" once every Order Item in it has been picked up — but each Order Item cooks on its own Machine, becomes ready on its own, gets notified and picked up on its own, and can be cancelled on its own (locked only once its own first Preparation Step starts). We chose this over treating the Order as the atomic unit because items in the same order can have very different cook times (a pizza vs. a burger); forcing them to stay in lockstep would mean a finished item goes cold waiting on a slower one, or a customer loses the ability to cancel a not-yet-started item just because a sibling item already started cooking.
|
Preparation, queuing, pickup, and cancellation are all tracked per Order Item, not per Order. An Order is just a bundle that is "complete" once every Order Item in it has reached a terminal state — but each Order Item cooks on its own Machine, becomes ready on its own, gets notified and picked up on its own, and can be cancelled on its own (locked only once its own current Preparation Step is claimed by a Machine). We chose this over treating the Order as the atomic unit because items in the same order can have very different cook times (a pizza vs. a burger); forcing them to stay in lockstep would mean a finished item goes cold waiting on a slower one, or a customer loses the ability to cancel a not-yet-started item just because a sibling item already started cooking.
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Single Charge per Order, refunded incrementally per cancelled Item
|
||||||
|
|
||||||
|
An Order is charged once, in full, at placement time — before any Order Item exists — rather than opening a separate charge per Order Item. When an Order Item is later cancelled, its share of the total is refunded against that same Charge, so one Charge can be partially refunded multiple times over an Order's life, once per cancelled Item. We chose one Charge per Order because it means a single payment authorization per customer checkout, keeping the payment-provider integration and the customer-facing checkout flow simple; the alternative — a Charge per Order Item — would let items be refunded fully independently of one another, but would mean authorizing the customer's card multiple times per Order and tracking a payment-provider transaction per Item instead of per Order.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Recipe snapshot per Order Item
|
||||||
|
|
||||||
|
An Order Item's full Recipe (every Preparation Step, in order) is copied from the Menu Item's live Recipe into a per-Order-Item `orderItemSteps` table at order-placement time, rather than each Step being looked up from the live Recipe as the Order Item progresses. We chose this because price is already snapshotted per Order Item for the same reason ("later menu changes don't rewrite history") — an operator editing a Menu Item's Recipe (reordering Steps, changing a Step's Machine Tag or duration) should never reshape an Order Item a customer already paid for and is partway through cooking. The live `menuItemSteps` table remains the source of truth for menu browsing and for new Orders; only `orderItemSteps` is read once an Order Item exists.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { config } from 'dotenv'
|
||||||
|
import { defineConfig } from 'drizzle-kit'
|
||||||
|
|
||||||
|
config({ path: ['.env.local', '.env'] })
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
out: './drizzle',
|
||||||
|
schema: './src/db/schema.ts',
|
||||||
|
dialect: 'postgresql',
|
||||||
|
dbCredentials: {
|
||||||
|
url: process.env.DATABASE_URL!,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
CREATE TABLE "brands" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "machine_tags" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"name" text NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "machines" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"machine_tag_id" integer NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "menu_item_steps" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"menu_item_id" integer NOT NULL,
|
||||||
|
"sequence" integer NOT NULL,
|
||||||
|
"machine_tag_id" integer NOT NULL,
|
||||||
|
"estimated_duration_seconds" integer NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "menu_items" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"brand_id" integer NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"price_cents" integer NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "order_items" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"order_id" integer NOT NULL,
|
||||||
|
"menu_item_id" integer NOT NULL,
|
||||||
|
"price_cents" integer NOT NULL,
|
||||||
|
"status" text DEFAULT 'placed' NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "orders" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"customer_id" text,
|
||||||
|
"guest_name" text,
|
||||||
|
"guest_contact" text,
|
||||||
|
"status" text DEFAULT 'placed' NOT NULL,
|
||||||
|
"payment_charge_id" text,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "preparation_step_instances" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"order_item_id" integer NOT NULL,
|
||||||
|
"sequence" integer NOT NULL,
|
||||||
|
"machine_tag_id" integer NOT NULL,
|
||||||
|
"estimated_duration_seconds" integer NOT NULL,
|
||||||
|
"status" text DEFAULT 'queued' NOT NULL,
|
||||||
|
"machine_id" integer,
|
||||||
|
"started_at" timestamp,
|
||||||
|
"done_at" timestamp
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "machines" ADD CONSTRAINT "machines_machine_tag_id_machine_tags_id_fk" FOREIGN KEY ("machine_tag_id") REFERENCES "public"."machine_tags"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "menu_item_steps" ADD CONSTRAINT "menu_item_steps_menu_item_id_menu_items_id_fk" FOREIGN KEY ("menu_item_id") REFERENCES "public"."menu_items"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "menu_item_steps" ADD CONSTRAINT "menu_item_steps_machine_tag_id_machine_tags_id_fk" FOREIGN KEY ("machine_tag_id") REFERENCES "public"."machine_tags"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "menu_items" ADD CONSTRAINT "menu_items_brand_id_brands_id_fk" FOREIGN KEY ("brand_id") REFERENCES "public"."brands"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "order_items" ADD CONSTRAINT "order_items_order_id_orders_id_fk" FOREIGN KEY ("order_id") REFERENCES "public"."orders"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "order_items" ADD CONSTRAINT "order_items_menu_item_id_menu_items_id_fk" FOREIGN KEY ("menu_item_id") REFERENCES "public"."menu_items"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "preparation_step_instances" ADD CONSTRAINT "preparation_step_instances_order_item_id_order_items_id_fk" FOREIGN KEY ("order_item_id") REFERENCES "public"."order_items"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "preparation_step_instances" ADD CONSTRAINT "preparation_step_instances_machine_tag_id_machine_tags_id_fk" FOREIGN KEY ("machine_tag_id") REFERENCES "public"."machine_tags"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "preparation_step_instances" ADD CONSTRAINT "preparation_step_instances_machine_id_machines_id_fk" FOREIGN KEY ("machine_id") REFERENCES "public"."machines"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "machine_tags_name_idx" ON "machine_tags" USING btree ("name");
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "orders" ADD COLUMN "guest_access_token" text;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
CREATE TABLE "order_item_steps" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"order_item_id" integer NOT NULL,
|
||||||
|
"sequence" integer NOT NULL,
|
||||||
|
"machine_tag_id" integer NOT NULL,
|
||||||
|
"estimated_duration_seconds" integer NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "order_item_steps" ADD CONSTRAINT "order_item_steps_order_item_id_order_items_id_fk" FOREIGN KEY ("order_item_id") REFERENCES "public"."order_items"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "order_item_steps" ADD CONSTRAINT "order_item_steps_machine_tag_id_machine_tags_id_fk" FOREIGN KEY ("machine_tag_id") REFERENCES "public"."machine_tags"("id") ON DELETE no action ON UPDATE no action;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "menu_item_steps" ADD COLUMN "name" text NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "order_item_steps" ADD COLUMN "name" text NOT NULL;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
CREATE TABLE "menu_item_options" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"menu_item_id" integer NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"price_delta_cents" integer DEFAULT 0 NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "order_item_options" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"order_item_id" integer NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"price_delta_cents" integer NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "menu_item_options" ADD CONSTRAINT "menu_item_options_menu_item_id_menu_items_id_fk" FOREIGN KEY ("menu_item_id") REFERENCES "public"."menu_items"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "order_item_options" ADD CONSTRAINT "order_item_options_order_item_id_order_items_id_fk" FOREIGN KEY ("order_item_id") REFERENCES "public"."order_items"("id") ON DELETE no action ON UPDATE no action;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
CREATE TABLE "contact_messages" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"email" text NOT NULL,
|
||||||
|
"message" text NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
CREATE TABLE "allergens" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"name" text NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "menu_item_allergens" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"menu_item_id" integer NOT NULL,
|
||||||
|
"allergen_id" integer NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "menu_item_allergens" ADD CONSTRAINT "menu_item_allergens_menu_item_id_menu_items_id_fk" FOREIGN KEY ("menu_item_id") REFERENCES "public"."menu_items"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "menu_item_allergens" ADD CONSTRAINT "menu_item_allergens_allergen_id_allergens_id_fk" FOREIGN KEY ("allergen_id") REFERENCES "public"."allergens"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "allergens_name_idx" ON "allergens" USING btree ("name");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "menu_item_allergens_item_allergen_idx" ON "menu_item_allergens" USING btree ("menu_item_id","allergen_id");
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "order_items" ADD COLUMN "cancelled_by" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "preparation_step_instances" ADD COLUMN "delay_seconds" integer DEFAULT 0 NOT NULL;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE "pickup_boxes" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"number" integer NOT NULL,
|
||||||
|
"is_open" boolean DEFAULT false NOT NULL,
|
||||||
|
CONSTRAINT "pickup_boxes_number_unique" UNIQUE("number")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "order_items" ADD COLUMN "pickup_box_id" integer;--> statement-breakpoint
|
||||||
|
ALTER TABLE "order_items" ADD CONSTRAINT "order_items_pickup_box_id_pickup_boxes_id_fk" FOREIGN KEY ("pickup_box_id") REFERENCES "public"."pickup_boxes"("id") ON DELETE no action ON UPDATE no action;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "order_items" ADD COLUMN "picked_up_at" timestamp;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "order_items" ADD COLUMN "placed_in_box_at" timestamp;
|
||||||
@@ -0,0 +1,514 @@
|
|||||||
|
{
|
||||||
|
"id": "f8244983-d51b-4ac7-a6af-e55c9f39edcd",
|
||||||
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.brands": {
|
||||||
|
"name": "brands",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.machine_tags": {
|
||||||
|
"name": "machine_tags",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"machine_tags_name_idx": {
|
||||||
|
"name": "machine_tags_name_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "name",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.machines": {
|
||||||
|
"name": "machines",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"machines_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "machines_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "machines",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_item_steps": {
|
||||||
|
"name": "menu_item_steps",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"name": "sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"estimated_duration_seconds": {
|
||||||
|
"name": "estimated_duration_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_item_steps_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "menu_item_steps_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "menu_item_steps",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"menu_item_steps_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "menu_item_steps_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "menu_item_steps",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_items": {
|
||||||
|
"name": "menu_items",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"brand_id": {
|
||||||
|
"name": "brand_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"price_cents": {
|
||||||
|
"name": "price_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_items_brand_id_brands_id_fk": {
|
||||||
|
"name": "menu_items_brand_id_brands_id_fk",
|
||||||
|
"tableFrom": "menu_items",
|
||||||
|
"tableTo": "brands",
|
||||||
|
"columnsFrom": [
|
||||||
|
"brand_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.order_items": {
|
||||||
|
"name": "order_items",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_id": {
|
||||||
|
"name": "order_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price_cents": {
|
||||||
|
"name": "price_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'placed'"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"order_items_order_id_orders_id_fk": {
|
||||||
|
"name": "order_items_order_id_orders_id_fk",
|
||||||
|
"tableFrom": "order_items",
|
||||||
|
"tableTo": "orders",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"order_items_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "order_items_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "order_items",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.orders": {
|
||||||
|
"name": "orders",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"name": "customer_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_name": {
|
||||||
|
"name": "guest_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_contact": {
|
||||||
|
"name": "guest_contact",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'placed'"
|
||||||
|
},
|
||||||
|
"payment_charge_id": {
|
||||||
|
"name": "payment_charge_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.preparation_step_instances": {
|
||||||
|
"name": "preparation_step_instances",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_item_id": {
|
||||||
|
"name": "order_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"name": "sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"estimated_duration_seconds": {
|
||||||
|
"name": "estimated_duration_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'queued'"
|
||||||
|
},
|
||||||
|
"machine_id": {
|
||||||
|
"name": "machine_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"started_at": {
|
||||||
|
"name": "started_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"done_at": {
|
||||||
|
"name": "done_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"preparation_step_instances_order_item_id_order_items_id_fk": {
|
||||||
|
"name": "preparation_step_instances_order_item_id_order_items_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "order_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"preparation_step_instances_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "preparation_step_instances_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"preparation_step_instances_machine_id_machines_id_fk": {
|
||||||
|
"name": "preparation_step_instances_machine_id_machines_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "machines",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,520 @@
|
|||||||
|
{
|
||||||
|
"id": "816e0a1c-3005-433c-911b-2e006cc9e91b",
|
||||||
|
"prevId": "f8244983-d51b-4ac7-a6af-e55c9f39edcd",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.brands": {
|
||||||
|
"name": "brands",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.machine_tags": {
|
||||||
|
"name": "machine_tags",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"machine_tags_name_idx": {
|
||||||
|
"name": "machine_tags_name_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "name",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.machines": {
|
||||||
|
"name": "machines",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"machines_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "machines_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "machines",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_item_steps": {
|
||||||
|
"name": "menu_item_steps",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"name": "sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"estimated_duration_seconds": {
|
||||||
|
"name": "estimated_duration_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_item_steps_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "menu_item_steps_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "menu_item_steps",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"menu_item_steps_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "menu_item_steps_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "menu_item_steps",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_items": {
|
||||||
|
"name": "menu_items",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"brand_id": {
|
||||||
|
"name": "brand_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"price_cents": {
|
||||||
|
"name": "price_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_items_brand_id_brands_id_fk": {
|
||||||
|
"name": "menu_items_brand_id_brands_id_fk",
|
||||||
|
"tableFrom": "menu_items",
|
||||||
|
"tableTo": "brands",
|
||||||
|
"columnsFrom": [
|
||||||
|
"brand_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.order_items": {
|
||||||
|
"name": "order_items",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_id": {
|
||||||
|
"name": "order_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price_cents": {
|
||||||
|
"name": "price_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'placed'"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"order_items_order_id_orders_id_fk": {
|
||||||
|
"name": "order_items_order_id_orders_id_fk",
|
||||||
|
"tableFrom": "order_items",
|
||||||
|
"tableTo": "orders",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"order_items_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "order_items_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "order_items",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.orders": {
|
||||||
|
"name": "orders",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"name": "customer_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_name": {
|
||||||
|
"name": "guest_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_contact": {
|
||||||
|
"name": "guest_contact",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_access_token": {
|
||||||
|
"name": "guest_access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'placed'"
|
||||||
|
},
|
||||||
|
"payment_charge_id": {
|
||||||
|
"name": "payment_charge_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.preparation_step_instances": {
|
||||||
|
"name": "preparation_step_instances",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_item_id": {
|
||||||
|
"name": "order_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"name": "sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"estimated_duration_seconds": {
|
||||||
|
"name": "estimated_duration_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'queued'"
|
||||||
|
},
|
||||||
|
"machine_id": {
|
||||||
|
"name": "machine_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"started_at": {
|
||||||
|
"name": "started_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"done_at": {
|
||||||
|
"name": "done_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"preparation_step_instances_order_item_id_order_items_id_fk": {
|
||||||
|
"name": "preparation_step_instances_order_item_id_order_items_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "order_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"preparation_step_instances_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "preparation_step_instances_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"preparation_step_instances_machine_id_machines_id_fk": {
|
||||||
|
"name": "preparation_step_instances_machine_id_machines_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "machines",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,590 @@
|
|||||||
|
{
|
||||||
|
"id": "aecfb37f-fb88-44ba-8e02-f290a3929293",
|
||||||
|
"prevId": "816e0a1c-3005-433c-911b-2e006cc9e91b",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.brands": {
|
||||||
|
"name": "brands",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.machine_tags": {
|
||||||
|
"name": "machine_tags",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"machine_tags_name_idx": {
|
||||||
|
"name": "machine_tags_name_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "name",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.machines": {
|
||||||
|
"name": "machines",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"machines_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "machines_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "machines",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_item_steps": {
|
||||||
|
"name": "menu_item_steps",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"name": "sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"estimated_duration_seconds": {
|
||||||
|
"name": "estimated_duration_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_item_steps_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "menu_item_steps_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "menu_item_steps",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"menu_item_steps_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "menu_item_steps_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "menu_item_steps",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_items": {
|
||||||
|
"name": "menu_items",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"brand_id": {
|
||||||
|
"name": "brand_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"price_cents": {
|
||||||
|
"name": "price_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_items_brand_id_brands_id_fk": {
|
||||||
|
"name": "menu_items_brand_id_brands_id_fk",
|
||||||
|
"tableFrom": "menu_items",
|
||||||
|
"tableTo": "brands",
|
||||||
|
"columnsFrom": [
|
||||||
|
"brand_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.order_item_steps": {
|
||||||
|
"name": "order_item_steps",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_item_id": {
|
||||||
|
"name": "order_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"name": "sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"estimated_duration_seconds": {
|
||||||
|
"name": "estimated_duration_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"order_item_steps_order_item_id_order_items_id_fk": {
|
||||||
|
"name": "order_item_steps_order_item_id_order_items_id_fk",
|
||||||
|
"tableFrom": "order_item_steps",
|
||||||
|
"tableTo": "order_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"order_item_steps_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "order_item_steps_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "order_item_steps",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.order_items": {
|
||||||
|
"name": "order_items",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_id": {
|
||||||
|
"name": "order_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price_cents": {
|
||||||
|
"name": "price_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'placed'"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"order_items_order_id_orders_id_fk": {
|
||||||
|
"name": "order_items_order_id_orders_id_fk",
|
||||||
|
"tableFrom": "order_items",
|
||||||
|
"tableTo": "orders",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"order_items_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "order_items_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "order_items",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.orders": {
|
||||||
|
"name": "orders",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"name": "customer_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_name": {
|
||||||
|
"name": "guest_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_contact": {
|
||||||
|
"name": "guest_contact",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_access_token": {
|
||||||
|
"name": "guest_access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'placed'"
|
||||||
|
},
|
||||||
|
"payment_charge_id": {
|
||||||
|
"name": "payment_charge_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.preparation_step_instances": {
|
||||||
|
"name": "preparation_step_instances",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_item_id": {
|
||||||
|
"name": "order_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"name": "sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"estimated_duration_seconds": {
|
||||||
|
"name": "estimated_duration_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'queued'"
|
||||||
|
},
|
||||||
|
"machine_id": {
|
||||||
|
"name": "machine_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"started_at": {
|
||||||
|
"name": "started_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"done_at": {
|
||||||
|
"name": "done_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"preparation_step_instances_order_item_id_order_items_id_fk": {
|
||||||
|
"name": "preparation_step_instances_order_item_id_order_items_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "order_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"preparation_step_instances_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "preparation_step_instances_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"preparation_step_instances_machine_id_machines_id_fk": {
|
||||||
|
"name": "preparation_step_instances_machine_id_machines_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "machines",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,602 @@
|
|||||||
|
{
|
||||||
|
"id": "3761e215-1f2d-4374-83af-942ef92e41df",
|
||||||
|
"prevId": "aecfb37f-fb88-44ba-8e02-f290a3929293",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.brands": {
|
||||||
|
"name": "brands",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.machine_tags": {
|
||||||
|
"name": "machine_tags",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"machine_tags_name_idx": {
|
||||||
|
"name": "machine_tags_name_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "name",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.machines": {
|
||||||
|
"name": "machines",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"machines_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "machines_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "machines",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_item_steps": {
|
||||||
|
"name": "menu_item_steps",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"name": "sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"estimated_duration_seconds": {
|
||||||
|
"name": "estimated_duration_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_item_steps_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "menu_item_steps_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "menu_item_steps",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"menu_item_steps_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "menu_item_steps_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "menu_item_steps",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_items": {
|
||||||
|
"name": "menu_items",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"brand_id": {
|
||||||
|
"name": "brand_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"price_cents": {
|
||||||
|
"name": "price_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_items_brand_id_brands_id_fk": {
|
||||||
|
"name": "menu_items_brand_id_brands_id_fk",
|
||||||
|
"tableFrom": "menu_items",
|
||||||
|
"tableTo": "brands",
|
||||||
|
"columnsFrom": [
|
||||||
|
"brand_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.order_item_steps": {
|
||||||
|
"name": "order_item_steps",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_item_id": {
|
||||||
|
"name": "order_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"name": "sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"estimated_duration_seconds": {
|
||||||
|
"name": "estimated_duration_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"order_item_steps_order_item_id_order_items_id_fk": {
|
||||||
|
"name": "order_item_steps_order_item_id_order_items_id_fk",
|
||||||
|
"tableFrom": "order_item_steps",
|
||||||
|
"tableTo": "order_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"order_item_steps_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "order_item_steps_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "order_item_steps",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.order_items": {
|
||||||
|
"name": "order_items",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_id": {
|
||||||
|
"name": "order_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price_cents": {
|
||||||
|
"name": "price_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'placed'"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"order_items_order_id_orders_id_fk": {
|
||||||
|
"name": "order_items_order_id_orders_id_fk",
|
||||||
|
"tableFrom": "order_items",
|
||||||
|
"tableTo": "orders",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"order_items_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "order_items_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "order_items",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.orders": {
|
||||||
|
"name": "orders",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"name": "customer_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_name": {
|
||||||
|
"name": "guest_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_contact": {
|
||||||
|
"name": "guest_contact",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_access_token": {
|
||||||
|
"name": "guest_access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'placed'"
|
||||||
|
},
|
||||||
|
"payment_charge_id": {
|
||||||
|
"name": "payment_charge_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.preparation_step_instances": {
|
||||||
|
"name": "preparation_step_instances",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_item_id": {
|
||||||
|
"name": "order_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"name": "sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"estimated_duration_seconds": {
|
||||||
|
"name": "estimated_duration_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'queued'"
|
||||||
|
},
|
||||||
|
"machine_id": {
|
||||||
|
"name": "machine_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"started_at": {
|
||||||
|
"name": "started_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"done_at": {
|
||||||
|
"name": "done_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"preparation_step_instances_order_item_id_order_items_id_fk": {
|
||||||
|
"name": "preparation_step_instances_order_item_id_order_items_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "order_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"preparation_step_instances_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "preparation_step_instances_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"preparation_step_instances_machine_id_machines_id_fk": {
|
||||||
|
"name": "preparation_step_instances_machine_id_machines_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "machines",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,705 @@
|
|||||||
|
{
|
||||||
|
"id": "cd474707-7446-42df-afd9-44d66a9a03a2",
|
||||||
|
"prevId": "3761e215-1f2d-4374-83af-942ef92e41df",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.brands": {
|
||||||
|
"name": "brands",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.machine_tags": {
|
||||||
|
"name": "machine_tags",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"machine_tags_name_idx": {
|
||||||
|
"name": "machine_tags_name_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "name",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.machines": {
|
||||||
|
"name": "machines",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"machines_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "machines_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "machines",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_item_options": {
|
||||||
|
"name": "menu_item_options",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price_delta_cents": {
|
||||||
|
"name": "price_delta_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_item_options_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "menu_item_options_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "menu_item_options",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_item_steps": {
|
||||||
|
"name": "menu_item_steps",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"name": "sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"estimated_duration_seconds": {
|
||||||
|
"name": "estimated_duration_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_item_steps_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "menu_item_steps_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "menu_item_steps",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"menu_item_steps_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "menu_item_steps_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "menu_item_steps",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_items": {
|
||||||
|
"name": "menu_items",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"brand_id": {
|
||||||
|
"name": "brand_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"price_cents": {
|
||||||
|
"name": "price_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_items_brand_id_brands_id_fk": {
|
||||||
|
"name": "menu_items_brand_id_brands_id_fk",
|
||||||
|
"tableFrom": "menu_items",
|
||||||
|
"tableTo": "brands",
|
||||||
|
"columnsFrom": [
|
||||||
|
"brand_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.order_item_options": {
|
||||||
|
"name": "order_item_options",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_item_id": {
|
||||||
|
"name": "order_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price_delta_cents": {
|
||||||
|
"name": "price_delta_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"order_item_options_order_item_id_order_items_id_fk": {
|
||||||
|
"name": "order_item_options_order_item_id_order_items_id_fk",
|
||||||
|
"tableFrom": "order_item_options",
|
||||||
|
"tableTo": "order_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.order_item_steps": {
|
||||||
|
"name": "order_item_steps",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_item_id": {
|
||||||
|
"name": "order_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"name": "sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"estimated_duration_seconds": {
|
||||||
|
"name": "estimated_duration_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"order_item_steps_order_item_id_order_items_id_fk": {
|
||||||
|
"name": "order_item_steps_order_item_id_order_items_id_fk",
|
||||||
|
"tableFrom": "order_item_steps",
|
||||||
|
"tableTo": "order_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"order_item_steps_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "order_item_steps_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "order_item_steps",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.order_items": {
|
||||||
|
"name": "order_items",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_id": {
|
||||||
|
"name": "order_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price_cents": {
|
||||||
|
"name": "price_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'placed'"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"order_items_order_id_orders_id_fk": {
|
||||||
|
"name": "order_items_order_id_orders_id_fk",
|
||||||
|
"tableFrom": "order_items",
|
||||||
|
"tableTo": "orders",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"order_items_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "order_items_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "order_items",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.orders": {
|
||||||
|
"name": "orders",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"name": "customer_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_name": {
|
||||||
|
"name": "guest_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_contact": {
|
||||||
|
"name": "guest_contact",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_access_token": {
|
||||||
|
"name": "guest_access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'placed'"
|
||||||
|
},
|
||||||
|
"payment_charge_id": {
|
||||||
|
"name": "payment_charge_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.preparation_step_instances": {
|
||||||
|
"name": "preparation_step_instances",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_item_id": {
|
||||||
|
"name": "order_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"name": "sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"estimated_duration_seconds": {
|
||||||
|
"name": "estimated_duration_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'queued'"
|
||||||
|
},
|
||||||
|
"machine_id": {
|
||||||
|
"name": "machine_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"started_at": {
|
||||||
|
"name": "started_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"done_at": {
|
||||||
|
"name": "done_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"preparation_step_instances_order_item_id_order_items_id_fk": {
|
||||||
|
"name": "preparation_step_instances_order_item_id_order_items_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "order_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"preparation_step_instances_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "preparation_step_instances_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"preparation_step_instances_machine_id_machines_id_fk": {
|
||||||
|
"name": "preparation_step_instances_machine_id_machines_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "machines",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,749 @@
|
|||||||
|
{
|
||||||
|
"id": "83dc14cf-82fb-43fc-8b7d-fd377bf48689",
|
||||||
|
"prevId": "cd474707-7446-42df-afd9-44d66a9a03a2",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.brands": {
|
||||||
|
"name": "brands",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.contact_messages": {
|
||||||
|
"name": "contact_messages",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"name": "message",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.machine_tags": {
|
||||||
|
"name": "machine_tags",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"machine_tags_name_idx": {
|
||||||
|
"name": "machine_tags_name_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "name",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.machines": {
|
||||||
|
"name": "machines",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"machines_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "machines_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "machines",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_item_options": {
|
||||||
|
"name": "menu_item_options",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price_delta_cents": {
|
||||||
|
"name": "price_delta_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_item_options_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "menu_item_options_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "menu_item_options",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_item_steps": {
|
||||||
|
"name": "menu_item_steps",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"name": "sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"estimated_duration_seconds": {
|
||||||
|
"name": "estimated_duration_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_item_steps_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "menu_item_steps_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "menu_item_steps",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"menu_item_steps_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "menu_item_steps_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "menu_item_steps",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_items": {
|
||||||
|
"name": "menu_items",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"brand_id": {
|
||||||
|
"name": "brand_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"price_cents": {
|
||||||
|
"name": "price_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_items_brand_id_brands_id_fk": {
|
||||||
|
"name": "menu_items_brand_id_brands_id_fk",
|
||||||
|
"tableFrom": "menu_items",
|
||||||
|
"tableTo": "brands",
|
||||||
|
"columnsFrom": [
|
||||||
|
"brand_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.order_item_options": {
|
||||||
|
"name": "order_item_options",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_item_id": {
|
||||||
|
"name": "order_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price_delta_cents": {
|
||||||
|
"name": "price_delta_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"order_item_options_order_item_id_order_items_id_fk": {
|
||||||
|
"name": "order_item_options_order_item_id_order_items_id_fk",
|
||||||
|
"tableFrom": "order_item_options",
|
||||||
|
"tableTo": "order_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.order_item_steps": {
|
||||||
|
"name": "order_item_steps",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_item_id": {
|
||||||
|
"name": "order_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"name": "sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"estimated_duration_seconds": {
|
||||||
|
"name": "estimated_duration_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"order_item_steps_order_item_id_order_items_id_fk": {
|
||||||
|
"name": "order_item_steps_order_item_id_order_items_id_fk",
|
||||||
|
"tableFrom": "order_item_steps",
|
||||||
|
"tableTo": "order_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"order_item_steps_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "order_item_steps_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "order_item_steps",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.order_items": {
|
||||||
|
"name": "order_items",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_id": {
|
||||||
|
"name": "order_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price_cents": {
|
||||||
|
"name": "price_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'placed'"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"order_items_order_id_orders_id_fk": {
|
||||||
|
"name": "order_items_order_id_orders_id_fk",
|
||||||
|
"tableFrom": "order_items",
|
||||||
|
"tableTo": "orders",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"order_items_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "order_items_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "order_items",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.orders": {
|
||||||
|
"name": "orders",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"name": "customer_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_name": {
|
||||||
|
"name": "guest_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_contact": {
|
||||||
|
"name": "guest_contact",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_access_token": {
|
||||||
|
"name": "guest_access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'placed'"
|
||||||
|
},
|
||||||
|
"payment_charge_id": {
|
||||||
|
"name": "payment_charge_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.preparation_step_instances": {
|
||||||
|
"name": "preparation_step_instances",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_item_id": {
|
||||||
|
"name": "order_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"name": "sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"estimated_duration_seconds": {
|
||||||
|
"name": "estimated_duration_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'queued'"
|
||||||
|
},
|
||||||
|
"machine_id": {
|
||||||
|
"name": "machine_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"started_at": {
|
||||||
|
"name": "started_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"done_at": {
|
||||||
|
"name": "done_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"preparation_step_instances_order_item_id_order_items_id_fk": {
|
||||||
|
"name": "preparation_step_instances_order_item_id_order_items_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "order_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"preparation_step_instances_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "preparation_step_instances_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"preparation_step_instances_machine_id_machines_id_fk": {
|
||||||
|
"name": "preparation_step_instances_machine_id_machines_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "machines",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,870 @@
|
|||||||
|
{
|
||||||
|
"id": "4086d6cd-5c93-4617-a15b-a90331b459a1",
|
||||||
|
"prevId": "83dc14cf-82fb-43fc-8b7d-fd377bf48689",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.allergens": {
|
||||||
|
"name": "allergens",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"allergens_name_idx": {
|
||||||
|
"name": "allergens_name_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "name",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.brands": {
|
||||||
|
"name": "brands",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.contact_messages": {
|
||||||
|
"name": "contact_messages",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"name": "message",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.machine_tags": {
|
||||||
|
"name": "machine_tags",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"machine_tags_name_idx": {
|
||||||
|
"name": "machine_tags_name_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "name",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.machines": {
|
||||||
|
"name": "machines",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"machines_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "machines_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "machines",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_item_allergens": {
|
||||||
|
"name": "menu_item_allergens",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"allergen_id": {
|
||||||
|
"name": "allergen_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"menu_item_allergens_item_allergen_idx": {
|
||||||
|
"name": "menu_item_allergens_item_allergen_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "menu_item_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "allergen_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_item_allergens_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "menu_item_allergens_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "menu_item_allergens",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"menu_item_allergens_allergen_id_allergens_id_fk": {
|
||||||
|
"name": "menu_item_allergens_allergen_id_allergens_id_fk",
|
||||||
|
"tableFrom": "menu_item_allergens",
|
||||||
|
"tableTo": "allergens",
|
||||||
|
"columnsFrom": [
|
||||||
|
"allergen_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_item_options": {
|
||||||
|
"name": "menu_item_options",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price_delta_cents": {
|
||||||
|
"name": "price_delta_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_item_options_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "menu_item_options_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "menu_item_options",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_item_steps": {
|
||||||
|
"name": "menu_item_steps",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"name": "sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"estimated_duration_seconds": {
|
||||||
|
"name": "estimated_duration_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_item_steps_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "menu_item_steps_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "menu_item_steps",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"menu_item_steps_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "menu_item_steps_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "menu_item_steps",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_items": {
|
||||||
|
"name": "menu_items",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"brand_id": {
|
||||||
|
"name": "brand_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"price_cents": {
|
||||||
|
"name": "price_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_items_brand_id_brands_id_fk": {
|
||||||
|
"name": "menu_items_brand_id_brands_id_fk",
|
||||||
|
"tableFrom": "menu_items",
|
||||||
|
"tableTo": "brands",
|
||||||
|
"columnsFrom": [
|
||||||
|
"brand_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.order_item_options": {
|
||||||
|
"name": "order_item_options",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_item_id": {
|
||||||
|
"name": "order_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price_delta_cents": {
|
||||||
|
"name": "price_delta_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"order_item_options_order_item_id_order_items_id_fk": {
|
||||||
|
"name": "order_item_options_order_item_id_order_items_id_fk",
|
||||||
|
"tableFrom": "order_item_options",
|
||||||
|
"tableTo": "order_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.order_item_steps": {
|
||||||
|
"name": "order_item_steps",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_item_id": {
|
||||||
|
"name": "order_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"name": "sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"estimated_duration_seconds": {
|
||||||
|
"name": "estimated_duration_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"order_item_steps_order_item_id_order_items_id_fk": {
|
||||||
|
"name": "order_item_steps_order_item_id_order_items_id_fk",
|
||||||
|
"tableFrom": "order_item_steps",
|
||||||
|
"tableTo": "order_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"order_item_steps_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "order_item_steps_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "order_item_steps",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.order_items": {
|
||||||
|
"name": "order_items",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_id": {
|
||||||
|
"name": "order_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price_cents": {
|
||||||
|
"name": "price_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'placed'"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"order_items_order_id_orders_id_fk": {
|
||||||
|
"name": "order_items_order_id_orders_id_fk",
|
||||||
|
"tableFrom": "order_items",
|
||||||
|
"tableTo": "orders",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"order_items_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "order_items_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "order_items",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.orders": {
|
||||||
|
"name": "orders",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"name": "customer_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_name": {
|
||||||
|
"name": "guest_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_contact": {
|
||||||
|
"name": "guest_contact",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_access_token": {
|
||||||
|
"name": "guest_access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'placed'"
|
||||||
|
},
|
||||||
|
"payment_charge_id": {
|
||||||
|
"name": "payment_charge_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.preparation_step_instances": {
|
||||||
|
"name": "preparation_step_instances",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_item_id": {
|
||||||
|
"name": "order_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"name": "sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"estimated_duration_seconds": {
|
||||||
|
"name": "estimated_duration_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'queued'"
|
||||||
|
},
|
||||||
|
"machine_id": {
|
||||||
|
"name": "machine_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"started_at": {
|
||||||
|
"name": "started_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"done_at": {
|
||||||
|
"name": "done_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"preparation_step_instances_order_item_id_order_items_id_fk": {
|
||||||
|
"name": "preparation_step_instances_order_item_id_order_items_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "order_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"preparation_step_instances_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "preparation_step_instances_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"preparation_step_instances_machine_id_machines_id_fk": {
|
||||||
|
"name": "preparation_step_instances_machine_id_machines_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "machines",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,883 @@
|
|||||||
|
{
|
||||||
|
"id": "1cec3157-9481-4f6a-83b0-a79064d2bed5",
|
||||||
|
"prevId": "4086d6cd-5c93-4617-a15b-a90331b459a1",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.allergens": {
|
||||||
|
"name": "allergens",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"allergens_name_idx": {
|
||||||
|
"name": "allergens_name_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "name",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.brands": {
|
||||||
|
"name": "brands",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.contact_messages": {
|
||||||
|
"name": "contact_messages",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"name": "message",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.machine_tags": {
|
||||||
|
"name": "machine_tags",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"machine_tags_name_idx": {
|
||||||
|
"name": "machine_tags_name_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "name",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.machines": {
|
||||||
|
"name": "machines",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"machines_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "machines_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "machines",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_item_allergens": {
|
||||||
|
"name": "menu_item_allergens",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"allergen_id": {
|
||||||
|
"name": "allergen_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"menu_item_allergens_item_allergen_idx": {
|
||||||
|
"name": "menu_item_allergens_item_allergen_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "menu_item_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "allergen_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_item_allergens_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "menu_item_allergens_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "menu_item_allergens",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"menu_item_allergens_allergen_id_allergens_id_fk": {
|
||||||
|
"name": "menu_item_allergens_allergen_id_allergens_id_fk",
|
||||||
|
"tableFrom": "menu_item_allergens",
|
||||||
|
"tableTo": "allergens",
|
||||||
|
"columnsFrom": [
|
||||||
|
"allergen_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_item_options": {
|
||||||
|
"name": "menu_item_options",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price_delta_cents": {
|
||||||
|
"name": "price_delta_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_item_options_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "menu_item_options_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "menu_item_options",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_item_steps": {
|
||||||
|
"name": "menu_item_steps",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"name": "sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"estimated_duration_seconds": {
|
||||||
|
"name": "estimated_duration_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_item_steps_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "menu_item_steps_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "menu_item_steps",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"menu_item_steps_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "menu_item_steps_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "menu_item_steps",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_items": {
|
||||||
|
"name": "menu_items",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"brand_id": {
|
||||||
|
"name": "brand_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"price_cents": {
|
||||||
|
"name": "price_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_items_brand_id_brands_id_fk": {
|
||||||
|
"name": "menu_items_brand_id_brands_id_fk",
|
||||||
|
"tableFrom": "menu_items",
|
||||||
|
"tableTo": "brands",
|
||||||
|
"columnsFrom": [
|
||||||
|
"brand_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.order_item_options": {
|
||||||
|
"name": "order_item_options",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_item_id": {
|
||||||
|
"name": "order_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price_delta_cents": {
|
||||||
|
"name": "price_delta_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"order_item_options_order_item_id_order_items_id_fk": {
|
||||||
|
"name": "order_item_options_order_item_id_order_items_id_fk",
|
||||||
|
"tableFrom": "order_item_options",
|
||||||
|
"tableTo": "order_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.order_item_steps": {
|
||||||
|
"name": "order_item_steps",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_item_id": {
|
||||||
|
"name": "order_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"name": "sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"estimated_duration_seconds": {
|
||||||
|
"name": "estimated_duration_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"order_item_steps_order_item_id_order_items_id_fk": {
|
||||||
|
"name": "order_item_steps_order_item_id_order_items_id_fk",
|
||||||
|
"tableFrom": "order_item_steps",
|
||||||
|
"tableTo": "order_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"order_item_steps_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "order_item_steps_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "order_item_steps",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.order_items": {
|
||||||
|
"name": "order_items",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_id": {
|
||||||
|
"name": "order_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price_cents": {
|
||||||
|
"name": "price_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'placed'"
|
||||||
|
},
|
||||||
|
"cancelled_by": {
|
||||||
|
"name": "cancelled_by",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"order_items_order_id_orders_id_fk": {
|
||||||
|
"name": "order_items_order_id_orders_id_fk",
|
||||||
|
"tableFrom": "order_items",
|
||||||
|
"tableTo": "orders",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"order_items_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "order_items_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "order_items",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.orders": {
|
||||||
|
"name": "orders",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"name": "customer_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_name": {
|
||||||
|
"name": "guest_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_contact": {
|
||||||
|
"name": "guest_contact",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_access_token": {
|
||||||
|
"name": "guest_access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'placed'"
|
||||||
|
},
|
||||||
|
"payment_charge_id": {
|
||||||
|
"name": "payment_charge_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.preparation_step_instances": {
|
||||||
|
"name": "preparation_step_instances",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_item_id": {
|
||||||
|
"name": "order_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"name": "sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"estimated_duration_seconds": {
|
||||||
|
"name": "estimated_duration_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"delay_seconds": {
|
||||||
|
"name": "delay_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'queued'"
|
||||||
|
},
|
||||||
|
"machine_id": {
|
||||||
|
"name": "machine_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"started_at": {
|
||||||
|
"name": "started_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"done_at": {
|
||||||
|
"name": "done_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"preparation_step_instances_order_item_id_order_items_id_fk": {
|
||||||
|
"name": "preparation_step_instances_order_item_id_order_items_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "order_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"preparation_step_instances_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "preparation_step_instances_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"preparation_step_instances_machine_id_machines_id_fk": {
|
||||||
|
"name": "preparation_step_instances_machine_id_machines_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "machines",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,942 @@
|
|||||||
|
{
|
||||||
|
"id": "42d1f3c1-18f8-42a3-ac41-9e29a4b14add",
|
||||||
|
"prevId": "1cec3157-9481-4f6a-83b0-a79064d2bed5",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.allergens": {
|
||||||
|
"name": "allergens",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"allergens_name_idx": {
|
||||||
|
"name": "allergens_name_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "name",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.brands": {
|
||||||
|
"name": "brands",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.contact_messages": {
|
||||||
|
"name": "contact_messages",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"name": "message",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.machine_tags": {
|
||||||
|
"name": "machine_tags",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"machine_tags_name_idx": {
|
||||||
|
"name": "machine_tags_name_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "name",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.machines": {
|
||||||
|
"name": "machines",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"machines_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "machines_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "machines",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_item_allergens": {
|
||||||
|
"name": "menu_item_allergens",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"allergen_id": {
|
||||||
|
"name": "allergen_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"menu_item_allergens_item_allergen_idx": {
|
||||||
|
"name": "menu_item_allergens_item_allergen_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "menu_item_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "allergen_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_item_allergens_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "menu_item_allergens_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "menu_item_allergens",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"menu_item_allergens_allergen_id_allergens_id_fk": {
|
||||||
|
"name": "menu_item_allergens_allergen_id_allergens_id_fk",
|
||||||
|
"tableFrom": "menu_item_allergens",
|
||||||
|
"tableTo": "allergens",
|
||||||
|
"columnsFrom": [
|
||||||
|
"allergen_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_item_options": {
|
||||||
|
"name": "menu_item_options",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price_delta_cents": {
|
||||||
|
"name": "price_delta_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_item_options_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "menu_item_options_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "menu_item_options",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_item_steps": {
|
||||||
|
"name": "menu_item_steps",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"name": "sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"estimated_duration_seconds": {
|
||||||
|
"name": "estimated_duration_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_item_steps_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "menu_item_steps_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "menu_item_steps",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"menu_item_steps_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "menu_item_steps_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "menu_item_steps",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_items": {
|
||||||
|
"name": "menu_items",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"brand_id": {
|
||||||
|
"name": "brand_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"price_cents": {
|
||||||
|
"name": "price_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_items_brand_id_brands_id_fk": {
|
||||||
|
"name": "menu_items_brand_id_brands_id_fk",
|
||||||
|
"tableFrom": "menu_items",
|
||||||
|
"tableTo": "brands",
|
||||||
|
"columnsFrom": [
|
||||||
|
"brand_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.order_item_options": {
|
||||||
|
"name": "order_item_options",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_item_id": {
|
||||||
|
"name": "order_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price_delta_cents": {
|
||||||
|
"name": "price_delta_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"order_item_options_order_item_id_order_items_id_fk": {
|
||||||
|
"name": "order_item_options_order_item_id_order_items_id_fk",
|
||||||
|
"tableFrom": "order_item_options",
|
||||||
|
"tableTo": "order_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.order_item_steps": {
|
||||||
|
"name": "order_item_steps",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_item_id": {
|
||||||
|
"name": "order_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"name": "sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"estimated_duration_seconds": {
|
||||||
|
"name": "estimated_duration_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"order_item_steps_order_item_id_order_items_id_fk": {
|
||||||
|
"name": "order_item_steps_order_item_id_order_items_id_fk",
|
||||||
|
"tableFrom": "order_item_steps",
|
||||||
|
"tableTo": "order_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"order_item_steps_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "order_item_steps_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "order_item_steps",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.order_items": {
|
||||||
|
"name": "order_items",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_id": {
|
||||||
|
"name": "order_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price_cents": {
|
||||||
|
"name": "price_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'placed'"
|
||||||
|
},
|
||||||
|
"cancelled_by": {
|
||||||
|
"name": "cancelled_by",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"pickup_box_id": {
|
||||||
|
"name": "pickup_box_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"order_items_order_id_orders_id_fk": {
|
||||||
|
"name": "order_items_order_id_orders_id_fk",
|
||||||
|
"tableFrom": "order_items",
|
||||||
|
"tableTo": "orders",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"order_items_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "order_items_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "order_items",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"order_items_pickup_box_id_pickup_boxes_id_fk": {
|
||||||
|
"name": "order_items_pickup_box_id_pickup_boxes_id_fk",
|
||||||
|
"tableFrom": "order_items",
|
||||||
|
"tableTo": "pickup_boxes",
|
||||||
|
"columnsFrom": [
|
||||||
|
"pickup_box_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.orders": {
|
||||||
|
"name": "orders",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"name": "customer_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_name": {
|
||||||
|
"name": "guest_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_contact": {
|
||||||
|
"name": "guest_contact",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_access_token": {
|
||||||
|
"name": "guest_access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'placed'"
|
||||||
|
},
|
||||||
|
"payment_charge_id": {
|
||||||
|
"name": "payment_charge_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.pickup_boxes": {
|
||||||
|
"name": "pickup_boxes",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"number": {
|
||||||
|
"name": "number",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"is_open": {
|
||||||
|
"name": "is_open",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"pickup_boxes_number_unique": {
|
||||||
|
"name": "pickup_boxes_number_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"number"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.preparation_step_instances": {
|
||||||
|
"name": "preparation_step_instances",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_item_id": {
|
||||||
|
"name": "order_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"name": "sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"estimated_duration_seconds": {
|
||||||
|
"name": "estimated_duration_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"delay_seconds": {
|
||||||
|
"name": "delay_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'queued'"
|
||||||
|
},
|
||||||
|
"machine_id": {
|
||||||
|
"name": "machine_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"started_at": {
|
||||||
|
"name": "started_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"done_at": {
|
||||||
|
"name": "done_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"preparation_step_instances_order_item_id_order_items_id_fk": {
|
||||||
|
"name": "preparation_step_instances_order_item_id_order_items_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "order_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"preparation_step_instances_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "preparation_step_instances_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"preparation_step_instances_machine_id_machines_id_fk": {
|
||||||
|
"name": "preparation_step_instances_machine_id_machines_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "machines",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,948 @@
|
|||||||
|
{
|
||||||
|
"id": "4826f90c-c779-4b2d-ab6e-491e72dbaee8",
|
||||||
|
"prevId": "42d1f3c1-18f8-42a3-ac41-9e29a4b14add",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.allergens": {
|
||||||
|
"name": "allergens",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"allergens_name_idx": {
|
||||||
|
"name": "allergens_name_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "name",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.brands": {
|
||||||
|
"name": "brands",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.contact_messages": {
|
||||||
|
"name": "contact_messages",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"name": "message",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.machine_tags": {
|
||||||
|
"name": "machine_tags",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"machine_tags_name_idx": {
|
||||||
|
"name": "machine_tags_name_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "name",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.machines": {
|
||||||
|
"name": "machines",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"machines_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "machines_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "machines",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_item_allergens": {
|
||||||
|
"name": "menu_item_allergens",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"allergen_id": {
|
||||||
|
"name": "allergen_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"menu_item_allergens_item_allergen_idx": {
|
||||||
|
"name": "menu_item_allergens_item_allergen_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "menu_item_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "allergen_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_item_allergens_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "menu_item_allergens_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "menu_item_allergens",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"menu_item_allergens_allergen_id_allergens_id_fk": {
|
||||||
|
"name": "menu_item_allergens_allergen_id_allergens_id_fk",
|
||||||
|
"tableFrom": "menu_item_allergens",
|
||||||
|
"tableTo": "allergens",
|
||||||
|
"columnsFrom": [
|
||||||
|
"allergen_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_item_options": {
|
||||||
|
"name": "menu_item_options",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price_delta_cents": {
|
||||||
|
"name": "price_delta_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_item_options_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "menu_item_options_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "menu_item_options",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_item_steps": {
|
||||||
|
"name": "menu_item_steps",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"name": "sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"estimated_duration_seconds": {
|
||||||
|
"name": "estimated_duration_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_item_steps_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "menu_item_steps_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "menu_item_steps",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"menu_item_steps_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "menu_item_steps_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "menu_item_steps",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_items": {
|
||||||
|
"name": "menu_items",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"brand_id": {
|
||||||
|
"name": "brand_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"price_cents": {
|
||||||
|
"name": "price_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_items_brand_id_brands_id_fk": {
|
||||||
|
"name": "menu_items_brand_id_brands_id_fk",
|
||||||
|
"tableFrom": "menu_items",
|
||||||
|
"tableTo": "brands",
|
||||||
|
"columnsFrom": [
|
||||||
|
"brand_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.order_item_options": {
|
||||||
|
"name": "order_item_options",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_item_id": {
|
||||||
|
"name": "order_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price_delta_cents": {
|
||||||
|
"name": "price_delta_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"order_item_options_order_item_id_order_items_id_fk": {
|
||||||
|
"name": "order_item_options_order_item_id_order_items_id_fk",
|
||||||
|
"tableFrom": "order_item_options",
|
||||||
|
"tableTo": "order_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.order_item_steps": {
|
||||||
|
"name": "order_item_steps",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_item_id": {
|
||||||
|
"name": "order_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"name": "sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"estimated_duration_seconds": {
|
||||||
|
"name": "estimated_duration_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"order_item_steps_order_item_id_order_items_id_fk": {
|
||||||
|
"name": "order_item_steps_order_item_id_order_items_id_fk",
|
||||||
|
"tableFrom": "order_item_steps",
|
||||||
|
"tableTo": "order_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"order_item_steps_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "order_item_steps_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "order_item_steps",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.order_items": {
|
||||||
|
"name": "order_items",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_id": {
|
||||||
|
"name": "order_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price_cents": {
|
||||||
|
"name": "price_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'placed'"
|
||||||
|
},
|
||||||
|
"cancelled_by": {
|
||||||
|
"name": "cancelled_by",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"pickup_box_id": {
|
||||||
|
"name": "pickup_box_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"picked_up_at": {
|
||||||
|
"name": "picked_up_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"order_items_order_id_orders_id_fk": {
|
||||||
|
"name": "order_items_order_id_orders_id_fk",
|
||||||
|
"tableFrom": "order_items",
|
||||||
|
"tableTo": "orders",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"order_items_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "order_items_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "order_items",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"order_items_pickup_box_id_pickup_boxes_id_fk": {
|
||||||
|
"name": "order_items_pickup_box_id_pickup_boxes_id_fk",
|
||||||
|
"tableFrom": "order_items",
|
||||||
|
"tableTo": "pickup_boxes",
|
||||||
|
"columnsFrom": [
|
||||||
|
"pickup_box_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.orders": {
|
||||||
|
"name": "orders",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"name": "customer_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_name": {
|
||||||
|
"name": "guest_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_contact": {
|
||||||
|
"name": "guest_contact",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_access_token": {
|
||||||
|
"name": "guest_access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'placed'"
|
||||||
|
},
|
||||||
|
"payment_charge_id": {
|
||||||
|
"name": "payment_charge_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.pickup_boxes": {
|
||||||
|
"name": "pickup_boxes",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"number": {
|
||||||
|
"name": "number",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"is_open": {
|
||||||
|
"name": "is_open",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"pickup_boxes_number_unique": {
|
||||||
|
"name": "pickup_boxes_number_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"number"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.preparation_step_instances": {
|
||||||
|
"name": "preparation_step_instances",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_item_id": {
|
||||||
|
"name": "order_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"name": "sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"estimated_duration_seconds": {
|
||||||
|
"name": "estimated_duration_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"delay_seconds": {
|
||||||
|
"name": "delay_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'queued'"
|
||||||
|
},
|
||||||
|
"machine_id": {
|
||||||
|
"name": "machine_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"started_at": {
|
||||||
|
"name": "started_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"done_at": {
|
||||||
|
"name": "done_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"preparation_step_instances_order_item_id_order_items_id_fk": {
|
||||||
|
"name": "preparation_step_instances_order_item_id_order_items_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "order_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"preparation_step_instances_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "preparation_step_instances_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"preparation_step_instances_machine_id_machines_id_fk": {
|
||||||
|
"name": "preparation_step_instances_machine_id_machines_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "machines",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,954 @@
|
|||||||
|
{
|
||||||
|
"id": "79d9d9d2-6e88-41ed-8d15-160c3549c881",
|
||||||
|
"prevId": "4826f90c-c779-4b2d-ab6e-491e72dbaee8",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.allergens": {
|
||||||
|
"name": "allergens",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"allergens_name_idx": {
|
||||||
|
"name": "allergens_name_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "name",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.brands": {
|
||||||
|
"name": "brands",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.contact_messages": {
|
||||||
|
"name": "contact_messages",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"name": "message",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.machine_tags": {
|
||||||
|
"name": "machine_tags",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"machine_tags_name_idx": {
|
||||||
|
"name": "machine_tags_name_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "name",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.machines": {
|
||||||
|
"name": "machines",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"machines_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "machines_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "machines",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_item_allergens": {
|
||||||
|
"name": "menu_item_allergens",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"allergen_id": {
|
||||||
|
"name": "allergen_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"menu_item_allergens_item_allergen_idx": {
|
||||||
|
"name": "menu_item_allergens_item_allergen_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "menu_item_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "allergen_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_item_allergens_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "menu_item_allergens_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "menu_item_allergens",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"menu_item_allergens_allergen_id_allergens_id_fk": {
|
||||||
|
"name": "menu_item_allergens_allergen_id_allergens_id_fk",
|
||||||
|
"tableFrom": "menu_item_allergens",
|
||||||
|
"tableTo": "allergens",
|
||||||
|
"columnsFrom": [
|
||||||
|
"allergen_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_item_options": {
|
||||||
|
"name": "menu_item_options",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price_delta_cents": {
|
||||||
|
"name": "price_delta_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_item_options_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "menu_item_options_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "menu_item_options",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_item_steps": {
|
||||||
|
"name": "menu_item_steps",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"name": "sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"estimated_duration_seconds": {
|
||||||
|
"name": "estimated_duration_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_item_steps_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "menu_item_steps_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "menu_item_steps",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"menu_item_steps_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "menu_item_steps_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "menu_item_steps",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.menu_items": {
|
||||||
|
"name": "menu_items",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"brand_id": {
|
||||||
|
"name": "brand_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"price_cents": {
|
||||||
|
"name": "price_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"menu_items_brand_id_brands_id_fk": {
|
||||||
|
"name": "menu_items_brand_id_brands_id_fk",
|
||||||
|
"tableFrom": "menu_items",
|
||||||
|
"tableTo": "brands",
|
||||||
|
"columnsFrom": [
|
||||||
|
"brand_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.order_item_options": {
|
||||||
|
"name": "order_item_options",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_item_id": {
|
||||||
|
"name": "order_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price_delta_cents": {
|
||||||
|
"name": "price_delta_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"order_item_options_order_item_id_order_items_id_fk": {
|
||||||
|
"name": "order_item_options_order_item_id_order_items_id_fk",
|
||||||
|
"tableFrom": "order_item_options",
|
||||||
|
"tableTo": "order_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.order_item_steps": {
|
||||||
|
"name": "order_item_steps",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_item_id": {
|
||||||
|
"name": "order_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"name": "sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"estimated_duration_seconds": {
|
||||||
|
"name": "estimated_duration_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"order_item_steps_order_item_id_order_items_id_fk": {
|
||||||
|
"name": "order_item_steps_order_item_id_order_items_id_fk",
|
||||||
|
"tableFrom": "order_item_steps",
|
||||||
|
"tableTo": "order_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"order_item_steps_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "order_item_steps_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "order_item_steps",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.order_items": {
|
||||||
|
"name": "order_items",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_id": {
|
||||||
|
"name": "order_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"menu_item_id": {
|
||||||
|
"name": "menu_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price_cents": {
|
||||||
|
"name": "price_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'placed'"
|
||||||
|
},
|
||||||
|
"cancelled_by": {
|
||||||
|
"name": "cancelled_by",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"pickup_box_id": {
|
||||||
|
"name": "pickup_box_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"placed_in_box_at": {
|
||||||
|
"name": "placed_in_box_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"picked_up_at": {
|
||||||
|
"name": "picked_up_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"order_items_order_id_orders_id_fk": {
|
||||||
|
"name": "order_items_order_id_orders_id_fk",
|
||||||
|
"tableFrom": "order_items",
|
||||||
|
"tableTo": "orders",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"order_items_menu_item_id_menu_items_id_fk": {
|
||||||
|
"name": "order_items_menu_item_id_menu_items_id_fk",
|
||||||
|
"tableFrom": "order_items",
|
||||||
|
"tableTo": "menu_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"menu_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"order_items_pickup_box_id_pickup_boxes_id_fk": {
|
||||||
|
"name": "order_items_pickup_box_id_pickup_boxes_id_fk",
|
||||||
|
"tableFrom": "order_items",
|
||||||
|
"tableTo": "pickup_boxes",
|
||||||
|
"columnsFrom": [
|
||||||
|
"pickup_box_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.orders": {
|
||||||
|
"name": "orders",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"name": "customer_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_name": {
|
||||||
|
"name": "guest_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_contact": {
|
||||||
|
"name": "guest_contact",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"guest_access_token": {
|
||||||
|
"name": "guest_access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'placed'"
|
||||||
|
},
|
||||||
|
"payment_charge_id": {
|
||||||
|
"name": "payment_charge_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.pickup_boxes": {
|
||||||
|
"name": "pickup_boxes",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"number": {
|
||||||
|
"name": "number",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"is_open": {
|
||||||
|
"name": "is_open",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"pickup_boxes_number_unique": {
|
||||||
|
"name": "pickup_boxes_number_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"number"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.preparation_step_instances": {
|
||||||
|
"name": "preparation_step_instances",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"order_item_id": {
|
||||||
|
"name": "order_item_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"name": "sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"machine_tag_id": {
|
||||||
|
"name": "machine_tag_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"estimated_duration_seconds": {
|
||||||
|
"name": "estimated_duration_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"delay_seconds": {
|
||||||
|
"name": "delay_seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'queued'"
|
||||||
|
},
|
||||||
|
"machine_id": {
|
||||||
|
"name": "machine_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"started_at": {
|
||||||
|
"name": "started_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"done_at": {
|
||||||
|
"name": "done_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"preparation_step_instances_order_item_id_order_items_id_fk": {
|
||||||
|
"name": "preparation_step_instances_order_item_id_order_items_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "order_items",
|
||||||
|
"columnsFrom": [
|
||||||
|
"order_item_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"preparation_step_instances_machine_tag_id_machine_tags_id_fk": {
|
||||||
|
"name": "preparation_step_instances_machine_tag_id_machine_tags_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "machine_tags",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_tag_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"preparation_step_instances_machine_id_machines_id_fk": {
|
||||||
|
"name": "preparation_step_instances_machine_id_machines_id_fk",
|
||||||
|
"tableFrom": "preparation_step_instances",
|
||||||
|
"tableTo": "machines",
|
||||||
|
"columnsFrom": [
|
||||||
|
"machine_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1788971014996,
|
||||||
|
"tag": "0000_tense_lady_vermin",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1788972318759,
|
||||||
|
"tag": "0001_graceful_frank_castle",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1788976393301,
|
||||||
|
"tag": "0002_large_lake",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 3,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1789028563859,
|
||||||
|
"tag": "0003_soft_psynapse",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 4,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1789036823859,
|
||||||
|
"tag": "0004_misty_hedge_knight",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 5,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1789636006749,
|
||||||
|
"tag": "0005_tan_power_man",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 6,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1789637974330,
|
||||||
|
"tag": "0006_worried_shooting_star",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 7,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1790804884144,
|
||||||
|
"tag": "0007_dazzling_norman_osborn",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 8,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1790845995013,
|
||||||
|
"tag": "0008_absurd_the_anarchist",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 9,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1790888552762,
|
||||||
|
"tag": "0009_damp_ender_wiggin",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 10,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1790889640651,
|
||||||
|
"tag": "0010_lush_black_bolt",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
import { tanstackConfig } from '@tanstack/eslint-config'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...tanstackConfig,
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'import/no-cycle': 'off',
|
||||||
|
'import/order': 'off',
|
||||||
|
'sort-imports': 'off',
|
||||||
|
'@typescript-eslint/array-type': 'off',
|
||||||
|
'@typescript-eslint/require-await': 'off',
|
||||||
|
'pnpm/json-enforce-catalog': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ignores: ['eslint.config.js', 'prettier.config.js'],
|
||||||
|
},
|
||||||
|
]
|
||||||
Generated
+11682
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
"name": "ghost-kitchen",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"imports": {
|
||||||
|
"#/*": "./src/*"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev --port 3000 --host",
|
||||||
|
"generate-routes": "tsr generate",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"db:generate": "drizzle-kit generate",
|
||||||
|
"db:migrate": "drizzle-kit migrate",
|
||||||
|
"db:push": "drizzle-kit push",
|
||||||
|
"db:pull": "drizzle-kit pull",
|
||||||
|
"db:studio": "drizzle-kit studio",
|
||||||
|
"db:seed": "tsx src/db/seed.ts",
|
||||||
|
"db:seed-admin": "tsx src/db/seed-admin.ts",
|
||||||
|
"auth:migrate": "better-auth migrate",
|
||||||
|
"lint": "eslint",
|
||||||
|
"format": "prettier --write . && eslint --fix",
|
||||||
|
"check": "prettier --check .",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/react-devtools": "latest",
|
||||||
|
"@tanstack/react-router": "latest",
|
||||||
|
"@tanstack/react-router-devtools": "latest",
|
||||||
|
"@tanstack/react-start": "latest",
|
||||||
|
"better-auth": "^1.5.3",
|
||||||
|
"drizzle-kit": "^0.31.9",
|
||||||
|
"drizzle-orm": "^0.45.1",
|
||||||
|
"framer-motion": "^13.2.0",
|
||||||
|
"pg": "^8.16.3",
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-dom": "^19.2.0",
|
||||||
|
"stripe": "^22.6.1",
|
||||||
|
"zod": "^4.5.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@better-auth/cli": "^1.4.21",
|
||||||
|
"@tanstack/devtools-vite": "latest",
|
||||||
|
"@tanstack/eslint-config": "latest",
|
||||||
|
"@tanstack/router-cli": "^1.132.0",
|
||||||
|
"@testcontainers/postgresql": "^12.1.0",
|
||||||
|
"@types/node": "^22.10.2",
|
||||||
|
"@types/pg": "^8.15.6",
|
||||||
|
"@types/react": "^19.2.0",
|
||||||
|
"@types/react-dom": "^19.2.0",
|
||||||
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
|
"dotenv": "^17.3.1",
|
||||||
|
"eslint": "^9.20.0",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
|
"sass": "^1.104.0",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
|
"typescript": "^6.0.2",
|
||||||
|
"vite": "^8.0.0",
|
||||||
|
"vitest": "^4.0.0"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"esbuild",
|
||||||
|
"lightningcss"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
/** @type {import('prettier').Config} */
|
||||||
|
const config = {
|
||||||
|
semi: false,
|
||||||
|
singleQuote: true,
|
||||||
|
trailingComma: "all",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160" fill="none" viewBox="0 0 160 160"><rect width="9.631" height="40.852" fill="#C5F74F" rx="4.816" transform="matrix(.87303 .48767 -.49721 .86763 43.48 67.304)"/><rect width="9.631" height="40.852" fill="#C5F74F" rx="4.816" transform="matrix(.87303 .48767 -.49721 .86763 76.94 46.534)"/><rect width="9.631" height="40.852" fill="#C5F74F" rx="4.816" transform="matrix(.87303 .48767 -.49721 .86763 128.424 46.535)"/><rect width="9.631" height="40.852" fill="#C5F74F" rx="4.816" transform="matrix(.87303 .48767 -.49721 .86763 94.957 67.304)"/></svg>
|
||||||
|
After Width: | Height: | Size: 608 B |
Binary file not shown.
|
After Width: | Height: | Size: 2.3 MiB |
Executable
+227
@@ -0,0 +1,227 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# A wizard walks a human through a manual procedure, step by step.
|
||||||
|
# Generated by the /wizard skill.
|
||||||
|
#
|
||||||
|
# Everything above the "STAGES" marker is the wizard library: do not hand-edit
|
||||||
|
# it. Author the per-step stages below the marker.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────
|
||||||
|
# Wizard library: delightful, consistent UX, identical across every wizard.
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if [[ -t 1 ]] && command -v tput >/dev/null 2>&1 && [[ "$(tput colors 2>/dev/null || echo 0)" -ge 8 ]]; then
|
||||||
|
BOLD=$(tput bold); DIM=$(tput dim); RESET=$(tput sgr0)
|
||||||
|
BLUE=$(tput setaf 4); GREEN=$(tput setaf 2); YELLOW=$(tput setaf 3); RED=$(tput setaf 1)
|
||||||
|
else
|
||||||
|
BOLD=""; DIM=""; RESET=""; BLUE=""; GREEN=""; YELLOW=""; RED=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Author sets this at the top of the stages section.
|
||||||
|
TOTAL_STAGES=0
|
||||||
|
|
||||||
|
_STAGE_INDEX=0
|
||||||
|
ENV_FILE="${ENV_FILE:-.env}"
|
||||||
|
WRITTEN_ENV=() # KEYs written to ENV_FILE this run
|
||||||
|
WRITTEN_SECRET=() # secret NAMEs set this run
|
||||||
|
SKIPPED=() # things we couldn't do (e.g. gh missing)
|
||||||
|
|
||||||
|
# _clear wipes the terminal so only the current step is on screen. No-op when
|
||||||
|
# output isn't a terminal, so piped logs stay readable.
|
||||||
|
_clear() {
|
||||||
|
[[ -t 1 ]] || return 0
|
||||||
|
if command -v tput >/dev/null 2>&1; then tput clear; else printf '\033[2J\033[3J\033[H'; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# banner "Title" shows the opening frame: what this wizard does.
|
||||||
|
banner() {
|
||||||
|
_clear
|
||||||
|
printf '\n%s%s %s%s\n' "$BOLD" "$BLUE" "$1" "$RESET"
|
||||||
|
printf '%s %s stages%s\n\n' "$DIM" "$TOTAL_STAGES" "$RESET"
|
||||||
|
printf '%s You drive the browser; this wizard tells you exactly what to do and\n' "$DIM"
|
||||||
|
printf ' captures the values you copy back. Stop any time with Ctrl-C and re-run\n'
|
||||||
|
printf ' later, since it remembers values already saved.%s\n' "$RESET"
|
||||||
|
pause "Ready to start?"
|
||||||
|
}
|
||||||
|
|
||||||
|
# stage "Name" clears the screen, then announces a stage and shows progress.
|
||||||
|
# Clearing keeps only the current step on screen.
|
||||||
|
stage() {
|
||||||
|
_clear
|
||||||
|
_STAGE_INDEX=$((_STAGE_INDEX + 1))
|
||||||
|
printf '\n%s%s▸ Stage %s/%s · %s%s\n' \
|
||||||
|
"$BOLD" "$BLUE" "$_STAGE_INDEX" "$TOTAL_STAGES" "$1" "$RESET"
|
||||||
|
}
|
||||||
|
|
||||||
|
# say "..." prints a plain instruction line.
|
||||||
|
say() { printf ' %s\n' "$1"; }
|
||||||
|
# step "..." is a numbered-feeling action the human takes in the browser.
|
||||||
|
step() { printf ' %s•%s %s\n' "$BLUE" "$RESET" "$1"; }
|
||||||
|
note() { printf ' %s%s%s\n' "$DIM" "$1" "$RESET"; }
|
||||||
|
warn() { printf ' %s⚠ %s%s\n' "$YELLOW" "$1" "$RESET"; }
|
||||||
|
|
||||||
|
# open_url URL opens it in the human's browser, cross-platform incl. WSL.
|
||||||
|
open_url() {
|
||||||
|
local url="$1"
|
||||||
|
printf ' %s↗ opening%s %s\n' "$GREEN" "$RESET" "$url"
|
||||||
|
{ if command -v wslview >/dev/null 2>&1; then wslview "$url"
|
||||||
|
elif command -v explorer.exe >/dev/null 2>&1; then explorer.exe "$url"
|
||||||
|
elif command -v xdg-open >/dev/null 2>&1; then xdg-open "$url"
|
||||||
|
elif command -v open >/dev/null 2>&1; then open "$url"
|
||||||
|
else warn "couldn't open a browser; visit it manually: $url"; fi
|
||||||
|
} >/dev/null 2>&1 || warn "couldn't open a browser, so visit it manually: $url"
|
||||||
|
}
|
||||||
|
|
||||||
|
# pause "msg" waits for the human to confirm they've done the manual part.
|
||||||
|
pause() {
|
||||||
|
printf ' %s%s%s ' "$DIM" "${1:-Press Enter to continue}" "$RESET"
|
||||||
|
read -r _ || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# confirm "question" is a y/N gate; returns success on yes.
|
||||||
|
confirm() {
|
||||||
|
local reply=""
|
||||||
|
printf ' %s? %s [y/N] ' "$YELLOW" "$1"
|
||||||
|
read -r reply || true
|
||||||
|
[[ "$reply" =~ ^[Yy] ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# _existing KEY: current value of KEY in ENV_FILE, if any.
|
||||||
|
_existing() {
|
||||||
|
[[ -f "$ENV_FILE" ]] || return 1
|
||||||
|
local line; line=$(grep -E "^${1}=" "$ENV_FILE" | tail -n1) || return 1
|
||||||
|
printf '%s' "${line#*=}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ask KEY "Prompt" reads a value into $KEY. Offers the existing .env value as
|
||||||
|
# a default on re-runs (Enter keeps it). Visible input (non-secret).
|
||||||
|
ask() {
|
||||||
|
local key="$1" prompt="$2" current input
|
||||||
|
current=$(_existing "$key" || true)
|
||||||
|
if [[ -n "$current" ]]; then
|
||||||
|
printf ' %s%s%s %s[Enter keeps current]%s ' "$BOLD" "$prompt" "$RESET" "$DIM" "$RESET"
|
||||||
|
else
|
||||||
|
printf ' %s%s%s ' "$BOLD" "$prompt" "$RESET"
|
||||||
|
fi
|
||||||
|
read -r input || true
|
||||||
|
[[ -z "$input" && -n "$current" ]] && input="$current"
|
||||||
|
printf -v "$key" '%s' "$input"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ask_secret KEY "Prompt" is like ask, but input is hidden.
|
||||||
|
ask_secret() {
|
||||||
|
local key="$1" prompt="$2" current input
|
||||||
|
current=$(_existing "$key" || true)
|
||||||
|
if [[ -n "$current" ]]; then
|
||||||
|
printf ' %s%s%s %s[Enter keeps current]%s ' "$BOLD" "$prompt" "$RESET" "$DIM" "$RESET"
|
||||||
|
else
|
||||||
|
printf ' %s%s%s ' "$BOLD" "$prompt" "$RESET"
|
||||||
|
fi
|
||||||
|
read -rs input || true
|
||||||
|
printf '\n'
|
||||||
|
[[ -z "$input" && -n "$current" ]] && input="$current"
|
||||||
|
printf -v "$key" '%s' "$input"
|
||||||
|
}
|
||||||
|
|
||||||
|
# write_env KEY VALUE upserts KEY=VALUE into ENV_FILE (creates it; replaces
|
||||||
|
# any existing line). Idempotent.
|
||||||
|
write_env() {
|
||||||
|
local key="$1" value="$2" tmp
|
||||||
|
touch "$ENV_FILE"
|
||||||
|
tmp=$(mktemp)
|
||||||
|
grep -vE "^${key}=" "$ENV_FILE" > "$tmp" || true
|
||||||
|
printf '%s=%s\n' "$key" "$value" >> "$tmp"
|
||||||
|
mv "$tmp" "$ENV_FILE"
|
||||||
|
WRITTEN_ENV+=("$key")
|
||||||
|
printf ' %s✓ wrote%s %s → %s\n' "$GREEN" "$RESET" "$key" "$ENV_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# set_secret NAME VALUE sets a GitHub Actions repo secret via gh. Falls back
|
||||||
|
# to a warning (and records it) if gh is unavailable or unauthenticated.
|
||||||
|
set_secret() {
|
||||||
|
local name="$1" value="$2"
|
||||||
|
if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
|
||||||
|
if printf '%s' "$value" | gh secret set "$name" >/dev/null 2>&1; then
|
||||||
|
WRITTEN_SECRET+=("$name")
|
||||||
|
printf ' %s✓ set%s GitHub secret %s\n' "$GREEN" "$RESET" "$name"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
SKIPPED+=("GitHub secret $name (set it manually: gh secret set $name)")
|
||||||
|
warn "skipped GitHub secret $name: gh not ready; set it later"
|
||||||
|
}
|
||||||
|
|
||||||
|
# set_var NAME VALUE sets a GitHub Actions repo variable (non-secret).
|
||||||
|
set_var() {
|
||||||
|
local name="$1" value="$2"
|
||||||
|
if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
|
||||||
|
if gh variable set "$name" --body "$value" >/dev/null 2>&1; then
|
||||||
|
printf ' %s✓ set%s GitHub variable %s\n' "$GREEN" "$RESET" "$name"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
SKIPPED+=("GitHub variable $name")
|
||||||
|
warn "skipped GitHub variable $name, gh not ready; set it later"
|
||||||
|
}
|
||||||
|
|
||||||
|
# finish clears, then shows a closing summary of everything configured.
|
||||||
|
finish() {
|
||||||
|
_clear
|
||||||
|
printf '\n%s%s ✓ Setup complete%s\n' "$BOLD" "$GREEN" "$RESET"
|
||||||
|
(( ${#WRITTEN_ENV[@]} )) && note "wrote ${#WRITTEN_ENV[@]} value(s) to $ENV_FILE: ${WRITTEN_ENV[*]}"
|
||||||
|
(( ${#WRITTEN_SECRET[@]} )) && note "set ${#WRITTEN_SECRET[@]} GitHub secret(s): ${WRITTEN_SECRET[*]}"
|
||||||
|
if (( ${#SKIPPED[@]} )); then
|
||||||
|
printf '\n'; warn "still to do by hand:"
|
||||||
|
for s in "${SKIPPED[@]}"; do note " - $s"; done
|
||||||
|
fi
|
||||||
|
printf '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────
|
||||||
|
# STAGES: author this section. One stage() per step the human takes.
|
||||||
|
# Replace the example below. Set TOTAL_STAGES to match the stages you write.
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
TOTAL_STAGES=2
|
||||||
|
|
||||||
|
banner "Google Sign-In setup"
|
||||||
|
|
||||||
|
BETTER_AUTH_URL_VALUE=$(_existing BETTER_AUTH_URL || true)
|
||||||
|
BETTER_AUTH_URL_VALUE="${BETTER_AUTH_URL_VALUE:-http://localhost:3000}"
|
||||||
|
REDIRECT_URI="${BETTER_AUTH_URL_VALUE}/api/auth/callback/google"
|
||||||
|
|
||||||
|
# ── Stage 1: OAuth consent screen ──────────────────────────────────────────
|
||||||
|
stage "Google Cloud: OAuth consent screen"
|
||||||
|
say "Google won't let you create an OAuth client until a consent screen exists"
|
||||||
|
say "for the project. Skip straight through if yours already has one."
|
||||||
|
open_url "https://console.cloud.google.com/apis/credentials/consent"
|
||||||
|
step "Select or create a Google Cloud project for this app."
|
||||||
|
step "User type: 'External' (or 'Internal' if you're on a Workspace org and"
|
||||||
|
step " only your org's users will ever sign in)."
|
||||||
|
step "Fill in App name, User support email, and Developer contact email,"
|
||||||
|
step " then click through Save on the remaining screens (scopes, test users)."
|
||||||
|
pause "Done configuring the consent screen?"
|
||||||
|
|
||||||
|
# ── Stage 2: OAuth client ID ────────────────────────────────────────────────
|
||||||
|
stage "Google Cloud: create OAuth client ID"
|
||||||
|
open_url "https://console.cloud.google.com/apis/credentials"
|
||||||
|
step "Click '+ Create Credentials' → 'OAuth client ID'."
|
||||||
|
step "Application type: 'Web application'."
|
||||||
|
say "Authorized redirect URI to add:"
|
||||||
|
note " $REDIRECT_URI"
|
||||||
|
warn "If BETTER_AUTH_URL changes later (e.g. you deploy this), add that"
|
||||||
|
warn "domain's /api/auth/callback/google URI here too."
|
||||||
|
step "Paste the redirect URI above into 'Authorized redirect URIs', then click 'Create'."
|
||||||
|
step "A dialog shows the Client ID and Client secret — copy both."
|
||||||
|
ask GOOGLE_CLIENT_ID "Paste the Client ID:"
|
||||||
|
ask_secret GOOGLE_CLIENT_SECRET "Paste the Client secret:"
|
||||||
|
write_env GOOGLE_CLIENT_ID "$GOOGLE_CLIENT_ID"
|
||||||
|
write_env GOOGLE_CLIENT_SECRET "$GOOGLE_CLIENT_SECRET"
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
finish
|
||||||
|
note "Restart 'npm run dev' so the server picks up the new env vars."
|
||||||
|
note "Apple SSO needs a real HTTPS domain (no localhost) — re-run /wizard for"
|
||||||
|
note "that once you have one to test against."
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
@use '../../styles/variables' as *;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: $space-3;
|
||||||
|
margin-bottom: $space-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addLink {
|
||||||
|
padding: $space-2 $space-4;
|
||||||
|
border-radius: $radius;
|
||||||
|
background: $color-primary;
|
||||||
|
color: $color-primary-contrast;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
color: $color-text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $space-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: $space-3;
|
||||||
|
padding: $space-3;
|
||||||
|
border: 1px solid $color-border;
|
||||||
|
border-radius: $radius;
|
||||||
|
background: $color-surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemInfo {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: $space-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemName {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemPrice {
|
||||||
|
color: $color-text-muted;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemMeta {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: $color-text-muted;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemActions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $space-2;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editLink {
|
||||||
|
padding: $space-1 $space-3;
|
||||||
|
border: 1px solid $color-border;
|
||||||
|
border-radius: $radius;
|
||||||
|
color: $color-text;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $color-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteButton {
|
||||||
|
padding: $space-1 $space-3;
|
||||||
|
border: 1px solid $color-border;
|
||||||
|
border-radius: $radius;
|
||||||
|
background: $color-background;
|
||||||
|
color: $color-danger;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $color-danger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmButton {
|
||||||
|
padding: $space-1 $space-3;
|
||||||
|
border: none;
|
||||||
|
border-radius: $radius;
|
||||||
|
background: $color-danger;
|
||||||
|
color: $color-primary-contrast;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: $color-danger;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin: $space-2 0 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { Link } from '@tanstack/react-router'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
import type { AdminMenuItem } from '#/domain/menu.ts'
|
||||||
|
import { formatPrice } from '#/lib/format.ts'
|
||||||
|
|
||||||
|
import styles from './AdminMenuListView.module.scss'
|
||||||
|
|
||||||
|
type AdminMenuListViewProps = {
|
||||||
|
items: Array<AdminMenuItem>
|
||||||
|
onDelete: (menuItemId: number) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AdminMenuListView = ({
|
||||||
|
items,
|
||||||
|
onDelete,
|
||||||
|
}: AdminMenuListViewProps) => {
|
||||||
|
const [confirmingId, setConfirmingId] = useState<number | null>(null)
|
||||||
|
const [deletingId, setDeletingId] = useState<number | null>(null)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const confirmDelete = async (menuItemId: number) => {
|
||||||
|
setDeletingId(menuItemId)
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
await onDelete(menuItemId)
|
||||||
|
} catch (deleteError) {
|
||||||
|
setError(
|
||||||
|
deleteError instanceof Error
|
||||||
|
? deleteError.message
|
||||||
|
: 'Something went wrong.',
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
setDeletingId(null)
|
||||||
|
setConfirmingId(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<h1 className={styles.heading}>Menu</h1>
|
||||||
|
<Link
|
||||||
|
to="/admin/menu/new"
|
||||||
|
className={styles.addLink}
|
||||||
|
viewTransition={false}
|
||||||
|
>
|
||||||
|
Add item
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{items.length === 0 ? (
|
||||||
|
<p className={styles.empty}>No menu items yet.</p>
|
||||||
|
) : (
|
||||||
|
<ul className={styles.items}>
|
||||||
|
{items.map((item) => (
|
||||||
|
<li key={item.id} className={styles.item}>
|
||||||
|
<div className={styles.itemInfo}>
|
||||||
|
<div className={styles.itemHeader}>
|
||||||
|
<span className={styles.itemName}>{item.name}</span>
|
||||||
|
<span className={styles.itemPrice}>
|
||||||
|
{formatPrice(item.priceCents)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className={styles.itemMeta}>
|
||||||
|
{item.steps.length} step
|
||||||
|
{item.steps.length === 1 ? '' : 's'} ·{' '}
|
||||||
|
{item.options.length} option
|
||||||
|
{item.options.length === 1 ? '' : 's'}
|
||||||
|
{item.allergens.length > 0
|
||||||
|
? ` · Contains: ${item.allergens.map((allergen) => allergen.name).join(', ')}`
|
||||||
|
: ''}
|
||||||
|
{item.description ? ` · ${item.description}` : ''}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.itemActions}>
|
||||||
|
<Link
|
||||||
|
to="/admin/menu/$menuItemId"
|
||||||
|
params={{ menuItemId: String(item.id) }}
|
||||||
|
className={styles.editLink}
|
||||||
|
viewTransition={false}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Link>
|
||||||
|
{confirmingId === item.id ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.confirmButton}
|
||||||
|
disabled={deletingId === item.id}
|
||||||
|
onClick={() => void confirmDelete(item.id)}
|
||||||
|
>
|
||||||
|
{deletingId === item.id ? 'Deleting…' : 'Confirm?'}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.deleteButton}
|
||||||
|
onClick={() => setConfirmingId(item.id)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
{error && <p className={styles.error}>{error}</p>}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
@use '../../styles/variables' as *;
|
||||||
|
|
||||||
|
.form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $space-4;
|
||||||
|
max-width: 640px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $space-1;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: $color-text-muted;
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
padding: $space-2;
|
||||||
|
border: 1px solid $color-border;
|
||||||
|
border-radius: $radius;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: inherit;
|
||||||
|
color: $color-text;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 4.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
gap: $space-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $space-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repeatingRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: $space-2;
|
||||||
|
|
||||||
|
.field {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-tablet) {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: $space-1;
|
||||||
|
padding-bottom: $space-2;
|
||||||
|
border-bottom: 1px solid $color-border;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stepName {
|
||||||
|
flex: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrowField {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.removeButton {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 2.25rem;
|
||||||
|
height: 2.25rem;
|
||||||
|
margin-top: 1.35rem;
|
||||||
|
border: 1px solid $color-border;
|
||||||
|
border-radius: $radius;
|
||||||
|
background: $color-background;
|
||||||
|
color: $color-danger;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $color-danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-tablet) {
|
||||||
|
margin-top: 0;
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.addButton {
|
||||||
|
align-self: flex-start;
|
||||||
|
padding: $space-1 $space-3;
|
||||||
|
border: 1px dashed $color-border;
|
||||||
|
border-radius: $radius;
|
||||||
|
background: $color-background;
|
||||||
|
color: $color-text;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $color-primary;
|
||||||
|
color: $color-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.allergenList {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: $space-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.allergenOption {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $space-1;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: $color-text;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $space-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submitButton {
|
||||||
|
padding: $space-2 $space-4;
|
||||||
|
border: none;
|
||||||
|
border-radius: $radius;
|
||||||
|
background: $color-primary;
|
||||||
|
color: $color-primary-contrast;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancelLink {
|
||||||
|
color: $color-text-muted;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $color-text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: $color-danger;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
import { Link } from '@tanstack/react-router'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import type { FormEvent } from 'react'
|
||||||
|
|
||||||
|
import type {
|
||||||
|
AdminMenuItem,
|
||||||
|
AllergenOption,
|
||||||
|
MenuItemInput,
|
||||||
|
} from '#/domain/menu.ts'
|
||||||
|
import type { MachineTagOption } from '#/domain/kitchen.ts'
|
||||||
|
|
||||||
|
import styles from './MenuItemForm.module.scss'
|
||||||
|
|
||||||
|
type OptionRow = { name: string; priceDelta: string }
|
||||||
|
type StepRow = { name: string; machineTagId: string; durationSeconds: string }
|
||||||
|
|
||||||
|
const toOptionRows = (item?: AdminMenuItem): Array<OptionRow> =>
|
||||||
|
item?.options.map((option) => ({
|
||||||
|
name: option.name,
|
||||||
|
priceDelta: (option.priceDeltaCents / 100).toString(),
|
||||||
|
})) ?? []
|
||||||
|
|
||||||
|
const toStepRows = (item?: AdminMenuItem): Array<StepRow> =>
|
||||||
|
item?.steps.map((step) => ({
|
||||||
|
name: step.name,
|
||||||
|
machineTagId: String(step.machineTagId),
|
||||||
|
durationSeconds: String(step.estimatedDurationSeconds),
|
||||||
|
})) ?? []
|
||||||
|
|
||||||
|
type MenuItemFormProps = {
|
||||||
|
machineTags: Array<MachineTagOption>
|
||||||
|
allergenOptions: Array<AllergenOption>
|
||||||
|
initialItem?: AdminMenuItem
|
||||||
|
submitLabel: string
|
||||||
|
onSubmit: (input: MenuItemInput) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MenuItemForm = ({
|
||||||
|
machineTags,
|
||||||
|
allergenOptions,
|
||||||
|
initialItem,
|
||||||
|
submitLabel,
|
||||||
|
onSubmit,
|
||||||
|
}: MenuItemFormProps) => {
|
||||||
|
const [name, setName] = useState(initialItem?.name ?? '')
|
||||||
|
const [description, setDescription] = useState(
|
||||||
|
initialItem?.description ?? '',
|
||||||
|
)
|
||||||
|
const [price, setPrice] = useState(
|
||||||
|
initialItem ? (initialItem.priceCents / 100).toString() : '',
|
||||||
|
)
|
||||||
|
const [options, setOptions] = useState<Array<OptionRow>>(
|
||||||
|
toOptionRows(initialItem),
|
||||||
|
)
|
||||||
|
const [steps, setSteps] = useState<Array<StepRow>>(toStepRows(initialItem))
|
||||||
|
const [allergenIds, setAllergenIds] = useState<Array<number>>(
|
||||||
|
initialItem?.allergens.map((allergen) => allergen.id) ?? [],
|
||||||
|
)
|
||||||
|
const [submitting, setSubmitting] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const updateOption = (index: number, patch: Partial<OptionRow>) =>
|
||||||
|
setOptions((rows) =>
|
||||||
|
rows.map((row, i) => (i === index ? { ...row, ...patch } : row)),
|
||||||
|
)
|
||||||
|
const updateStep = (index: number, patch: Partial<StepRow>) =>
|
||||||
|
setSteps((rows) =>
|
||||||
|
rows.map((row, i) => (i === index ? { ...row, ...patch } : row)),
|
||||||
|
)
|
||||||
|
const toggleAllergen = (allergenId: number) =>
|
||||||
|
setAllergenIds((ids) =>
|
||||||
|
ids.includes(allergenId)
|
||||||
|
? ids.filter((id) => id !== allergenId)
|
||||||
|
: [...ids, allergenId],
|
||||||
|
)
|
||||||
|
|
||||||
|
const submit = async (event: FormEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
if (steps.length === 0) {
|
||||||
|
setError('Add at least one Preparation Step — otherwise this item can never be cooked.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const input: MenuItemInput = {
|
||||||
|
name,
|
||||||
|
description: description || undefined,
|
||||||
|
priceCents: Math.round(Number(price) * 100),
|
||||||
|
options: options
|
||||||
|
.filter((option) => option.name.trim() !== '')
|
||||||
|
.map((option) => ({
|
||||||
|
name: option.name,
|
||||||
|
priceDeltaCents: Math.round(Number(option.priceDelta || '0') * 100),
|
||||||
|
})),
|
||||||
|
steps: steps.map((step) => ({
|
||||||
|
name: step.name,
|
||||||
|
machineTagId: Number(step.machineTagId),
|
||||||
|
estimatedDurationSeconds: Math.round(Number(step.durationSeconds)),
|
||||||
|
})),
|
||||||
|
allergenIds,
|
||||||
|
}
|
||||||
|
|
||||||
|
setSubmitting(true)
|
||||||
|
try {
|
||||||
|
await onSubmit(input)
|
||||||
|
} catch (submitError) {
|
||||||
|
setError(
|
||||||
|
submitError instanceof Error
|
||||||
|
? submitError.message
|
||||||
|
: 'Something went wrong.',
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className={styles.form} onSubmit={submit}>
|
||||||
|
<label className={styles.field}>
|
||||||
|
Name
|
||||||
|
<input
|
||||||
|
value={name}
|
||||||
|
onChange={(event) => setName(event.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className={styles.field}>
|
||||||
|
Description
|
||||||
|
<textarea
|
||||||
|
value={description}
|
||||||
|
onChange={(event) => setDescription(event.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className={styles.field}>
|
||||||
|
Price
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
min="0"
|
||||||
|
value={price}
|
||||||
|
onChange={(event) => setPrice(event.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<p className={styles.sectionTitle}>Options</p>
|
||||||
|
</div>
|
||||||
|
{options.map((option, index) => (
|
||||||
|
<div key={index} className={styles.repeatingRow}>
|
||||||
|
<label className={styles.field}>
|
||||||
|
Name
|
||||||
|
<input
|
||||||
|
value={option.name}
|
||||||
|
onChange={(event) =>
|
||||||
|
updateOption(index, { name: event.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className={`${styles.field} ${styles.narrowField}`}>
|
||||||
|
Price delta
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
value={option.priceDelta}
|
||||||
|
onChange={(event) =>
|
||||||
|
updateOption(index, { priceDelta: event.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.removeButton}
|
||||||
|
aria-label={`Remove option ${index + 1}`}
|
||||||
|
onClick={() =>
|
||||||
|
setOptions((rows) => rows.filter((_, i) => i !== index))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.addButton}
|
||||||
|
onClick={() =>
|
||||||
|
setOptions((rows) => [...rows, { name: '', priceDelta: '0' }])
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Add option
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<p className={styles.sectionTitle}>Preparation steps</p>
|
||||||
|
</div>
|
||||||
|
{steps.map((step, index) => (
|
||||||
|
<div key={index} className={styles.repeatingRow}>
|
||||||
|
<label className={`${styles.field} ${styles.stepName}`}>
|
||||||
|
Step name
|
||||||
|
<input
|
||||||
|
value={step.name}
|
||||||
|
onChange={(event) =>
|
||||||
|
updateStep(index, { name: event.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className={`${styles.field} ${styles.narrowField}`}>
|
||||||
|
Machine
|
||||||
|
<select
|
||||||
|
value={step.machineTagId}
|
||||||
|
onChange={(event) =>
|
||||||
|
updateStep(index, { machineTagId: event.target.value })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="" disabled>
|
||||||
|
Select…
|
||||||
|
</option>
|
||||||
|
{machineTags.map((tag) => (
|
||||||
|
<option key={tag.id} value={tag.id}>
|
||||||
|
{tag.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label className={`${styles.field} ${styles.narrowField}`}>
|
||||||
|
Duration (s)
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={step.durationSeconds}
|
||||||
|
onChange={(event) =>
|
||||||
|
updateStep(index, { durationSeconds: event.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.removeButton}
|
||||||
|
aria-label={`Remove step ${index + 1}`}
|
||||||
|
onClick={() =>
|
||||||
|
setSteps((rows) => rows.filter((_, i) => i !== index))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.addButton}
|
||||||
|
onClick={() =>
|
||||||
|
setSteps((rows) => [
|
||||||
|
...rows,
|
||||||
|
{ name: '', machineTagId: '', durationSeconds: '60' },
|
||||||
|
])
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Add step
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<p className={styles.sectionTitle}>Allergens</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.allergenList}>
|
||||||
|
{allergenOptions.map((allergen) => (
|
||||||
|
<label key={allergen.id} className={styles.allergenOption}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={allergenIds.includes(allergen.id)}
|
||||||
|
onChange={() => toggleAllergen(allergen.id)}
|
||||||
|
/>
|
||||||
|
{allergen.name}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <p className={styles.error}>{error}</p>}
|
||||||
|
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<button type="submit" className={styles.submitButton} disabled={submitting}>
|
||||||
|
{submitting ? 'Saving…' : submitLabel}
|
||||||
|
</button>
|
||||||
|
<Link
|
||||||
|
to="/admin/menu"
|
||||||
|
className={styles.cancelLink}
|
||||||
|
viewTransition={false}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
@use '../../styles/variables' as *;
|
||||||
|
|
||||||
|
.page {
|
||||||
|
max-width: 420px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: $space-2 $space-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $space-3;
|
||||||
|
padding: $space-4;
|
||||||
|
border: 1px solid $color-border;
|
||||||
|
border-radius: $radius;
|
||||||
|
background: $color-surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: $color-text-muted;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $color-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import type { ReactNode } from 'react'
|
||||||
|
|
||||||
|
import styles from './AuthCard.module.scss'
|
||||||
|
|
||||||
|
type AuthCardProps = {
|
||||||
|
title: string
|
||||||
|
children: ReactNode
|
||||||
|
footer: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AuthCard = ({ title, children, footer }: AuthCardProps) => (
|
||||||
|
<main className={styles.page}>
|
||||||
|
<div className={styles.card}>
|
||||||
|
<h1 className={styles.heading}>{title}</h1>
|
||||||
|
{children}
|
||||||
|
<p className={styles.footer}>{footer}</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
@use '../../styles/variables' as *;
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $space-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.google,
|
||||||
|
.apple {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: $space-2;
|
||||||
|
padding: $space-2 $space-3;
|
||||||
|
border-radius: $radius;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.google {
|
||||||
|
background: $color-surface;
|
||||||
|
color: $color-text;
|
||||||
|
border: 1px solid $color-border;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $color-border;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.apple {
|
||||||
|
background: #000;
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid #000;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #1a1a1a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { authClient } from '#/lib/auth-client.ts'
|
||||||
|
|
||||||
|
import styles from './SsoButtons.module.scss'
|
||||||
|
|
||||||
|
type Provider = 'google' | 'apple'
|
||||||
|
|
||||||
|
type SsoButtonsProps = {
|
||||||
|
onError: (message: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const continueWith = async (
|
||||||
|
provider: Provider,
|
||||||
|
onError: (message: string) => void,
|
||||||
|
) => {
|
||||||
|
const { error } = await authClient.signIn.social({
|
||||||
|
provider,
|
||||||
|
callbackURL: '/',
|
||||||
|
})
|
||||||
|
if (error) {
|
||||||
|
onError(error.message ?? `Could not continue with ${provider}.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const GoogleIcon = () => (
|
||||||
|
<svg className={styles.icon} viewBox="0 0 18 18" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
fill="#4285F4"
|
||||||
|
d="M17.64 9.2045c0-.6381-.0573-1.2518-.1636-1.8409H9v3.4814h4.8436c-.2086 1.125-.8427 2.0782-1.7959 2.7164v2.2581h2.9087c1.7018-1.5668 2.6836-3.874 2.6836-6.615z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#34A853"
|
||||||
|
d="M9 18c2.43 0 4.4673-.806 5.9564-2.1805l-2.9087-2.2581c-.8059.5404-1.8368.8595-3.0477.8595-2.3441 0-4.3273-1.5831-5.0345-3.7104H.9573v2.3318C2.4382 15.9832 5.4818 18 9 18z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#FBBC05"
|
||||||
|
d="M3.9655 10.71c-.18-.5404-.2823-1.1177-.2823-1.71s.1023-1.1696.2823-1.71V4.9582H.9573A8.9965 8.9965 0 0 0 0 9c0 1.4523.3477 2.8268.9573 4.0418L3.9655 10.71z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#EA4335"
|
||||||
|
d="M9 3.5795c1.3214 0 2.5077.4541 3.4405 1.346l2.5813-2.5814C13.4632.8918 11.426 0 9 0 5.4818 0 2.4382 2.0168.9573 4.9582L3.9655 7.29C4.6727 5.1627 6.6559 3.5795 9 3.5795z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const AppleIcon = () => (
|
||||||
|
<svg
|
||||||
|
className={styles.icon}
|
||||||
|
viewBox="0 0 384 512"
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const SsoButtons = ({ onError }: SsoButtonsProps) => (
|
||||||
|
<div className={styles.buttons}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.google}
|
||||||
|
onClick={() => continueWith('google', onError)}
|
||||||
|
>
|
||||||
|
<GoogleIcon />
|
||||||
|
Continue with Google
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.apple}
|
||||||
|
onClick={() => continueWith('apple', onError)}
|
||||||
|
>
|
||||||
|
<AppleIcon />
|
||||||
|
Continue with Apple
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
@use '../../styles/variables' as *;
|
||||||
|
|
||||||
|
.trigger {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 2.25rem;
|
||||||
|
width: 2.25rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: $color-background;
|
||||||
|
color: $color-text-muted;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $color-surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
height: 1.25rem;
|
||||||
|
width: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
right: -2px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 1.1rem;
|
||||||
|
height: 1.1rem;
|
||||||
|
padding: 0 3px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: $color-primary;
|
||||||
|
color: $color-primary-contrast;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { Link, useRouterState } from '@tanstack/react-router'
|
||||||
|
import { AnimatePresence, motion, useReducedMotion } from 'framer-motion'
|
||||||
|
|
||||||
|
import { useBasket } from '#/lib/basket.tsx'
|
||||||
|
|
||||||
|
import styles from './BasketLink.module.scss'
|
||||||
|
|
||||||
|
const MotionLink = motion.create(Link)
|
||||||
|
|
||||||
|
export const BasketIcon = () => (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M3 6h2l2.4 12.2a2 2 0 0 0 2 1.8h8.4a2 2 0 0 0 2-1.6L22 9H6" />
|
||||||
|
<circle cx="9" cy="20" r="1.4" />
|
||||||
|
<circle cx="18" cy="20" r="1.4" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
// Enter bounces in with a spring; exit uses a quick fixed-duration tween
|
||||||
|
// instead — a spring's slow settling tail would otherwise leave an
|
||||||
|
// invisible-but-still-interactive link sitting in the DOM for ~2s after it's
|
||||||
|
// visually gone.
|
||||||
|
const enterTransition = { type: 'spring', stiffness: 500, damping: 22 } as const
|
||||||
|
const exitTransition = { duration: 0.18, ease: 'easeIn' } as const
|
||||||
|
|
||||||
|
export const BasketLink = () => {
|
||||||
|
const { itemCount } = useBasket()
|
||||||
|
const reduceMotion = useReducedMotion()
|
||||||
|
const pathname = useRouterState({
|
||||||
|
select: (state) => state.location.pathname,
|
||||||
|
})
|
||||||
|
const onBasketPage = pathname === '/basket'
|
||||||
|
|
||||||
|
const pop = reduceMotion
|
||||||
|
? {
|
||||||
|
initial: { opacity: 0 },
|
||||||
|
animate: { opacity: 1, transition: exitTransition },
|
||||||
|
exit: { opacity: 0, transition: exitTransition },
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
initial: { opacity: 0, scale: 0.4 },
|
||||||
|
animate: { opacity: 1, scale: 1, transition: enterTransition },
|
||||||
|
exit: { opacity: 0, scale: 0.4, transition: exitTransition },
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{itemCount > 0 && !onBasketPage && (
|
||||||
|
<MotionLink
|
||||||
|
key="basket-link"
|
||||||
|
to="/basket"
|
||||||
|
className={styles.trigger}
|
||||||
|
aria-label={`Basket, ${itemCount} item${itemCount === 1 ? '' : 's'}`}
|
||||||
|
{...pop}
|
||||||
|
>
|
||||||
|
<BasketIcon />
|
||||||
|
<span className={styles.badge}>{itemCount}</span>
|
||||||
|
</MotionLink>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
@use '../../styles/variables' as *;
|
||||||
|
|
||||||
|
.section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $space-2;
|
||||||
|
margin-top: $space-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $space-1;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: $color-text-muted;
|
||||||
|
|
||||||
|
input {
|
||||||
|
padding: $space-2;
|
||||||
|
border: 1px solid $color-border;
|
||||||
|
border-radius: $radius;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: $color-text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.submitButton {
|
||||||
|
padding: $space-2 $space-3;
|
||||||
|
border: none;
|
||||||
|
border-radius: $radius;
|
||||||
|
background: $color-primary;
|
||||||
|
color: $color-primary-contrast;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: $color-danger;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forgetButton {
|
||||||
|
align-self: center;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: $color-text-muted;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $color-text;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import { authClient } from '#/lib/auth-client.ts'
|
||||||
|
import {
|
||||||
|
clearGuestIdentity,
|
||||||
|
getGuestIdentity,
|
||||||
|
saveGuestIdentity,
|
||||||
|
} from '#/lib/guestIdentity.ts'
|
||||||
|
import type { GuestIdentity } from '#/lib/guestIdentity.ts'
|
||||||
|
import { addGuestOrderRef } from '#/lib/guestOrders.ts'
|
||||||
|
import { placeOrder } from '#/server/orders.ts'
|
||||||
|
|
||||||
|
import styles from './CheckoutSection.module.scss'
|
||||||
|
|
||||||
|
type Customer =
|
||||||
|
| { type: 'guest'; name: string; contact: string }
|
||||||
|
| { type: 'account' }
|
||||||
|
|
||||||
|
type PlacedOrder = { id: number; guestAccessToken: string | null }
|
||||||
|
|
||||||
|
type CheckoutSectionProps = {
|
||||||
|
items: Array<{ menuItemId: number; selectedOptionIds?: Array<number> }>
|
||||||
|
disabled: boolean
|
||||||
|
onPlaced: (order: PlacedOrder) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CheckoutSection = ({
|
||||||
|
items,
|
||||||
|
disabled,
|
||||||
|
onPlaced,
|
||||||
|
}: CheckoutSectionProps) => {
|
||||||
|
const { data: session, isPending } = authClient.useSession()
|
||||||
|
const [guestName, setGuestName] = useState('')
|
||||||
|
const [guestContact, setGuestContact] = useState('')
|
||||||
|
const [rememberedGuest, setRememberedGuest] = useState<GuestIdentity | null>(
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
const [submitting, setSubmitting] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// Read after mount, not during initial render, so the server-rendered and
|
||||||
|
// first client-rendered HTML match (no localStorage on the server) and
|
||||||
|
// React doesn't flag a hydration mismatch — same reasoning as
|
||||||
|
// BasketProvider.
|
||||||
|
useEffect(() => {
|
||||||
|
setRememberedGuest(getGuestIdentity())
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const submit = async (customer: Customer) => {
|
||||||
|
setSubmitting(true)
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
const order = await placeOrder({ data: { items, customer } })
|
||||||
|
if (order.guestAccessToken) {
|
||||||
|
addGuestOrderRef({
|
||||||
|
orderId: order.id,
|
||||||
|
guestAccessToken: order.guestAccessToken,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (customer.type === 'guest') {
|
||||||
|
const identity = { name: customer.name, contact: customer.contact }
|
||||||
|
saveGuestIdentity(identity)
|
||||||
|
setRememberedGuest(identity)
|
||||||
|
}
|
||||||
|
onPlaced(order)
|
||||||
|
} catch (err) {
|
||||||
|
setError(
|
||||||
|
err instanceof Error
|
||||||
|
? err.message
|
||||||
|
: 'Could not place the order. Please try again.',
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPending) return null
|
||||||
|
|
||||||
|
if (session) {
|
||||||
|
return (
|
||||||
|
<div className={styles.section}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.submitButton}
|
||||||
|
disabled={disabled || submitting}
|
||||||
|
onClick={() => submit({ type: 'account' })}
|
||||||
|
>
|
||||||
|
{submitting ? 'Placing order…' : 'Place order'}
|
||||||
|
</button>
|
||||||
|
{error && <p className={styles.error}>{error}</p>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rememberedGuest) {
|
||||||
|
return (
|
||||||
|
<div className={styles.section}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.submitButton}
|
||||||
|
disabled={disabled || submitting}
|
||||||
|
onClick={() => submit({ type: 'guest', ...rememberedGuest })}
|
||||||
|
>
|
||||||
|
{submitting
|
||||||
|
? 'Placing order…'
|
||||||
|
: `Place order as ${rememberedGuest.name}`}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.forgetButton}
|
||||||
|
onClick={() => {
|
||||||
|
clearGuestIdentity()
|
||||||
|
setRememberedGuest(null)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Not you?
|
||||||
|
</button>
|
||||||
|
{error && <p className={styles.error}>{error}</p>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
className={styles.section}
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
void submit({ type: 'guest', name: guestName, contact: guestContact })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<label className={styles.field}>
|
||||||
|
Name
|
||||||
|
<input
|
||||||
|
value={guestName}
|
||||||
|
onChange={(event) => setGuestName(event.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className={styles.field}>
|
||||||
|
Email or phone
|
||||||
|
<input
|
||||||
|
value={guestContact}
|
||||||
|
onChange={(event) => setGuestContact(event.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={styles.submitButton}
|
||||||
|
disabled={disabled || submitting}
|
||||||
|
>
|
||||||
|
{submitting ? 'Placing order…' : 'Place order as guest'}
|
||||||
|
</button>
|
||||||
|
{error && <p className={styles.error}>{error}</p>}
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
@use '../../styles/variables' as *;
|
||||||
|
|
||||||
|
.customizer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $space-3;
|
||||||
|
padding: $space-3;
|
||||||
|
border: 1px solid $color-border;
|
||||||
|
border-radius: $radius;
|
||||||
|
background: $color-surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $space-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $space-2;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
span:first-of-type {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.delta {
|
||||||
|
color: $color-text-muted;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stepperRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-top: $space-3;
|
||||||
|
border-top: 1px solid $color-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stepper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $space-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stepperButton {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
padding: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid $color-border;
|
||||||
|
background: $color-background;
|
||||||
|
color: $color-text;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.15s ease;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
border-color: $color-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
min-width: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
import { AnimatedNumber } from '#/components/ui/AnimatedNumber.tsx'
|
||||||
|
import type { MenuItemWithRecipe } from '#/domain/ordering.ts'
|
||||||
|
import type { BasketLine } from '#/lib/basket.tsx'
|
||||||
|
import { formatPrice, formatPriceDelta } from '#/lib/format.ts'
|
||||||
|
|
||||||
|
import styles from './MenuItemCustomizer.module.scss'
|
||||||
|
|
||||||
|
type MenuItemCustomizerProps = {
|
||||||
|
item: MenuItemWithRecipe
|
||||||
|
// Every basket line for this item, including the plain no-options one —
|
||||||
|
// the stepper below tracks whichever combination is currently checked.
|
||||||
|
lines: Array<BasketLine>
|
||||||
|
onAdd: (menuItemId: number, optionIds: Array<number>) => void
|
||||||
|
onRemove: (menuItemId: number, optionIds: Array<number>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedKey = (optionIds: Array<number>) =>
|
||||||
|
[...optionIds].sort((a, b) => a - b).join(',')
|
||||||
|
|
||||||
|
export const MenuItemCustomizer = ({
|
||||||
|
item,
|
||||||
|
lines,
|
||||||
|
onAdd,
|
||||||
|
onRemove,
|
||||||
|
}: MenuItemCustomizerProps) => {
|
||||||
|
const [checkedOptionIds, setCheckedOptionIds] = useState<Array<number>>([])
|
||||||
|
|
||||||
|
const toggleOption = (optionId: number) =>
|
||||||
|
setCheckedOptionIds((prev) =>
|
||||||
|
prev.includes(optionId)
|
||||||
|
? prev.filter((id) => id !== optionId)
|
||||||
|
: [...prev, optionId],
|
||||||
|
)
|
||||||
|
|
||||||
|
// The one stepper on this page always reflects whatever combination of
|
||||||
|
// options is currently checked — checking different boxes just points it
|
||||||
|
// at a different (possibly already-existing) basket line.
|
||||||
|
const currentLine = lines.find(
|
||||||
|
(line) => sortedKey(line.optionIds) === sortedKey(checkedOptionIds),
|
||||||
|
)
|
||||||
|
const quantity = currentLine?.quantity ?? 0
|
||||||
|
const unitPriceCents =
|
||||||
|
item.priceCents +
|
||||||
|
checkedOptionIds.reduce(
|
||||||
|
(sum, id) =>
|
||||||
|
sum + (item.options.find((option) => option.id === id)?.priceDeltaCents ?? 0),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.customizer}>
|
||||||
|
<h2 className={styles.heading}>Customize</h2>
|
||||||
|
<div className={styles.panel}>
|
||||||
|
{item.options.map((option) => (
|
||||||
|
<label key={option.id} className={styles.option}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={checkedOptionIds.includes(option.id)}
|
||||||
|
onChange={() => toggleOption(option.id)}
|
||||||
|
/>
|
||||||
|
<span>{option.name}</span>
|
||||||
|
{formatPriceDelta(option.priceDeltaCents) && (
|
||||||
|
<span className={styles.delta}>
|
||||||
|
{formatPriceDelta(option.priceDeltaCents)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.stepperRow}>
|
||||||
|
<AnimatedNumber
|
||||||
|
value={unitPriceCents}
|
||||||
|
format={formatPrice}
|
||||||
|
className={styles.price}
|
||||||
|
/>
|
||||||
|
<div className={styles.stepper}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.stepperButton}
|
||||||
|
onClick={() => onRemove(item.id, checkedOptionIds)}
|
||||||
|
disabled={quantity === 0}
|
||||||
|
aria-label={`Remove one ${item.name}`}
|
||||||
|
>
|
||||||
|
<div aria-hidden="true" className={styles.icon}>−</div>
|
||||||
|
</button>
|
||||||
|
<AnimatedNumber value={quantity} className={styles.count} />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.stepperButton}
|
||||||
|
onClick={() => onAdd(item.id, checkedOptionIds)}
|
||||||
|
aria-label={`Add one ${item.name}`}
|
||||||
|
>
|
||||||
|
<div aria-hidden="true" className={styles.icon}>+</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
@use '../../styles/variables' as *;
|
||||||
|
|
||||||
|
.detail {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $space-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
margin: 0 0 $space-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin: 0 0 $space-1;
|
||||||
|
color: $color-text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
margin: 0;
|
||||||
|
color: $color-text-muted;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.allergens {
|
||||||
|
margin: $space-1 0 0;
|
||||||
|
color: $color-text-muted;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { MenuItemCustomizer } from '#/components/ordering/MenuItemCustomizer.tsx'
|
||||||
|
import { formatDuration } from '#/components/ordering/MenuList.tsx'
|
||||||
|
import { MenuItemThumbnail } from '#/components/ordering/MenuItemThumbnail.tsx'
|
||||||
|
import type { MenuItemWithRecipe } from '#/domain/ordering.ts'
|
||||||
|
import type { BasketLine } from '#/lib/basket.tsx'
|
||||||
|
import { formatPrice } from '#/lib/format.ts'
|
||||||
|
|
||||||
|
import styles from './MenuItemDetail.module.scss'
|
||||||
|
|
||||||
|
type MenuItemDetailProps = {
|
||||||
|
item: MenuItemWithRecipe
|
||||||
|
lines: Array<BasketLine>
|
||||||
|
onAdd: (menuItemId: number, optionIds?: Array<number>) => void
|
||||||
|
onRemove: (menuItemId: number, optionIds?: Array<number>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MenuItemDetail = ({
|
||||||
|
item,
|
||||||
|
lines,
|
||||||
|
onAdd,
|
||||||
|
onRemove,
|
||||||
|
}: MenuItemDetailProps) => {
|
||||||
|
const totalSeconds = item.steps.reduce(
|
||||||
|
(sum, step) => sum + step.estimatedDurationSeconds,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.detail}>
|
||||||
|
<MenuItemThumbnail
|
||||||
|
name={item.name}
|
||||||
|
size="hero"
|
||||||
|
style={{ viewTransitionName: `menu-item-${item.id}` }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.info}>
|
||||||
|
<h1 className={styles.name}>{item.name}</h1>
|
||||||
|
{item.description && (
|
||||||
|
<p className={styles.description}>{item.description}</p>
|
||||||
|
)}
|
||||||
|
<p className={styles.meta}>
|
||||||
|
{formatPrice(item.priceCents)} · {formatDuration(totalSeconds)}
|
||||||
|
</p>
|
||||||
|
{item.allergens.length > 0 && (
|
||||||
|
<p className={styles.allergens}>
|
||||||
|
Contains:{' '}
|
||||||
|
{item.allergens.map((allergen) => allergen.name).join(', ')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MenuItemCustomizer
|
||||||
|
item={item}
|
||||||
|
lines={lines}
|
||||||
|
onAdd={onAdd}
|
||||||
|
onRemove={onRemove}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
@use '../../styles/variables' as *;
|
||||||
|
|
||||||
|
.thumbnail {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 3.5rem;
|
||||||
|
height: 3.5rem;
|
||||||
|
border-radius: $radius;
|
||||||
|
overflow: hidden;
|
||||||
|
background: $color-background;
|
||||||
|
color: $color-text-muted;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
height: 1.75rem;
|
||||||
|
width: 1.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
border-radius: $radius;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
height: 3rem;
|
||||||
|
width: 3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import type { CSSProperties } from 'react'
|
||||||
|
|
||||||
|
import styles from './MenuItemThumbnail.module.scss'
|
||||||
|
|
||||||
|
type MenuItemThumbnailProps = {
|
||||||
|
name: string
|
||||||
|
size?: 'small' | 'hero'
|
||||||
|
style?: CSSProperties
|
||||||
|
}
|
||||||
|
|
||||||
|
const UtensilsIcon = () => (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M7 3v7a2 2 0 0 0 2 2v9" />
|
||||||
|
<path d="M7 3v4M11 3v4" />
|
||||||
|
<path d="M17 3c-1.5 0-2.5 2-2.5 5s1 5 1.5 5v9" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const photoByKeyword: Array<{ keyword: string; src: string }> = [
|
||||||
|
{ keyword: 'pizza', src: '/pizza.png' },
|
||||||
|
{ keyword: 'burger', src: '/burger.png' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// Falls back to a simple icon for any menu item that isn't Pizza or Burger,
|
||||||
|
// since there's no photo for it yet.
|
||||||
|
const photoFor = (name: string) => {
|
||||||
|
const lower = name.toLowerCase()
|
||||||
|
return photoByKeyword.find((entry) => lower.includes(entry.keyword))?.src
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MenuItemThumbnail = ({
|
||||||
|
name,
|
||||||
|
size = 'small',
|
||||||
|
style,
|
||||||
|
}: MenuItemThumbnailProps) => {
|
||||||
|
const src = photoFor(name)
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${styles.thumbnail} ${size === 'hero' ? styles.hero : ''}`}
|
||||||
|
style={style}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{src ? (
|
||||||
|
<img src={src} alt="" className={styles.photo} />
|
||||||
|
) : (
|
||||||
|
<UtensilsIcon />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
@use '../../styles/variables' as *;
|
||||||
|
|
||||||
|
.list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $space-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
padding: $space-3;
|
||||||
|
border: 1px solid $color-border;
|
||||||
|
border-radius: $radius;
|
||||||
|
background: $color-surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: $space-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $space-3;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
text-align: left;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:is(a, button) {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover .name {
|
||||||
|
color: $color-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
margin: 0 0 $space-1;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
transition: color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin: 0 0 $space-1;
|
||||||
|
color: $color-text-muted;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: $color-text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.allergens {
|
||||||
|
margin: $space-1 0 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: $color-text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $space-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stepperButton {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid $color-border;
|
||||||
|
background: $color-background;
|
||||||
|
color: $color-text;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
align-self: center;
|
||||||
|
justify-self: center;
|
||||||
|
transition: border-color 0.15s ease;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
border-color: $color-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
min-width: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { Link } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
import { MenuItemThumbnail } from '#/components/ordering/MenuItemThumbnail.tsx'
|
||||||
|
import { AnimatedNumber } from '#/components/ui/AnimatedNumber.tsx'
|
||||||
|
import type { MenuItemWithRecipe } from '#/domain/ordering.ts'
|
||||||
|
import type { BasketLine } from '#/lib/basket.tsx'
|
||||||
|
import { formatPrice } from '#/lib/format.ts'
|
||||||
|
|
||||||
|
import styles from './MenuList.module.scss'
|
||||||
|
|
||||||
|
type MenuListProps = {
|
||||||
|
menu: Array<MenuItemWithRecipe>
|
||||||
|
lines: Array<BasketLine>
|
||||||
|
onAdd: (menuItemId: number, optionIds?: Array<number>) => void
|
||||||
|
onRemove: (menuItemId: number, optionIds?: Array<number>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formatDuration = (seconds: number) => {
|
||||||
|
const minutes = Math.round(seconds / 60)
|
||||||
|
return minutes <= 1 ? '1 min' : `${minutes} min`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MenuList = ({ menu, lines, onAdd, onRemove }: MenuListProps) => (
|
||||||
|
<ul className={styles.list}>
|
||||||
|
{menu.map((item) => {
|
||||||
|
const defaultLine = lines.find(
|
||||||
|
(line) => line.menuItemId === item.id && line.optionIds.length === 0,
|
||||||
|
)
|
||||||
|
const hasOptions = item.options.length > 0
|
||||||
|
const totalSeconds = item.steps.reduce(
|
||||||
|
(sum, step) => sum + step.estimatedDurationSeconds,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
const info = (
|
||||||
|
<>
|
||||||
|
<MenuItemThumbnail
|
||||||
|
name={item.name}
|
||||||
|
style={{ viewTransitionName: `menu-item-${item.id}` }}
|
||||||
|
/>
|
||||||
|
<div className={styles.info}>
|
||||||
|
<h3 className={styles.name}>{item.name}</h3>
|
||||||
|
{item.description && (
|
||||||
|
<p className={styles.description}>{item.description}</p>
|
||||||
|
)}
|
||||||
|
<p className={styles.meta}>
|
||||||
|
{formatPrice(item.priceCents)} · {formatDuration(totalSeconds)}
|
||||||
|
</p>
|
||||||
|
{item.allergens.length > 0 && (
|
||||||
|
<p className={styles.allergens}>
|
||||||
|
Contains:{' '}
|
||||||
|
{item.allergens.map((allergen) => allergen.name).join(', ')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={item.id} className={styles.item}>
|
||||||
|
<div className={styles.row}>
|
||||||
|
{hasOptions ? (
|
||||||
|
<Link
|
||||||
|
to="/items/$menuItemId"
|
||||||
|
params={{ menuItemId: String(item.id) }}
|
||||||
|
className={styles.main}
|
||||||
|
>
|
||||||
|
{info}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<div className={styles.main}>{info}</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.quantity}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.stepperButton}
|
||||||
|
onClick={() => onRemove(item.id)}
|
||||||
|
disabled={(defaultLine?.quantity ?? 0) === 0}
|
||||||
|
aria-label={`Remove one ${item.name}`}
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">−</span>
|
||||||
|
</button>
|
||||||
|
<AnimatedNumber
|
||||||
|
value={defaultLine?.quantity ?? 0}
|
||||||
|
className={styles.count}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.stepperButton}
|
||||||
|
onClick={() => onAdd(item.id)}
|
||||||
|
aria-label={`Add one ${item.name}`}
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">+</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
@use '../../styles/variables' as *;
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
margin: 0 0 $space-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
color: $color-text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $space-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order {
|
||||||
|
display: block;
|
||||||
|
padding: $space-3;
|
||||||
|
border: 1px solid $color-border;
|
||||||
|
border-radius: $radius;
|
||||||
|
background: $color-surface;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: border-color 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $color-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.orderHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: $space-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orderStatus {
|
||||||
|
text-transform: capitalize;
|
||||||
|
color: $color-text-muted;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orderCustomer {
|
||||||
|
margin: 0 0 $space-1;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: $color-text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orderItems {
|
||||||
|
margin: 0 0 $space-1;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orderMeta {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: $color-text-muted;
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { Link } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
import type { AdminOrderSummary } from '#/domain/ordering.ts'
|
||||||
|
import { formatPrice } from '#/lib/format.ts'
|
||||||
|
|
||||||
|
import styles from './AdminOrdersView.module.scss'
|
||||||
|
|
||||||
|
type AdminOrdersViewProps = {
|
||||||
|
orders: Array<AdminOrderSummary>
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatItems = (items: AdminOrderSummary['items']) =>
|
||||||
|
items
|
||||||
|
.map(({ menuItemName, quantity }) =>
|
||||||
|
quantity > 1 ? `${quantity}× ${menuItemName}` : menuItemName,
|
||||||
|
)
|
||||||
|
.join(', ')
|
||||||
|
|
||||||
|
const formatCustomer = (customer: AdminOrderSummary['customer']) =>
|
||||||
|
customer.type === 'account'
|
||||||
|
? `Account ${customer.customerId}`
|
||||||
|
: `${customer.name} (guest) · ${customer.contact}`
|
||||||
|
|
||||||
|
export const AdminOrdersView = ({ orders }: AdminOrdersViewProps) => (
|
||||||
|
<section>
|
||||||
|
<h1 className={styles.heading}>All orders</h1>
|
||||||
|
{orders.length === 0 ? (
|
||||||
|
<p className={styles.empty}>No orders have been placed yet.</p>
|
||||||
|
) : (
|
||||||
|
<ul className={styles.orders}>
|
||||||
|
{orders.map((order) => (
|
||||||
|
<li key={order.id}>
|
||||||
|
<Link
|
||||||
|
to="/admin/orders/$orderId"
|
||||||
|
params={{ orderId: String(order.id) }}
|
||||||
|
className={styles.order}
|
||||||
|
viewTransition={false}
|
||||||
|
>
|
||||||
|
<div className={styles.orderHeader}>
|
||||||
|
<span>Order #{order.id}</span>
|
||||||
|
<span className={styles.orderStatus}>{order.status}</span>
|
||||||
|
</div>
|
||||||
|
<p className={styles.orderCustomer}>
|
||||||
|
{formatCustomer(order.customer)}
|
||||||
|
</p>
|
||||||
|
<p className={styles.orderItems}>{formatItems(order.items)}</p>
|
||||||
|
<p className={styles.orderMeta}>
|
||||||
|
{order.createdAt.toLocaleString([], {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: '2-digit',
|
||||||
|
})}{' '}
|
||||||
|
· {formatPrice(order.totalCents)}
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
@use '../../styles/variables' as *;
|
||||||
|
|
||||||
|
.eta {
|
||||||
|
margin: 0 0 $space-2;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: $color-success;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
overflow: hidden;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
.valueText {
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { AnimatePresence, motion, useReducedMotion } from 'framer-motion'
|
||||||
|
|
||||||
|
import { formatEtaParts } from '#/lib/format.ts'
|
||||||
|
|
||||||
|
import styles from './EtaMessage.module.scss'
|
||||||
|
|
||||||
|
export const EtaMessage = ({ etaSeconds }: { etaSeconds: number }) => {
|
||||||
|
const reduceMotion = useReducedMotion()
|
||||||
|
const { prefix, value } = formatEtaParts(etaSeconds)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p className={styles.eta}>
|
||||||
|
{prefix}{' '}
|
||||||
|
<span className={styles.value}>
|
||||||
|
<AnimatePresence initial={false} mode="popLayout">
|
||||||
|
<motion.span
|
||||||
|
key={value}
|
||||||
|
className={styles.valueText}
|
||||||
|
initial={reduceMotion ? false : { y: '100%', opacity: 0 }}
|
||||||
|
animate={{ y: 0, opacity: 1 }}
|
||||||
|
exit={reduceMotion ? { opacity: 0 } : { y: '-100%', opacity: 0 }}
|
||||||
|
transition={{ duration: reduceMotion ? 0 : 0.35, ease: 'easeOut' }}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</motion.span>
|
||||||
|
</AnimatePresence>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
@use '../../styles/variables' as *;
|
||||||
|
|
||||||
|
.banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $space-2;
|
||||||
|
color: $color-success;
|
||||||
|
|
||||||
|
&[data-variant='cancelled'] {
|
||||||
|
color: $color-danger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 1.75rem;
|
||||||
|
height: 1.75rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
fill: none;
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-width: 2;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: $color-text-muted;
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { motion, useReducedMotion } from 'framer-motion'
|
||||||
|
|
||||||
|
import styles from './ItemStatusBanner.module.scss'
|
||||||
|
|
||||||
|
export type ItemStatusBannerVariant = 'ready' | 'pickedUp' | 'cancelled'
|
||||||
|
|
||||||
|
const iconPaths = {
|
||||||
|
ready: ['M7.5 12.5l3 3 6-6.5'],
|
||||||
|
pickedUp: ['M7.5 12.5l3 3 6-6.5'],
|
||||||
|
cancelled: ['M8.5 8.5l7 7', 'M15.5 8.5l-7 7'],
|
||||||
|
} satisfies Record<ItemStatusBannerVariant, Array<string>>
|
||||||
|
|
||||||
|
type ItemStatusBannerProps = {
|
||||||
|
variant: ItemStatusBannerVariant
|
||||||
|
label: string
|
||||||
|
detail?: string
|
||||||
|
animateIn: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ItemStatusBanner = ({
|
||||||
|
variant,
|
||||||
|
label,
|
||||||
|
detail,
|
||||||
|
animateIn,
|
||||||
|
}: ItemStatusBannerProps) => {
|
||||||
|
const reduceMotion = useReducedMotion()
|
||||||
|
const skipAnimation = !animateIn || reduceMotion
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className={styles.banner}
|
||||||
|
data-variant={variant}
|
||||||
|
role="status"
|
||||||
|
initial={skipAnimation ? false : { opacity: 0, y: 4 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
<svg className={styles.icon} viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<motion.circle
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
initial={skipAnimation ? false : { pathLength: 0, rotate: -90 }}
|
||||||
|
animate={{ pathLength: 1, rotate: -90 }}
|
||||||
|
transition={{ duration: 0.5, ease: 'easeOut' }}
|
||||||
|
/>
|
||||||
|
{iconPaths[variant].map((d, index) => (
|
||||||
|
<motion.path
|
||||||
|
key={d}
|
||||||
|
d={d}
|
||||||
|
initial={skipAnimation ? false : { pathLength: 0 }}
|
||||||
|
animate={{ pathLength: 1 }}
|
||||||
|
transition={{
|
||||||
|
duration: 0.3,
|
||||||
|
ease: 'easeOut',
|
||||||
|
delay: 0.45 + index * 0.2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
<span className={styles.text}>
|
||||||
|
<span className={styles.label}>{label}</span>
|
||||||
|
{detail && <span className={styles.detail}>{detail}</span>}
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,404 @@
|
|||||||
|
@use '../../styles/variables' as *;
|
||||||
|
|
||||||
|
.page {
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: $space-5 $space-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backLink {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $space-1;
|
||||||
|
margin: 0 0 $space-3;
|
||||||
|
color: $color-text-muted;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $color-primary;
|
||||||
|
|
||||||
|
.backArrow {
|
||||||
|
transform: translateX(-3px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.backArrow {
|
||||||
|
height: 1rem;
|
||||||
|
width: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: transform 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
margin: 0 0 $space-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orderStatus {
|
||||||
|
margin: 0 0 $space-1;
|
||||||
|
color: $color-text-muted;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orderDate {
|
||||||
|
margin: 0 0 $space-4;
|
||||||
|
color: $color-text-muted;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.simulateButton {
|
||||||
|
margin: 0 0 $space-4;
|
||||||
|
padding: $space-1 $space-3;
|
||||||
|
border: 1px dashed $color-text-muted;
|
||||||
|
border-radius: $radius;
|
||||||
|
background: none;
|
||||||
|
color: $color-text-muted;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
border-color 0.15s ease,
|
||||||
|
color 0.15s ease;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
border-color: $color-primary;
|
||||||
|
color: $color-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: $color-danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $space-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
padding: $space-3;
|
||||||
|
border: 1px solid $color-border;
|
||||||
|
border-radius: $radius;
|
||||||
|
background: $color-surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: $space-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuButton {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin: (-$space-2) (-$space-2) (-$space-2) $space-2;
|
||||||
|
padding: $space-2;
|
||||||
|
border: none;
|
||||||
|
border-radius: $radius;
|
||||||
|
background: none;
|
||||||
|
color: $color-text-muted;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
stroke-width: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $color-text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
position: fixed;
|
||||||
|
position-area: bottom span-left;
|
||||||
|
position-try-fallbacks: flip-block;
|
||||||
|
inset: auto;
|
||||||
|
margin: $space-1 0;
|
||||||
|
padding: $space-1;
|
||||||
|
min-width: 160px;
|
||||||
|
border: 1px solid $color-border;
|
||||||
|
border-radius: $radius;
|
||||||
|
background: $color-surface;
|
||||||
|
color: $color-text;
|
||||||
|
font-weight: 400;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $space-2;
|
||||||
|
width: 100%;
|
||||||
|
padding: $space-2 $space-3;
|
||||||
|
border: none;
|
||||||
|
border-radius: calc(#{$radius} - 2px);
|
||||||
|
background: none;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-align: left;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: $color-background;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemOptions {
|
||||||
|
margin: 0 0 $space-2;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: $color-text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The negative margin + matching padding leaves room for the in-progress
|
||||||
|
// dot's sonar ping, which the collapse animation's overflow would clip.
|
||||||
|
.timelineWrapper {
|
||||||
|
overflow: hidden;
|
||||||
|
margin: (-$space-2);
|
||||||
|
padding: $space-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bannerRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: $space-2;
|
||||||
|
margin: 0 0 $space-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline {
|
||||||
|
margin: 0 0 $space-2;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: $space-3 1fr;
|
||||||
|
column-gap: $space-2;
|
||||||
|
padding-bottom: $space-3;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: $color-text-muted;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stepMarker {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stepDot {
|
||||||
|
position: relative;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
margin-top: 3px;
|
||||||
|
border: 2px solid $color-border;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: $color-background;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stepConnector {
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
bottom: -$space-3;
|
||||||
|
left: 50%;
|
||||||
|
width: 2px;
|
||||||
|
background: $color-border;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stepConnectorFill {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: $color-success;
|
||||||
|
transform-origin: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step:last-child .stepConnector {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stepContent {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $space-1;
|
||||||
|
padding-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stepMeta {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step[data-status='done'] {
|
||||||
|
opacity: 0.3;
|
||||||
|
|
||||||
|
.stepDot {
|
||||||
|
border-color: $color-success;
|
||||||
|
background: $color-success;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stepLabel {
|
||||||
|
color: $color-text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.step[data-status='in_progress'] {
|
||||||
|
.stepDot {
|
||||||
|
border-color: $color-success;
|
||||||
|
background: $color-success;
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
animation: dotPopIn 0.4s ease-out 0.6s backwards;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: $color-success;
|
||||||
|
opacity: 0;
|
||||||
|
animation: sonarPing 1.6s ease-out 1s infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animates the box size instead of transform: scale, which Chrome
|
||||||
|
// rasterizes at a low resolution mid-animation and renders the tiny dot square.
|
||||||
|
@keyframes dotPopIn {
|
||||||
|
0% {
|
||||||
|
border-color: $color-border;
|
||||||
|
background: $color-background;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sonarPing {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(2.8);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.step[data-status='pending'] {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: $space-2;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
button,
|
||||||
|
a {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $space-1;
|
||||||
|
padding: $space-2 $space-4;
|
||||||
|
border: 1px solid $color-border;
|
||||||
|
border-radius: $radius;
|
||||||
|
background: $color-background;
|
||||||
|
color: $color-text;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.15s ease;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
border-color: $color-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pickupButton {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
border-color: $color-text;
|
||||||
|
background: $color-text;
|
||||||
|
color: $color-background;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
border-color: $color-text;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-tablet) {
|
||||||
|
flex-basis: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: $space-2;
|
||||||
|
margin-bottom: $space-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionIcon {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The shackle lifts above the 24-unit viewBox when unlocked.
|
||||||
|
.lockIcon {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancelButton {
|
||||||
|
padding: $space-1 $space-2;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: $color-text-muted;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
text-decoration: underline;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
color: $color-danger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,576 @@
|
|||||||
|
import { Link, useNavigate, useRouter } from '@tanstack/react-router'
|
||||||
|
import { AnimatePresence, motion, useReducedMotion } from 'framer-motion'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
import { HoldButton } from '#/components/ui/HoldButton.tsx'
|
||||||
|
import type { OrderStatus } from '#/db/schema.ts'
|
||||||
|
import type { OrderItemStatusView } from '#/domain/ordering.ts'
|
||||||
|
import { useBasket } from '#/lib/basket.tsx'
|
||||||
|
import { formatCountdown } from '#/lib/format.ts'
|
||||||
|
import {
|
||||||
|
advanceSimulatedKitchen,
|
||||||
|
cancelOrderItem,
|
||||||
|
closePickupBox,
|
||||||
|
getOrderStatus,
|
||||||
|
openPickupBox,
|
||||||
|
} from '#/server/orders.ts'
|
||||||
|
|
||||||
|
import styles from './OrderStatusView.module.scss'
|
||||||
|
import { EtaMessage } from './EtaMessage.tsx'
|
||||||
|
import { ItemStatusBanner } from './ItemStatusBanner.tsx'
|
||||||
|
|
||||||
|
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
|
|
||||||
|
const isSettled = (item: OrderItemStatusView) =>
|
||||||
|
item.status === 'ready' ||
|
||||||
|
item.status === 'picked_up' ||
|
||||||
|
item.status === 'cancelled' ||
|
||||||
|
item.status === 'discarded'
|
||||||
|
|
||||||
|
// `animationKey` groups variants that should read as one continuous banner:
|
||||||
|
// going from ready to picked up swaps the label without replaying the check.
|
||||||
|
// `now` is null until the page hydrates, so the server and first client
|
||||||
|
// render agree and the countdown only appears once it can tick.
|
||||||
|
const getItemBanner = (item: OrderItemStatusView, now: number | null) => {
|
||||||
|
if (item.status === 'ready') {
|
||||||
|
return {
|
||||||
|
animationKey: 'done',
|
||||||
|
variant: 'ready',
|
||||||
|
label: 'Ready for pickup',
|
||||||
|
detail: !item.pickupBox
|
||||||
|
? 'Waiting for a free pickup box'
|
||||||
|
: item.pickupDeadline && now !== null
|
||||||
|
? `Pickup box ${item.pickupBox.number} · ${formatCountdown(item.pickupDeadline.getTime() - now)} left`
|
||||||
|
: `Pickup box ${item.pickupBox.number}`,
|
||||||
|
} as const
|
||||||
|
}
|
||||||
|
if (item.status === 'discarded') {
|
||||||
|
return {
|
||||||
|
animationKey: 'discarded',
|
||||||
|
variant: 'cancelled',
|
||||||
|
label: 'Not picked up in time',
|
||||||
|
detail: `${item.pickupBox ? `Pickup box ${item.pickupBox.number}` : 'The pickup box'} was emptied after 15 minutes. This item can't be refunded.`,
|
||||||
|
} as const
|
||||||
|
}
|
||||||
|
if (item.status === 'picked_up') {
|
||||||
|
return {
|
||||||
|
animationKey: 'done',
|
||||||
|
variant: 'pickedUp',
|
||||||
|
label: 'Picked up',
|
||||||
|
detail: item.pickupBox
|
||||||
|
? `Pickup box ${item.pickupBox.number}`
|
||||||
|
: undefined,
|
||||||
|
} as const
|
||||||
|
}
|
||||||
|
if (item.status === 'cancelled') {
|
||||||
|
return item.cancelledBy === 'kitchen'
|
||||||
|
? ({
|
||||||
|
animationKey: 'cancelled',
|
||||||
|
variant: 'cancelled',
|
||||||
|
label: 'Cancelled by the kitchen',
|
||||||
|
detail:
|
||||||
|
"Something went wrong on our end, so we've refunded this item.",
|
||||||
|
} as const)
|
||||||
|
: ({
|
||||||
|
animationKey: 'cancelled',
|
||||||
|
variant: 'cancelled',
|
||||||
|
label: 'Cancelled',
|
||||||
|
detail: "You've been refunded for this item.",
|
||||||
|
} as const)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const bannerKey = (item: OrderItemStatusView) => {
|
||||||
|
const banner = getItemBanner(item, null)
|
||||||
|
return banner ? `${item.id}:${banner.animationKey}` : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const BackArrowIcon = () => (
|
||||||
|
<svg
|
||||||
|
className={styles.backArrow}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M19 12H5" />
|
||||||
|
<path d="M11 18l-6-6 6-6" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const actionIconPaths = {
|
||||||
|
buyAgain: ['M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8', 'M3 3v5h5'],
|
||||||
|
help: [
|
||||||
|
'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20Z',
|
||||||
|
'M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3',
|
||||||
|
'M12 17h.01',
|
||||||
|
],
|
||||||
|
cancel: ['M18 6 6 18', 'M6 6l12 12'],
|
||||||
|
more: ['M12 12h.01', 'M19 12h.01', 'M5 12h.01'],
|
||||||
|
}
|
||||||
|
|
||||||
|
const ActionIcon = ({ name }: { name: keyof typeof actionIconPaths }) => (
|
||||||
|
<svg
|
||||||
|
className={styles.actionIcon}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{actionIconPaths[name].map((d) => (
|
||||||
|
<path key={d} d={d} />
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const CLOSED_BOX_VISIBLE_MS = 5 * 60 * 1000
|
||||||
|
// Same commands in both so the shackle morphs: unlocked, the left leg
|
||||||
|
// stretches up from the body and the right leg shrinks to a stub.
|
||||||
|
const LOCKED_SHACKLE = 'M7 11V7a5 5 0 0 1 10 0v4'
|
||||||
|
const UNLOCKED_SHACKLE = 'M7 11V4a5 5 0 0 1 10 0v1'
|
||||||
|
// Overshoots past the target, so the shackle springs open and snaps shut.
|
||||||
|
const SHACKLE_EASE = [0.34, 1.8, 0.64, 1] as const
|
||||||
|
|
||||||
|
const pickupButtonLabel = (
|
||||||
|
box: { number: number; isOpen: boolean },
|
||||||
|
closed: boolean,
|
||||||
|
) => {
|
||||||
|
if (closed) return `Box ${box.number} closed`
|
||||||
|
return box.isOpen
|
||||||
|
? `Hold to close box ${box.number}`
|
||||||
|
: `Hold to open box ${box.number}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const LockIcon = ({ open }: { open: boolean }) => {
|
||||||
|
const reduceMotion = useReducedMotion()
|
||||||
|
return (
|
||||||
|
<motion.svg
|
||||||
|
className={`${styles.actionIcon} ${styles.lockIcon}`}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
style={{ originY: 1 }}
|
||||||
|
initial={false}
|
||||||
|
animate={
|
||||||
|
open
|
||||||
|
? {
|
||||||
|
y: [0, -4, 1, -1.5, 0],
|
||||||
|
scaleX: [1, 0.94, 1.06, 0.98, 1],
|
||||||
|
scaleY: [1, 1.1, 0.92, 1.03, 1],
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
y: [0, 2, -3.5, 1, -1, 0],
|
||||||
|
scaleX: [1, 1.14, 0.94, 1.04, 0.99, 1],
|
||||||
|
scaleY: [1, 0.8, 1.1, 0.96, 1.02, 1],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
transition={
|
||||||
|
reduceMotion ? { duration: 0 } : { duration: 0.65, ease: 'easeOut' }
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<path d="M5 11h14a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2Z" />
|
||||||
|
<motion.path
|
||||||
|
initial={false}
|
||||||
|
animate={{ d: open ? UNLOCKED_SHACKLE : LOCKED_SHACKLE }}
|
||||||
|
transition={
|
||||||
|
reduceMotion ? { duration: 0 } : { duration: 0.4, ease: SHACKLE_EASE }
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</motion.svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const StepConnector = ({ filled }: { filled: boolean }) => {
|
||||||
|
const reduceMotion = useReducedMotion()
|
||||||
|
return (
|
||||||
|
<span className={styles.stepConnector}>
|
||||||
|
<motion.span
|
||||||
|
className={styles.stepConnectorFill}
|
||||||
|
initial={false}
|
||||||
|
animate={{ scaleY: filled ? 1 : 0 }}
|
||||||
|
transition={
|
||||||
|
reduceMotion ? { duration: 0 } : { duration: 0.6, ease: 'easeInOut' }
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OrderStatusResult = {
|
||||||
|
id: number
|
||||||
|
status: OrderStatus
|
||||||
|
createdAt: Date
|
||||||
|
items: Array<OrderItemStatusView>
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrderStatusViewProps = {
|
||||||
|
status: OrderStatusResult
|
||||||
|
guestAccessToken: string | undefined
|
||||||
|
backTo?: '/orders' | '/admin/orders'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OrderStatusView = ({
|
||||||
|
status,
|
||||||
|
guestAccessToken,
|
||||||
|
backTo = '/orders',
|
||||||
|
}: OrderStatusViewProps) => {
|
||||||
|
const router = useRouter()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const basket = useBasket()
|
||||||
|
const [pendingItemId, setPendingItemId] = useState<number | null>(null)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [simulating, setSimulating] = useState(false)
|
||||||
|
// Only banners that appear while the page is open animate in; ones
|
||||||
|
// already showing on load render straight away.
|
||||||
|
const [initialBannerKeys] = useState(
|
||||||
|
() => new Set(status.items.map(bannerKey).filter((key) => key !== null)),
|
||||||
|
)
|
||||||
|
// Ticks every second while a pickup countdown runs; otherwise only wakes
|
||||||
|
// when a closed box's button (kept for a while after pickup) is due to go.
|
||||||
|
const [now, setNow] = useState(() => Date.now())
|
||||||
|
const [hydrated, setHydrated] = useState(false)
|
||||||
|
useEffect(() => setHydrated(true), [])
|
||||||
|
const hasPickupCountdown = status.items.some(
|
||||||
|
(item) => item.pickupDeadline !== null,
|
||||||
|
)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hasPickupCountdown) return
|
||||||
|
setNow(Date.now())
|
||||||
|
const interval = setInterval(() => setNow(Date.now()), 1000)
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [hasPickupCountdown])
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasPickupCountdown) return
|
||||||
|
const hideAt = status.items
|
||||||
|
.flatMap((item) =>
|
||||||
|
item.pickedUpAt
|
||||||
|
? [item.pickedUpAt.getTime() + CLOSED_BOX_VISIBLE_MS]
|
||||||
|
: [],
|
||||||
|
)
|
||||||
|
.filter((time) => time > now)
|
||||||
|
if (hideAt.length === 0) return
|
||||||
|
const timeout = setTimeout(
|
||||||
|
() => setNow(Date.now()),
|
||||||
|
Math.min(...hideAt) - Date.now(),
|
||||||
|
)
|
||||||
|
return () => clearTimeout(timeout)
|
||||||
|
}, [status.items, now, hasPickupCountdown])
|
||||||
|
// Once a countdown runs out, reload so the server can empty the box and the
|
||||||
|
// item shows as discarded. Only once per item: an open box isn't emptied.
|
||||||
|
const refreshedExpiredItemIds = useRef(new Set<number>())
|
||||||
|
useEffect(() => {
|
||||||
|
const newlyExpired = status.items.filter(
|
||||||
|
(item) =>
|
||||||
|
item.pickupDeadline !== null &&
|
||||||
|
item.pickupDeadline.getTime() <= now &&
|
||||||
|
!refreshedExpiredItemIds.current.has(item.id),
|
||||||
|
)
|
||||||
|
if (newlyExpired.length === 0) return
|
||||||
|
for (const item of newlyExpired)
|
||||||
|
refreshedExpiredItemIds.current.add(item.id)
|
||||||
|
void router.invalidate()
|
||||||
|
}, [status.items, now, router])
|
||||||
|
const reduceMotion = useReducedMotion()
|
||||||
|
const collapseTransition = reduceMotion
|
||||||
|
? { duration: 0 }
|
||||||
|
: { duration: 0.4, ease: 'easeInOut' as const }
|
||||||
|
|
||||||
|
const buyAgain = (item: OrderItemStatusView) => {
|
||||||
|
basket.add(item.menuItemId, item.buyAgainOptionIds)
|
||||||
|
void navigate({ to: '/basket' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const runAction = async (
|
||||||
|
orderItemId: number,
|
||||||
|
action: () => Promise<void>,
|
||||||
|
) => {
|
||||||
|
setPendingItemId(orderItemId)
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
await action()
|
||||||
|
await router.invalidate()
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Something went wrong.')
|
||||||
|
} finally {
|
||||||
|
setPendingItemId(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collapses to nothing on both axes, so the row closes the gap whether the
|
||||||
|
// button sits inline (desktop) or on its own full-width line (mobile); the
|
||||||
|
// negative margins cancel the flex gap it leaves behind.
|
||||||
|
const pickupButtonExit = reduceMotion
|
||||||
|
? { opacity: 0, transition: { duration: 0 } }
|
||||||
|
: {
|
||||||
|
opacity: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
paddingTop: 0,
|
||||||
|
paddingBottom: 0,
|
||||||
|
paddingLeft: 0,
|
||||||
|
paddingRight: 0,
|
||||||
|
borderWidth: 0,
|
||||||
|
marginTop: 0,
|
||||||
|
marginRight: -8,
|
||||||
|
marginBottom: -8,
|
||||||
|
transition: { duration: 0.3, ease: 'easeInOut' as const },
|
||||||
|
}
|
||||||
|
|
||||||
|
const togglePickupBox = (item: OrderItemStatusView) => {
|
||||||
|
const toggle = item.pickupBox?.isOpen ? closePickupBox : openPickupBox
|
||||||
|
return toggle({ data: { orderItemId: item.id, guestAccessToken } })
|
||||||
|
}
|
||||||
|
|
||||||
|
const runKitchenSimulation = async () => {
|
||||||
|
setSimulating(true)
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
for (;;) {
|
||||||
|
await advanceSimulatedKitchen()
|
||||||
|
const fresh = await getOrderStatus({
|
||||||
|
data: { orderId: status.id, guestAccessToken },
|
||||||
|
})
|
||||||
|
await router.invalidate()
|
||||||
|
if (
|
||||||
|
!fresh.items.some(
|
||||||
|
(item) => item.status === 'placed' || item.status === 'paid',
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await sleep(1000)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Something went wrong.')
|
||||||
|
} finally {
|
||||||
|
setSimulating(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasItemsToSimulate = status.items.some(
|
||||||
|
(item) => item.status === 'placed' || item.status === 'paid',
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={styles.page}>
|
||||||
|
<Link
|
||||||
|
to={backTo}
|
||||||
|
className={styles.backLink}
|
||||||
|
viewTransition={backTo !== '/admin/orders'}
|
||||||
|
>
|
||||||
|
<BackArrowIcon />
|
||||||
|
{backTo === '/admin/orders' ? 'Back to all orders' : 'Back to orders'}
|
||||||
|
</Link>
|
||||||
|
<div style={{ viewTransitionName: `order-card-${status.id}` }}>
|
||||||
|
<h1 className={styles.heading}>Order #{status.id}</h1>
|
||||||
|
<p className={styles.orderStatus}>Status: {status.status}</p>
|
||||||
|
<p className={styles.orderDate}>
|
||||||
|
Placed{' '}
|
||||||
|
{status.createdAt.toLocaleDateString([], {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
})}
|
||||||
|
{' at '}
|
||||||
|
{status.createdAt.toLocaleTimeString([], {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{import.meta.env.DEV && hasItemsToSimulate && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.simulateButton}
|
||||||
|
disabled={simulating}
|
||||||
|
onClick={() => void runKitchenSimulation()}
|
||||||
|
>
|
||||||
|
{simulating ? 'Simulating kitchen…' : 'Simulate kitchen (dev)'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{error && <p className={styles.error}>{error}</p>}
|
||||||
|
<ul className={styles.items}>
|
||||||
|
{status.items.map((item) => {
|
||||||
|
const banner = getItemBanner(item, hydrated ? now : null)
|
||||||
|
const boxClosed = item.status !== 'ready'
|
||||||
|
const pickupBox =
|
||||||
|
item.status === 'ready' ||
|
||||||
|
(item.pickedUpAt !== null &&
|
||||||
|
now - item.pickedUpAt.getTime() < CLOSED_BOX_VISIBLE_MS)
|
||||||
|
? item.pickupBox
|
||||||
|
: null
|
||||||
|
return (
|
||||||
|
<li key={item.id} className={styles.item}>
|
||||||
|
<div className={styles.itemHeader}>
|
||||||
|
<span className={styles.itemName}>{item.menuItemName}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.menuButton}
|
||||||
|
aria-label={`More actions for ${item.menuItemName}`}
|
||||||
|
aria-haspopup="menu"
|
||||||
|
popoverTarget={`item-menu-${item.id}`}
|
||||||
|
style={{ anchorName: `--item-menu-${item.id}` }}
|
||||||
|
>
|
||||||
|
<ActionIcon name="more" />
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
id={`item-menu-${item.id}`}
|
||||||
|
popover="auto"
|
||||||
|
role="menu"
|
||||||
|
className={styles.menu}
|
||||||
|
style={{ positionAnchor: `--item-menu-${item.id}` }}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="menuitem"
|
||||||
|
className={styles.menuItem}
|
||||||
|
disabled={pendingItemId === item.id}
|
||||||
|
onClick={() => buyAgain(item)}
|
||||||
|
>
|
||||||
|
<ActionIcon name="buyAgain" />
|
||||||
|
Buy again
|
||||||
|
</button>
|
||||||
|
<Link
|
||||||
|
role="menuitem"
|
||||||
|
className={styles.menuItem}
|
||||||
|
to="/help"
|
||||||
|
search={{
|
||||||
|
orderId: status.id,
|
||||||
|
item: item.menuItemName,
|
||||||
|
status: item.status,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ActionIcon name="help" />
|
||||||
|
Get help
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{item.options.length > 0 && (
|
||||||
|
<p className={styles.itemOptions}>
|
||||||
|
{item.options.map((option) => option.name).join(', ')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<AnimatePresence initial={false}>
|
||||||
|
{!isSettled(item) && (
|
||||||
|
<motion.div
|
||||||
|
key="timeline"
|
||||||
|
className={styles.timelineWrapper}
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{ height: 'auto', opacity: 1 }}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
transition={collapseTransition}
|
||||||
|
>
|
||||||
|
<ol className={styles.timeline}>
|
||||||
|
{item.steps.map((step) => (
|
||||||
|
<li
|
||||||
|
key={step.sequence}
|
||||||
|
className={styles.step}
|
||||||
|
data-status={step.status}
|
||||||
|
aria-current={
|
||||||
|
step.status === 'in_progress' ? 'step' : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={styles.stepMarker}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<span className={styles.stepDot} />
|
||||||
|
<StepConnector filled={step.status === 'done'} />
|
||||||
|
</span>
|
||||||
|
<span className={styles.stepContent}>
|
||||||
|
<span className={styles.stepLabel}>
|
||||||
|
{step.name}
|
||||||
|
</span>
|
||||||
|
{step.status === 'queued' &&
|
||||||
|
step.queuePosition !== null && (
|
||||||
|
<span className={styles.stepMeta}>
|
||||||
|
#{step.queuePosition} in line
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
{banner && (
|
||||||
|
<div className={styles.bannerRow}>
|
||||||
|
<ItemStatusBanner
|
||||||
|
key={banner.animationKey}
|
||||||
|
variant={banner.variant}
|
||||||
|
label={banner.label}
|
||||||
|
detail={'detail' in banner ? banner.detail : undefined}
|
||||||
|
animateIn={
|
||||||
|
!initialBannerKeys.has(
|
||||||
|
`${item.id}:${banner.animationKey}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{item.etaSeconds !== null && (
|
||||||
|
<EtaMessage etaSeconds={item.etaSeconds} />
|
||||||
|
)}
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<AnimatePresence initial={false}>
|
||||||
|
{pickupBox && (
|
||||||
|
<HoldButton
|
||||||
|
key="pickupBox"
|
||||||
|
className={styles.pickupButton}
|
||||||
|
disabled={pendingItemId === item.id || boxClosed}
|
||||||
|
onHold={() =>
|
||||||
|
void runAction(item.id, () => togglePickupBox(item))
|
||||||
|
}
|
||||||
|
exit={pickupButtonExit}
|
||||||
|
>
|
||||||
|
<LockIcon open={pickupBox.isOpen} />
|
||||||
|
<AnimatePresence mode="wait" initial={false}>
|
||||||
|
<motion.span
|
||||||
|
key={pickupButtonLabel(pickupBox, boxClosed)}
|
||||||
|
initial={{ opacity: 0, y: 6 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -6 }}
|
||||||
|
transition={{ duration: reduceMotion ? 0 : 0.18 }}
|
||||||
|
>
|
||||||
|
{pickupButtonLabel(pickupBox, boxClosed)}
|
||||||
|
</motion.span>
|
||||||
|
</AnimatePresence>
|
||||||
|
</HoldButton>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
{item.cancellable && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.cancelButton}
|
||||||
|
disabled={pendingItemId === item.id}
|
||||||
|
onClick={() =>
|
||||||
|
void runAction(item.id, () =>
|
||||||
|
cancelOrderItem({
|
||||||
|
data: { orderItemId: item.id, guestAccessToken },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ActionIcon name="cancel" />
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
@use '../../styles/variables' as *;
|
||||||
|
|
||||||
|
.page {
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: $space-5 $space-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
margin: 0 0 $space-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes empty-state-enter {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(8px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: $space-1;
|
||||||
|
padding: $space-5 $space-3;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px dashed $color-border;
|
||||||
|
border-radius: $radius;
|
||||||
|
background: $color-surface;
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
animation: empty-state-enter 0.32s cubic-bezier(0.2, 0, 0, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
margin-bottom: $space-2;
|
||||||
|
color: $color-text-muted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyTitle {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyText {
|
||||||
|
margin: 0 0 $space-3;
|
||||||
|
color: $color-text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyCta {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $space-1;
|
||||||
|
padding: $space-2 $space-4;
|
||||||
|
border-radius: $radius;
|
||||||
|
background: $color-primary;
|
||||||
|
color: $color-primary-contrast;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
box-shadow: 0 2px 8px color-mix(in srgb, $color-primary 35%, transparent);
|
||||||
|
transition:
|
||||||
|
transform 0.15s ease,
|
||||||
|
box-shadow 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px color-mix(in srgb, $color-primary 45%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
box-shadow: 0 2px 8px color-mix(in srgb, $color-primary 35%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyCtaArrow {
|
||||||
|
transition: transform 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyCta:hover .emptyCtaArrow {
|
||||||
|
transform: translateX(3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $space-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order {
|
||||||
|
display: block;
|
||||||
|
padding: $space-3;
|
||||||
|
border: 1px solid $color-border;
|
||||||
|
border-radius: $radius;
|
||||||
|
background: $color-surface;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
transition:
|
||||||
|
border-color 0.15s ease,
|
||||||
|
opacity 0.28s ease,
|
||||||
|
transform 0.28s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $color-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.orderExiting .order {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-8px) scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.orderHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: $space-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orderStatus {
|
||||||
|
text-transform: capitalize;
|
||||||
|
color: $color-text-muted;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&[data-progress='ready'],
|
||||||
|
&[data-progress='picked_up'] {
|
||||||
|
color: $color-success;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-progress='cancelled'],
|
||||||
|
&[data-progress='discarded'] {
|
||||||
|
color: $color-danger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.orderItems {
|
||||||
|
margin: 0 0 $space-1;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orderMeta {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: $color-text-muted;
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import { Link } from '@tanstack/react-router'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
import type { CustomerOrderSummary } from '#/domain/ordering.ts'
|
||||||
|
import { formatPrice, formatStatus } from '#/lib/format.ts'
|
||||||
|
|
||||||
|
import styles from './OrdersListView.module.scss'
|
||||||
|
|
||||||
|
type OrdersListViewProps = {
|
||||||
|
orders: Array<CustomerOrderSummary>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must match the CSS transition duration on .order in
|
||||||
|
// OrdersListView.module.scss.
|
||||||
|
const EXIT_DURATION_MS = 280
|
||||||
|
|
||||||
|
const OrdersIcon = () => (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M9 3h6l1 3H8l1-3Z" />
|
||||||
|
<path d="M4 6h16l-1.2 13.2A2 2 0 0 1 16.81 21H7.19a2 2 0 0 1-2-1.8L4 6Z" />
|
||||||
|
<path d="M9 11h6" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const formatItems = (items: CustomerOrderSummary['items']) =>
|
||||||
|
items
|
||||||
|
.map(({ menuItemName, quantity }) =>
|
||||||
|
quantity > 1 ? `${quantity}× ${menuItemName}` : menuItemName,
|
||||||
|
)
|
||||||
|
.join(', ')
|
||||||
|
|
||||||
|
export const OrdersListView = ({ orders }: OrdersListViewProps) => {
|
||||||
|
const [displayedOrders, setDisplayedOrders] = useState(orders)
|
||||||
|
const [isExiting, setIsExiting] = useState(false)
|
||||||
|
const hadOrders = useRef(orders.length > 0)
|
||||||
|
|
||||||
|
// Orders can go from populated to empty in one prop update (e.g. signing
|
||||||
|
// out clears them) — keep the previous list on screen just long enough to
|
||||||
|
// fade it out instead of popping straight to the empty state.
|
||||||
|
useEffect(() => {
|
||||||
|
if (orders.length === 0 && hadOrders.current) {
|
||||||
|
hadOrders.current = false
|
||||||
|
setIsExiting(true)
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
setDisplayedOrders([])
|
||||||
|
setIsExiting(false)
|
||||||
|
}, EXIT_DURATION_MS)
|
||||||
|
return () => clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
hadOrders.current = orders.length > 0
|
||||||
|
setDisplayedOrders(orders)
|
||||||
|
}, [orders])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={styles.page}>
|
||||||
|
<h1 className={styles.heading}>Your orders</h1>
|
||||||
|
{displayedOrders.length === 0 ? (
|
||||||
|
<div className={styles.empty}>
|
||||||
|
<OrdersIcon />
|
||||||
|
<p className={styles.emptyTitle}>No orders yet</p>
|
||||||
|
<p className={styles.emptyText}>
|
||||||
|
Your placed orders will show up here.
|
||||||
|
</p>
|
||||||
|
<Link to="/" className={styles.emptyCta}>
|
||||||
|
Browse the menu
|
||||||
|
<span className={styles.emptyCtaArrow} aria-hidden="true">
|
||||||
|
→
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ul className={styles.orders}>
|
||||||
|
{displayedOrders.map((order) => (
|
||||||
|
<li
|
||||||
|
key={order.id}
|
||||||
|
className={isExiting ? styles.orderExiting : undefined}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
to="/orders/$orderId"
|
||||||
|
params={{ orderId: String(order.id) }}
|
||||||
|
search={
|
||||||
|
order.guestAccessToken
|
||||||
|
? { token: order.guestAccessToken }
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
className={styles.order}
|
||||||
|
style={{ viewTransitionName: `order-card-${order.id}` }}
|
||||||
|
>
|
||||||
|
<div className={styles.orderHeader}>
|
||||||
|
<span className={styles.orderName}>Order #{order.id}</span>
|
||||||
|
<span
|
||||||
|
className={styles.orderStatus}
|
||||||
|
data-progress={order.progress}
|
||||||
|
>
|
||||||
|
{formatStatus(order.progress)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className={styles.orderItems}>{formatItems(order.items)}</p>
|
||||||
|
<p className={styles.orderMeta}>
|
||||||
|
{order.createdAt.toLocaleDateString([], {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
})}{' '}
|
||||||
|
· {formatPrice(order.totalCents)}
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { animate, useReducedMotion } from 'framer-motion'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
type AnimatedCounterProps = {
|
||||||
|
value: number
|
||||||
|
format?: (value: number) => string
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tweens the displayed number from its previous value to the new one,
|
||||||
|
// rather than swapping straight to it, so a price change reads as counting
|
||||||
|
// up or down. `displayValueRef` tracks the in-flight value so a change that
|
||||||
|
// interrupts a running tween continues from wherever it currently is
|
||||||
|
// instead of jumping back to the last committed value.
|
||||||
|
export const AnimatedCounter = ({
|
||||||
|
value,
|
||||||
|
format = String,
|
||||||
|
className,
|
||||||
|
}: AnimatedCounterProps) => {
|
||||||
|
const reduceMotion = useReducedMotion()
|
||||||
|
const [displayValue, setDisplayValue] = useState(value)
|
||||||
|
const displayValueRef = useRef(value)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (reduceMotion) {
|
||||||
|
displayValueRef.current = value
|
||||||
|
setDisplayValue(value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const controls = animate(displayValueRef.current, value, {
|
||||||
|
duration: 0.5,
|
||||||
|
ease: 'easeOut',
|
||||||
|
onUpdate: (latest) => {
|
||||||
|
displayValueRef.current = latest
|
||||||
|
setDisplayValue(latest)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => controls.stop()
|
||||||
|
}, [value, reduceMotion])
|
||||||
|
|
||||||
|
return <span className={className}>{format(Math.round(displayValue))}</span>
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
.wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: inline-grid;
|
||||||
|
justify-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.digit {
|
||||||
|
grid-area: 1 / 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { AnimatePresence, motion, useReducedMotion } from 'framer-motion'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
import styles from './AnimatedNumber.module.scss'
|
||||||
|
|
||||||
|
type AnimatedNumberProps = {
|
||||||
|
value: number
|
||||||
|
format?: (value: number) => string
|
||||||
|
align?: 'center' | 'start'
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const variants = {
|
||||||
|
initial: (direction: number) => ({ x: direction * 12, opacity: 0 }),
|
||||||
|
animate: { x: 0, opacity: 1 },
|
||||||
|
exit: (direction: number) => ({ x: direction * -12, opacity: 0 }),
|
||||||
|
}
|
||||||
|
|
||||||
|
// A quantity stepper's count slides leftward when incrementing (the new
|
||||||
|
// digit enters from the right, the old one exits to the left) and
|
||||||
|
// rightward when decrementing.
|
||||||
|
//
|
||||||
|
// `direction` is derived from a prop change during render (the React-docs
|
||||||
|
// "adjusting state when a prop changes" pattern) rather than mutating a ref
|
||||||
|
// in the render body — mutating a ref here would already reflect the new
|
||||||
|
// value by the time `exit`'s "custom" is resolved for the outgoing digit,
|
||||||
|
// so every transition — increment or decrement — read as "forward".
|
||||||
|
export const AnimatedNumber = ({
|
||||||
|
value,
|
||||||
|
format = String,
|
||||||
|
align = 'center',
|
||||||
|
className,
|
||||||
|
}: AnimatedNumberProps) => {
|
||||||
|
const reduceMotion = useReducedMotion()
|
||||||
|
const [previousValue, setPreviousValue] = useState(value)
|
||||||
|
const [direction, setDirection] = useState(1)
|
||||||
|
|
||||||
|
if (value !== previousValue) {
|
||||||
|
setDirection(value > previousValue ? 1 : -1)
|
||||||
|
setPreviousValue(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reduceMotion) {
|
||||||
|
return <span className={className}>{format(value)}</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={`${styles.wrapper} ${className ?? ''}`}
|
||||||
|
style={{ justifyItems: align }}
|
||||||
|
>
|
||||||
|
<AnimatePresence mode="popLayout" initial={false} custom={direction}>
|
||||||
|
<motion.span
|
||||||
|
key={value}
|
||||||
|
custom={direction}
|
||||||
|
variants={variants}
|
||||||
|
initial="initial"
|
||||||
|
animate="animate"
|
||||||
|
exit="exit"
|
||||||
|
transition={{ duration: 0.15, ease: 'easeOut' }}
|
||||||
|
className={styles.digit}
|
||||||
|
>
|
||||||
|
{format(value)}
|
||||||
|
</motion.span>
|
||||||
|
</AnimatePresence>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
@use '../../styles/variables' as *;
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin-top: $space-5;
|
||||||
|
padding: $space-4 $space-3;
|
||||||
|
border-top: 1px solid $color-border;
|
||||||
|
background: $color-surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
max-width: 640px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: $space-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
text-decoration: none;
|
||||||
|
color: $color-text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.links {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: $space-3;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $color-text-muted;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $color-text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyright {
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
color: $color-text-muted;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Link } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
import { Logo } from '#/components/ui/Logo.tsx'
|
||||||
|
|
||||||
|
import styles from './Footer.module.scss'
|
||||||
|
|
||||||
|
type FooterProps = {
|
||||||
|
brandName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Footer = ({ brandName }: FooterProps) => (
|
||||||
|
<footer className={styles.footer}>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<Link to="/" className={styles.brand}>
|
||||||
|
<Logo name={brandName} />
|
||||||
|
</Link>
|
||||||
|
<nav className={styles.links} aria-label="Footer">
|
||||||
|
<Link to="/">Menu</Link>
|
||||||
|
<Link to="/basket">Basket</Link>
|
||||||
|
<Link to="/orders">Your orders</Link>
|
||||||
|
<Link to="/help">Help</Link>
|
||||||
|
</nav>
|
||||||
|
<p className={styles.copyright}>
|
||||||
|
© {new Date().getFullYear()} {brandName}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
.holdButton {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: currentColor;
|
||||||
|
opacity: 0.2;
|
||||||
|
transform: scaleX(0);
|
||||||
|
transform-origin: left;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: inherit;
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import type { HTMLMotionProps } from 'framer-motion'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import type { KeyboardEvent, ReactNode } from 'react'
|
||||||
|
|
||||||
|
import styles from './HoldButton.module.scss'
|
||||||
|
|
||||||
|
type HoldButtonProps = Omit<
|
||||||
|
HTMLMotionProps<'button'>,
|
||||||
|
'onClick' | 'children' | 'type'
|
||||||
|
> & {
|
||||||
|
onHold: () => void
|
||||||
|
holdMs?: number
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const isHoldKey = (event: KeyboardEvent) =>
|
||||||
|
event.key === ' ' || event.key === 'Enter'
|
||||||
|
|
||||||
|
// Fires only after being held down for `holdMs`; letting go early, or
|
||||||
|
// sliding off the button, cancels. Space and Enter work the same way.
|
||||||
|
export const HoldButton = ({
|
||||||
|
onHold,
|
||||||
|
holdMs = 800,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
disabled,
|
||||||
|
...rest
|
||||||
|
}: HoldButtonProps) => {
|
||||||
|
const [holding, setHolding] = useState(false)
|
||||||
|
|
||||||
|
const start = () => {
|
||||||
|
if (!disabled) setHolding(true)
|
||||||
|
}
|
||||||
|
const stop = () => setHolding(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.button
|
||||||
|
{...rest}
|
||||||
|
type="button"
|
||||||
|
disabled={disabled}
|
||||||
|
className={[styles.holdButton, className].filter(Boolean).join(' ')}
|
||||||
|
onPointerDown={(event) => {
|
||||||
|
if (event.button === 0) start()
|
||||||
|
}}
|
||||||
|
onPointerUp={stop}
|
||||||
|
onPointerLeave={stop}
|
||||||
|
onPointerCancel={stop}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (!isHoldKey(event)) return
|
||||||
|
event.preventDefault()
|
||||||
|
if (!event.repeat) start()
|
||||||
|
}}
|
||||||
|
onKeyUp={(event) => {
|
||||||
|
if (isHoldKey(event)) stop()
|
||||||
|
}}
|
||||||
|
onBlur={stop}
|
||||||
|
onContextMenu={(event) => event.preventDefault()}
|
||||||
|
>
|
||||||
|
<motion.span
|
||||||
|
className={styles.fill}
|
||||||
|
aria-hidden="true"
|
||||||
|
initial={false}
|
||||||
|
animate={{ scaleX: holding ? 1 : 0 }}
|
||||||
|
transition={
|
||||||
|
holding
|
||||||
|
? { duration: holdMs / 1000, ease: 'linear' }
|
||||||
|
: { duration: 0.2, ease: 'easeOut' }
|
||||||
|
}
|
||||||
|
onAnimationComplete={() => {
|
||||||
|
if (!holding) return
|
||||||
|
setHolding(false)
|
||||||
|
onHold()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className={styles.content}>{children}</span>
|
||||||
|
</motion.button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
.logo {
|
||||||
|
font-family: 'Space Grotesk', system-ui, sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faded {
|
||||||
|
font-weight: 500;
|
||||||
|
opacity: 0.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghostIcon {
|
||||||
|
display: inline-block;
|
||||||
|
height: 0.78em;
|
||||||
|
width: 0.78em;
|
||||||
|
vertical-align: -0.05em;
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { useId } from 'react'
|
||||||
|
|
||||||
|
import styles from './Logo.module.scss'
|
||||||
|
|
||||||
|
type LogoProps = {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const GhostIcon = () => {
|
||||||
|
const eyeMaskId = useId()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className={styles.ghostIcon}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<mask id={eyeMaskId}>
|
||||||
|
<rect x="0" y="0" width="24" height="24" fill="#fff" />
|
||||||
|
<circle cx="9.5" cy="10.5" r="1.3" />
|
||||||
|
<circle cx="14.5" cy="10.5" r="1.3" />
|
||||||
|
</mask>
|
||||||
|
<path
|
||||||
|
d="M12 2C7.58 2 4 5.58 4 10v7l2.5 2.2L9 17l3 2.2 3-2.2 2.5 2.2L20 17v-7c0-4.42-3.58-8-8-8Z"
|
||||||
|
mask={`url(#${eyeMaskId})`}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The brand's first word fades toward transparent — a literal "ghost" for a
|
||||||
|
// Kitchen whose name starts with one. When it actually is "Ghost", its "o"
|
||||||
|
// is swapped for a small ghost mark instead (a differently-named deployment
|
||||||
|
// just gets the fade).
|
||||||
|
export const Logo = ({ name }: LogoProps) => {
|
||||||
|
const [first, ...rest] = name.split(' ')
|
||||||
|
const restText = rest.join(' ')
|
||||||
|
|
||||||
|
if (!restText) return <span className={styles.logo}>{name}</span>
|
||||||
|
|
||||||
|
const oIndex = first.toLowerCase().indexOf('o')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={styles.logo}>
|
||||||
|
<span className={styles.faded}>
|
||||||
|
{oIndex === -1 ? (
|
||||||
|
first
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{first.slice(0, oIndex)}
|
||||||
|
<GhostIcon />
|
||||||
|
{first.slice(oIndex + 1)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>{' '}
|
||||||
|
{restText}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
@use '../../styles/variables' as *;
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: $space-1;
|
||||||
|
max-width: 640px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: $space-5 $space-3;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
margin: 0 0 $space-3;
|
||||||
|
color: $color-text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta {
|
||||||
|
padding: $space-2 $space-4;
|
||||||
|
border-radius: $radius;
|
||||||
|
background: $color-primary;
|
||||||
|
color: $color-primary-contrast;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Link } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
import styles from './NotFound.module.scss'
|
||||||
|
|
||||||
|
export const NotFound = () => (
|
||||||
|
<div className={styles.wrapper}>
|
||||||
|
<p className={styles.title}>Page not found</p>
|
||||||
|
<p className={styles.text}>
|
||||||
|
The page you're looking for doesn't exist or may have moved.
|
||||||
|
</p>
|
||||||
|
<Link to="/" className={styles.cta}>
|
||||||
|
Browse the menu
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { drizzle } from 'drizzle-orm/node-postgres'
|
||||||
|
|
||||||
|
import * as schema from './schema.ts'
|
||||||
|
|
||||||
|
export const db = drizzle(process.env.DATABASE_URL!, { schema })
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
import {
|
||||||
|
boolean,
|
||||||
|
integer,
|
||||||
|
pgTable,
|
||||||
|
serial,
|
||||||
|
text,
|
||||||
|
timestamp,
|
||||||
|
uniqueIndex,
|
||||||
|
} from 'drizzle-orm/pg-core'
|
||||||
|
|
||||||
|
// A Kitchen runs one Brand. Since each Kitchen is its own independent
|
||||||
|
// deployment (see docs/adr/0001-one-server-per-kitchen.md), this table
|
||||||
|
// only ever holds a single row, seeded per deployment.
|
||||||
|
export const brands = pgTable('brands', {
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
name: text().notNull(),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// A capability label a Machine can carry, e.g. "oven", "grill", "assembly".
|
||||||
|
export const machineTags = pgTable(
|
||||||
|
'machine_tags',
|
||||||
|
{
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
name: text().notNull(),
|
||||||
|
},
|
||||||
|
(table) => [uniqueIndex('machine_tags_name_idx').on(table.name)],
|
||||||
|
)
|
||||||
|
|
||||||
|
// A physical cooking hardware unit. MVP simplification: one Tag per
|
||||||
|
// Machine (a Kitchen adds more Machines of the same Tag for parallelism,
|
||||||
|
// see docs/adr/0003-machine-tag-pool-dynamic-assignment.md).
|
||||||
|
export const machines = pgTable('machines', {
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
name: text().notNull(),
|
||||||
|
machineTagId: integer('machine_tag_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => machineTags.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const menuItems = pgTable('menu_items', {
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
brandId: integer('brand_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => brands.id),
|
||||||
|
name: text().notNull(),
|
||||||
|
description: text(),
|
||||||
|
priceCents: integer('price_cents').notNull(),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// A MenuItem's Recipe: its ordered list of required Preparation Steps.
|
||||||
|
export const menuItemSteps = pgTable('menu_item_steps', {
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
menuItemId: integer('menu_item_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => menuItems.id),
|
||||||
|
sequence: integer().notNull(),
|
||||||
|
// Customer-facing label, e.g. "Flattening dough" — distinct from the
|
||||||
|
// Machine Tag because several Steps in a Recipe can share one Tag (and
|
||||||
|
// so the same physical Machine) while doing different work on it.
|
||||||
|
name: text().notNull(),
|
||||||
|
machineTagId: integer('machine_tag_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => machineTags.id),
|
||||||
|
estimatedDurationSeconds: integer('estimated_duration_seconds').notNull(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// A customization a customer can toggle onto a Menu Item at order time,
|
||||||
|
// e.g. "Extra cheese" (+150 cents) or "No onions" (no price change).
|
||||||
|
export const menuItemOptions = pgTable('menu_item_options', {
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
menuItemId: integer('menu_item_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => menuItems.id),
|
||||||
|
name: text().notNull(),
|
||||||
|
priceDeltaCents: integer('price_delta_cents').notNull().default(0),
|
||||||
|
})
|
||||||
|
|
||||||
|
// A shared catalog of allergens (e.g. "Gluten", "Dairy") that Menu Items
|
||||||
|
// draw from — unlike menuItemOptions, this list is reused across items
|
||||||
|
// rather than owned by any one of them.
|
||||||
|
export const allergens = pgTable(
|
||||||
|
'allergens',
|
||||||
|
{
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
name: text().notNull(),
|
||||||
|
},
|
||||||
|
(table) => [uniqueIndex('allergens_name_idx').on(table.name)],
|
||||||
|
)
|
||||||
|
|
||||||
|
// Which Allergens a Menu Item's recipe contains, as ordered by an operator.
|
||||||
|
export const menuItemAllergens = pgTable(
|
||||||
|
'menu_item_allergens',
|
||||||
|
{
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
menuItemId: integer('menu_item_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => menuItems.id),
|
||||||
|
allergenId: integer('allergen_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => allergens.id),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
uniqueIndex('menu_item_allergens_item_allergen_idx').on(
|
||||||
|
table.menuItemId,
|
||||||
|
table.allergenId,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
// A locker the customer collects a ready Order Item from, identified to them
|
||||||
|
// by the number on its door.
|
||||||
|
export const pickupBoxes = pgTable('pickup_boxes', {
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
number: integer().notNull().unique(),
|
||||||
|
isOpen: boolean('is_open').notNull().default(false),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const orderStatusValues = ['placed', 'paid', 'complete'] as const
|
||||||
|
export type OrderStatus = (typeof orderStatusValues)[number]
|
||||||
|
|
||||||
|
export const orders = pgTable('orders', {
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
// References better-auth's `user.id` for an account order; null for a guest order.
|
||||||
|
customerId: text('customer_id'),
|
||||||
|
guestName: text('guest_name'),
|
||||||
|
guestContact: text('guest_contact'),
|
||||||
|
// Bearer token handed to a guest at placement time so they can prove
|
||||||
|
// ownership of the Order on later calls (there's no session to check
|
||||||
|
// against for a guest). Null for an account Order.
|
||||||
|
guestAccessToken: text('guest_access_token'),
|
||||||
|
status: text().$type<OrderStatus>().notNull().default('placed'),
|
||||||
|
paymentChargeId: text('payment_charge_id'),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const orderItemStatusValues = [
|
||||||
|
'placed',
|
||||||
|
'paid',
|
||||||
|
'ready',
|
||||||
|
'picked_up',
|
||||||
|
'cancelled',
|
||||||
|
// Left uncollected in its Pickup Box past the pickup window; not refunded.
|
||||||
|
'discarded',
|
||||||
|
] as const
|
||||||
|
export type OrderItemStatus = (typeof orderItemStatusValues)[number]
|
||||||
|
|
||||||
|
export const orderItemCancelledByValues = ['customer', 'kitchen'] as const
|
||||||
|
export type OrderItemCancelledBy = (typeof orderItemCancelledByValues)[number]
|
||||||
|
|
||||||
|
export const orderItems = pgTable('order_items', {
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
orderId: integer('order_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => orders.id),
|
||||||
|
menuItemId: integer('menu_item_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => menuItems.id),
|
||||||
|
// Captured at order time so later menu price changes don't rewrite history.
|
||||||
|
priceCents: integer('price_cents').notNull(),
|
||||||
|
status: text().$type<OrderItemStatus>().notNull().default('placed'),
|
||||||
|
// Only set once status is 'cancelled'.
|
||||||
|
cancelledBy: text('cancelled_by').$type<OrderItemCancelledBy>(),
|
||||||
|
// Set once the item is ready and a Pickup Box is free; kept after pickup.
|
||||||
|
pickupBoxId: integer('pickup_box_id').references(() => pickupBoxes.id),
|
||||||
|
// When the item went into its box, which starts the pickup window.
|
||||||
|
placedInBoxAt: timestamp('placed_in_box_at'),
|
||||||
|
pickedUpAt: timestamp('picked_up_at'),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// A frozen copy of a Menu Item's Recipe as it existed when a given Order
|
||||||
|
// Item was placed. A later edit to the live Recipe (menuItemSteps) never
|
||||||
|
// reshapes an Order Item already in flight — see
|
||||||
|
// docs/adr/0005-recipe-snapshot-per-order-item.md.
|
||||||
|
export const orderItemSteps = pgTable('order_item_steps', {
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
orderItemId: integer('order_item_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => orderItems.id),
|
||||||
|
sequence: integer().notNull(),
|
||||||
|
name: text().notNull(),
|
||||||
|
machineTagId: integer('machine_tag_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => machineTags.id),
|
||||||
|
estimatedDurationSeconds: integer('estimated_duration_seconds').notNull(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// A frozen copy of the Menu Item Options selected for a given Order Item —
|
||||||
|
// same reasoning as orderItemSteps: a later edit to the live option catalog
|
||||||
|
// (menuItemOptions) never rewrites an Order already placed.
|
||||||
|
export const orderItemOptions = pgTable('order_item_options', {
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
orderItemId: integer('order_item_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => orderItems.id),
|
||||||
|
name: text().notNull(),
|
||||||
|
priceDeltaCents: integer('price_delta_cents').notNull(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const preparationStepStatusValues = [
|
||||||
|
'queued',
|
||||||
|
'in_progress',
|
||||||
|
'done',
|
||||||
|
] as const
|
||||||
|
export type PreparationStepStatus = (typeof preparationStepStatusValues)[number]
|
||||||
|
|
||||||
|
export const preparationStepInstances = pgTable('preparation_step_instances', {
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
orderItemId: integer('order_item_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => orderItems.id),
|
||||||
|
sequence: integer().notNull(),
|
||||||
|
machineTagId: integer('machine_tag_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => machineTags.id),
|
||||||
|
estimatedDurationSeconds: integer('estimated_duration_seconds').notNull(),
|
||||||
|
// Extra time a Machine reported on top of the estimate after claiming it.
|
||||||
|
delaySeconds: integer('delay_seconds').notNull().default(0),
|
||||||
|
status: text().$type<PreparationStepStatus>().notNull().default('queued'),
|
||||||
|
machineId: integer('machine_id').references(() => machines.id),
|
||||||
|
startedAt: timestamp('started_at'),
|
||||||
|
doneAt: timestamp('done_at'),
|
||||||
|
})
|
||||||
|
|
||||||
|
// A message submitted through the Help page's contact form.
|
||||||
|
export const contactMessages = pgTable('contact_messages', {
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
name: text().notNull(),
|
||||||
|
email: text().notNull(),
|
||||||
|
message: text().notNull(),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
})
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { sql } from 'drizzle-orm'
|
||||||
|
import type { NodePgDatabase } from 'drizzle-orm/node-postgres'
|
||||||
|
|
||||||
|
import type * as schema from '#/db/schema.ts'
|
||||||
|
import type { auth as Auth } from '#/lib/auth.ts'
|
||||||
|
|
||||||
|
// Drizzle types a raw `.execute()`'s rows as always present, even though
|
||||||
|
// zero rows is a real possibility at runtime (without
|
||||||
|
// noUncheckedIndexedAccess) — mirrors the firstRow helper in
|
||||||
|
// domain/ordering.ts.
|
||||||
|
const firstRow = <T>(rows: Array<T>): T | undefined => rows[0]
|
||||||
|
|
||||||
|
// The `role` field is `input: false` in the Better Auth config (see
|
||||||
|
// src/lib/auth.ts) so it can't be set through sign-up itself — an admin
|
||||||
|
// account is always a normal sign-up followed by a direct DB update here,
|
||||||
|
// never something the sign-up API can be asked to do.
|
||||||
|
export const seedAdmin = async (
|
||||||
|
db: NodePgDatabase<typeof schema>,
|
||||||
|
auth: typeof Auth,
|
||||||
|
email: string,
|
||||||
|
password: string,
|
||||||
|
name = 'Admin',
|
||||||
|
) => {
|
||||||
|
let userId: string
|
||||||
|
try {
|
||||||
|
const signUp = await auth.api.signUpEmail({ body: { email, password, name } })
|
||||||
|
userId = signUp.user.id
|
||||||
|
} catch {
|
||||||
|
// Already signed up (e.g. this script has run before) — just make sure
|
||||||
|
// the existing account has the admin role.
|
||||||
|
const existing = await db.execute<{ id: string }>(
|
||||||
|
sql`select id from "user" where email = ${email}`,
|
||||||
|
)
|
||||||
|
const user = firstRow(existing.rows)
|
||||||
|
if (!user) throw new Error(`Sign-up failed and no existing user found for ${email}`)
|
||||||
|
userId = user.id
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.execute(sql`update "user" set role = 'admin' where id = ${userId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
|
const { config } = await import('dotenv')
|
||||||
|
config({ path: ['.env.local', '.env'] })
|
||||||
|
|
||||||
|
const email = process.env.ADMIN_EMAIL
|
||||||
|
const password = process.env.ADMIN_PASSWORD
|
||||||
|
if (!email || !password) {
|
||||||
|
console.error(
|
||||||
|
'Set ADMIN_EMAIL and ADMIN_PASSWORD (in .env.local or the environment) before running this script.',
|
||||||
|
)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Imported dynamically, after dotenv config runs, since both #/db/index.ts
|
||||||
|
// and #/lib/auth.ts read DATABASE_URL from process.env at module load time.
|
||||||
|
const { drizzle } = await import('drizzle-orm/node-postgres')
|
||||||
|
const schemaModule = await import('#/db/schema.ts')
|
||||||
|
const { auth } = await import('#/lib/auth.ts')
|
||||||
|
const db = drizzle(process.env.DATABASE_URL!, { schema: schemaModule })
|
||||||
|
|
||||||
|
await seedAdmin(db, auth, email, password, process.env.ADMIN_NAME)
|
||||||
|
console.log(`Created admin account for ${email}.`)
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
+201
@@ -0,0 +1,201 @@
|
|||||||
|
import type { NodePgDatabase } from 'drizzle-orm/node-postgres'
|
||||||
|
|
||||||
|
import * as schema from '#/db/schema.ts'
|
||||||
|
|
||||||
|
// Sample Brand/menu/hardware data for local dev and as the shared fixture
|
||||||
|
// tests seed against. Specific menu items are explicitly out of scope for
|
||||||
|
// the ordering spec — Pizza/Burger here are illustrative, not final content.
|
||||||
|
export const seedGhostKitchen = async (db: NodePgDatabase<typeof schema>) => {
|
||||||
|
const [brand] = await db
|
||||||
|
.insert(schema.brands)
|
||||||
|
.values({ name: 'Ghost Kitchen' })
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
const [prepTag] = await db
|
||||||
|
.insert(schema.machineTags)
|
||||||
|
.values({ name: 'prep' })
|
||||||
|
.returning()
|
||||||
|
const [ovenTag] = await db
|
||||||
|
.insert(schema.machineTags)
|
||||||
|
.values({ name: 'oven' })
|
||||||
|
.returning()
|
||||||
|
const [grillTag] = await db
|
||||||
|
.insert(schema.machineTags)
|
||||||
|
.values({ name: 'grill' })
|
||||||
|
.returning()
|
||||||
|
const [assemblyTag] = await db
|
||||||
|
.insert(schema.machineTags)
|
||||||
|
.values({ name: 'assembly' })
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
const [prep1] = await db
|
||||||
|
.insert(schema.machines)
|
||||||
|
.values({ name: 'Prep Station 1', machineTagId: prepTag.id })
|
||||||
|
.returning()
|
||||||
|
const [oven1] = await db
|
||||||
|
.insert(schema.machines)
|
||||||
|
.values({ name: 'Oven 1', machineTagId: ovenTag.id })
|
||||||
|
.returning()
|
||||||
|
const [grill1] = await db
|
||||||
|
.insert(schema.machines)
|
||||||
|
.values({ name: 'Grill 1', machineTagId: grillTag.id })
|
||||||
|
.returning()
|
||||||
|
const [assembly1] = await db
|
||||||
|
.insert(schema.machines)
|
||||||
|
.values({ name: 'Assembly Station 1', machineTagId: assemblyTag.id })
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
const pickupBoxes = await db
|
||||||
|
.insert(schema.pickupBoxes)
|
||||||
|
.values([1, 2, 3, 4].map((number) => ({ number })))
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
const [pizza] = await db
|
||||||
|
.insert(schema.menuItems)
|
||||||
|
.values({
|
||||||
|
brandId: brand.id,
|
||||||
|
name: 'Pizza',
|
||||||
|
description: 'Classic oven-baked pizza.',
|
||||||
|
priceCents: 1299,
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
await db.insert(schema.menuItemSteps).values([
|
||||||
|
{
|
||||||
|
menuItemId: pizza.id,
|
||||||
|
sequence: 1,
|
||||||
|
name: 'Flattening dough',
|
||||||
|
machineTagId: prepTag.id,
|
||||||
|
estimatedDurationSeconds: 30,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
menuItemId: pizza.id,
|
||||||
|
sequence: 2,
|
||||||
|
name: 'Spreading sauce',
|
||||||
|
machineTagId: prepTag.id,
|
||||||
|
estimatedDurationSeconds: 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
menuItemId: pizza.id,
|
||||||
|
sequence: 3,
|
||||||
|
name: 'Adding toppings',
|
||||||
|
machineTagId: prepTag.id,
|
||||||
|
estimatedDurationSeconds: 30,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
menuItemId: pizza.id,
|
||||||
|
sequence: 4,
|
||||||
|
name: 'Baking in the oven',
|
||||||
|
machineTagId: ovenTag.id,
|
||||||
|
estimatedDurationSeconds: 600,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
menuItemId: pizza.id,
|
||||||
|
sequence: 5,
|
||||||
|
name: 'Slicing',
|
||||||
|
machineTagId: assemblyTag.id,
|
||||||
|
estimatedDurationSeconds: 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
menuItemId: pizza.id,
|
||||||
|
sequence: 6,
|
||||||
|
name: 'Packaging',
|
||||||
|
machineTagId: assemblyTag.id,
|
||||||
|
estimatedDurationSeconds: 20,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const pizzaOptions = await db
|
||||||
|
.insert(schema.menuItemOptions)
|
||||||
|
.values([
|
||||||
|
{ menuItemId: pizza.id, name: 'Extra cheese', priceDeltaCents: 150 },
|
||||||
|
{ menuItemId: pizza.id, name: 'No onions', priceDeltaCents: 0 },
|
||||||
|
{ menuItemId: pizza.id, name: 'Extra sauce', priceDeltaCents: 0 },
|
||||||
|
])
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
const [burger] = await db
|
||||||
|
.insert(schema.menuItems)
|
||||||
|
.values({
|
||||||
|
brandId: brand.id,
|
||||||
|
name: 'Burger',
|
||||||
|
description: 'Grilled burger, assembled to order.',
|
||||||
|
priceCents: 999,
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
await db.insert(schema.menuItemSteps).values([
|
||||||
|
{
|
||||||
|
menuItemId: burger.id,
|
||||||
|
sequence: 1,
|
||||||
|
name: 'Grilling patty',
|
||||||
|
machineTagId: grillTag.id,
|
||||||
|
estimatedDurationSeconds: 240,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
menuItemId: burger.id,
|
||||||
|
sequence: 2,
|
||||||
|
name: 'Assembling burger',
|
||||||
|
machineTagId: assemblyTag.id,
|
||||||
|
estimatedDurationSeconds: 90,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const burgerOptions = await db
|
||||||
|
.insert(schema.menuItemOptions)
|
||||||
|
.values([
|
||||||
|
{ menuItemId: burger.id, name: 'Extra cheese', priceDeltaCents: 100 },
|
||||||
|
{ menuItemId: burger.id, name: 'No pickles', priceDeltaCents: 0 },
|
||||||
|
{ menuItemId: burger.id, name: 'Extra patty', priceDeltaCents: 250 },
|
||||||
|
])
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
const allergens = await db
|
||||||
|
.insert(schema.allergens)
|
||||||
|
.values([
|
||||||
|
{ name: 'Gluten' },
|
||||||
|
{ name: 'Dairy' },
|
||||||
|
{ name: 'Eggs' },
|
||||||
|
{ name: 'Soy' },
|
||||||
|
{ name: 'Peanuts' },
|
||||||
|
{ name: 'Tree nuts' },
|
||||||
|
{ name: 'Sesame' },
|
||||||
|
])
|
||||||
|
.returning()
|
||||||
|
const allergenByName = Object.fromEntries(
|
||||||
|
allergens.map((allergen) => [allergen.name, allergen]),
|
||||||
|
)
|
||||||
|
|
||||||
|
await db.insert(schema.menuItemAllergens).values([
|
||||||
|
{ menuItemId: pizza.id, allergenId: allergenByName['Gluten'].id },
|
||||||
|
{ menuItemId: pizza.id, allergenId: allergenByName['Dairy'].id },
|
||||||
|
{ menuItemId: burger.id, allergenId: allergenByName['Gluten'].id },
|
||||||
|
{ menuItemId: burger.id, allergenId: allergenByName['Sesame'].id },
|
||||||
|
])
|
||||||
|
|
||||||
|
return {
|
||||||
|
brand,
|
||||||
|
machineTags: {
|
||||||
|
prep: prepTag,
|
||||||
|
oven: ovenTag,
|
||||||
|
grill: grillTag,
|
||||||
|
assembly: assemblyTag,
|
||||||
|
},
|
||||||
|
machines: { prep1, oven1, grill1, assembly1 },
|
||||||
|
pickupBoxes,
|
||||||
|
menuItems: { pizza, burger },
|
||||||
|
menuItemOptions: { pizza: pizzaOptions, burger: burgerOptions },
|
||||||
|
allergens,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
|
const { config } = await import('dotenv')
|
||||||
|
config({ path: ['.env.local', '.env'] })
|
||||||
|
|
||||||
|
const { drizzle } = await import('drizzle-orm/node-postgres')
|
||||||
|
const db = drizzle(process.env.DATABASE_URL!, { schema })
|
||||||
|
await seedGhostKitchen(db)
|
||||||
|
console.log('Seeded Ghost Kitchen sample data.')
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import { and, eq, sql } from 'drizzle-orm'
|
||||||
|
import type { NodePgDatabase } from 'drizzle-orm/node-postgres'
|
||||||
|
|
||||||
|
import * as schema from '#/db/schema.ts'
|
||||||
|
|
||||||
|
type Db = NodePgDatabase<typeof schema>
|
||||||
|
|
||||||
|
export type MachineTagOption = { id: number; name: string }
|
||||||
|
|
||||||
|
export const getMachineTags = async (
|
||||||
|
db: Db,
|
||||||
|
): Promise<Array<MachineTagOption>> =>
|
||||||
|
db.select({ id: schema.machineTags.id, name: schema.machineTags.name }).from(
|
||||||
|
schema.machineTags,
|
||||||
|
)
|
||||||
|
|
||||||
|
export type MachineStatus = {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
tagName: string
|
||||||
|
status: 'busy' | 'idle'
|
||||||
|
current: {
|
||||||
|
stepName: string
|
||||||
|
menuItemName: string
|
||||||
|
orderId: number
|
||||||
|
startedAt: Date | null
|
||||||
|
} | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TagQueueSummary = {
|
||||||
|
tagName: string
|
||||||
|
idleMachineCount: number
|
||||||
|
busyMachineCount: number
|
||||||
|
queuedStepCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type KitchenStatus = {
|
||||||
|
machines: Array<MachineStatus>
|
||||||
|
queues: Array<TagQueueSummary>
|
||||||
|
}
|
||||||
|
|
||||||
|
// A snapshot of every Machine's current work, and how many Preparation
|
||||||
|
// Steps are still waiting per Tag — the admin "Kitchen" page's whole model.
|
||||||
|
// Read-only: nothing here mutates state (see assignQueuedStepsForTag in
|
||||||
|
// ordering.ts for how a Machine actually picks up a queued Step).
|
||||||
|
export const getKitchenStatus = async (db: Db): Promise<KitchenStatus> => {
|
||||||
|
const machines = await db
|
||||||
|
.select({
|
||||||
|
id: schema.machines.id,
|
||||||
|
name: schema.machines.name,
|
||||||
|
tagId: schema.machineTags.id,
|
||||||
|
tagName: schema.machineTags.name,
|
||||||
|
})
|
||||||
|
.from(schema.machines)
|
||||||
|
.innerJoin(
|
||||||
|
schema.machineTags,
|
||||||
|
eq(schema.machines.machineTagId, schema.machineTags.id),
|
||||||
|
)
|
||||||
|
.orderBy(schema.machineTags.name, schema.machines.name)
|
||||||
|
|
||||||
|
const busyInstances = await db
|
||||||
|
.select({
|
||||||
|
machineId: schema.preparationStepInstances.machineId,
|
||||||
|
startedAt: schema.preparationStepInstances.startedAt,
|
||||||
|
stepName: schema.orderItemSteps.name,
|
||||||
|
orderId: schema.orders.id,
|
||||||
|
menuItemName: schema.menuItems.name,
|
||||||
|
})
|
||||||
|
.from(schema.preparationStepInstances)
|
||||||
|
.innerJoin(
|
||||||
|
schema.orderItems,
|
||||||
|
eq(schema.preparationStepInstances.orderItemId, schema.orderItems.id),
|
||||||
|
)
|
||||||
|
.innerJoin(schema.orders, eq(schema.orderItems.orderId, schema.orders.id))
|
||||||
|
.innerJoin(
|
||||||
|
schema.menuItems,
|
||||||
|
eq(schema.orderItems.menuItemId, schema.menuItems.id),
|
||||||
|
)
|
||||||
|
.innerJoin(
|
||||||
|
schema.orderItemSteps,
|
||||||
|
and(
|
||||||
|
eq(
|
||||||
|
schema.orderItemSteps.orderItemId,
|
||||||
|
schema.preparationStepInstances.orderItemId,
|
||||||
|
),
|
||||||
|
eq(
|
||||||
|
schema.orderItemSteps.sequence,
|
||||||
|
schema.preparationStepInstances.sequence,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.where(eq(schema.preparationStepInstances.status, 'in_progress'))
|
||||||
|
|
||||||
|
const busyByMachineId = new Map(
|
||||||
|
busyInstances
|
||||||
|
.filter((row) => row.machineId !== null)
|
||||||
|
.map((row) => [row.machineId, row]),
|
||||||
|
)
|
||||||
|
|
||||||
|
const machineStatuses: Array<MachineStatus> = machines.map((machine) => {
|
||||||
|
const busy = busyByMachineId.get(machine.id)
|
||||||
|
return {
|
||||||
|
id: machine.id,
|
||||||
|
name: machine.name,
|
||||||
|
tagName: machine.tagName,
|
||||||
|
status: busy ? 'busy' : 'idle',
|
||||||
|
current: busy
|
||||||
|
? {
|
||||||
|
stepName: busy.stepName,
|
||||||
|
menuItemName: busy.menuItemName,
|
||||||
|
orderId: busy.orderId,
|
||||||
|
startedAt: busy.startedAt,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const queuedCounts = await db
|
||||||
|
.select({
|
||||||
|
machineTagId: schema.preparationStepInstances.machineTagId,
|
||||||
|
count: sql<number>`count(*)::int`,
|
||||||
|
})
|
||||||
|
.from(schema.preparationStepInstances)
|
||||||
|
.where(eq(schema.preparationStepInstances.status, 'queued'))
|
||||||
|
.groupBy(schema.preparationStepInstances.machineTagId)
|
||||||
|
|
||||||
|
const queuedCountByTagId = new Map(
|
||||||
|
queuedCounts.map((row) => [row.machineTagId, row.count]),
|
||||||
|
)
|
||||||
|
|
||||||
|
const tagsById = new Map(
|
||||||
|
machines.map((machine) => [machine.tagId, machine.tagName]),
|
||||||
|
)
|
||||||
|
|
||||||
|
const queues: Array<TagQueueSummary> = [...tagsById.entries()].map(
|
||||||
|
([tagId, tagName]) => {
|
||||||
|
const tagMachines = machineStatuses.filter((m) => m.tagName === tagName)
|
||||||
|
return {
|
||||||
|
tagName,
|
||||||
|
idleMachineCount: tagMachines.filter((m) => m.status === 'idle')
|
||||||
|
.length,
|
||||||
|
busyMachineCount: tagMachines.filter((m) => m.status === 'busy')
|
||||||
|
.length,
|
||||||
|
queuedStepCount: queuedCountByTagId.get(tagId) ?? 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return { machines: machineStatuses, queues }
|
||||||
|
}
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
import { eq } from 'drizzle-orm'
|
||||||
|
import type { NodePgDatabase } from 'drizzle-orm/node-postgres'
|
||||||
|
|
||||||
|
import * as schema from '#/db/schema.ts'
|
||||||
|
|
||||||
|
type Db = NodePgDatabase<typeof schema>
|
||||||
|
|
||||||
|
export class MenuItemHasOrdersError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super('This item cannot be deleted because it has past Orders')
|
||||||
|
this.name = 'MenuItemHasOrdersError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AdminMenuItem = {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
description: string | null
|
||||||
|
priceCents: number
|
||||||
|
options: Array<{ id: number; name: string; priceDeltaCents: number }>
|
||||||
|
steps: Array<{
|
||||||
|
id: number
|
||||||
|
sequence: number
|
||||||
|
name: string
|
||||||
|
machineTagId: number
|
||||||
|
machineTagName: string
|
||||||
|
estimatedDurationSeconds: number
|
||||||
|
}>
|
||||||
|
allergens: Array<{ id: number; name: string }>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MenuItemInput = {
|
||||||
|
name: string
|
||||||
|
description?: string
|
||||||
|
priceCents: number
|
||||||
|
options: Array<{ name: string; priceDeltaCents: number }>
|
||||||
|
steps: Array<{
|
||||||
|
name: string
|
||||||
|
machineTagId: number
|
||||||
|
estimatedDurationSeconds: number
|
||||||
|
}>
|
||||||
|
allergenIds: Array<number>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AllergenOption = { id: number; name: string }
|
||||||
|
|
||||||
|
export const getAllergens = async (
|
||||||
|
db: Db,
|
||||||
|
): Promise<Array<AllergenOption>> =>
|
||||||
|
db
|
||||||
|
.select({ id: schema.allergens.id, name: schema.allergens.name })
|
||||||
|
.from(schema.allergens)
|
||||||
|
.orderBy(schema.allergens.name)
|
||||||
|
|
||||||
|
const assertHasSteps = (input: MenuItemInput) => {
|
||||||
|
if (input.steps.length === 0) {
|
||||||
|
throw new Error('A menu item needs at least one Preparation Step')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMenuItemsForAdmin = async (
|
||||||
|
db: Db,
|
||||||
|
brandId: number,
|
||||||
|
): Promise<Array<AdminMenuItem>> => {
|
||||||
|
const items = await db
|
||||||
|
.select()
|
||||||
|
.from(schema.menuItems)
|
||||||
|
.where(eq(schema.menuItems.brandId, brandId))
|
||||||
|
.orderBy(schema.menuItems.name)
|
||||||
|
|
||||||
|
const options = await db.select().from(schema.menuItemOptions)
|
||||||
|
const steps = await db
|
||||||
|
.select({
|
||||||
|
id: schema.menuItemSteps.id,
|
||||||
|
menuItemId: schema.menuItemSteps.menuItemId,
|
||||||
|
sequence: schema.menuItemSteps.sequence,
|
||||||
|
name: schema.menuItemSteps.name,
|
||||||
|
machineTagId: schema.menuItemSteps.machineTagId,
|
||||||
|
machineTagName: schema.machineTags.name,
|
||||||
|
estimatedDurationSeconds: schema.menuItemSteps.estimatedDurationSeconds,
|
||||||
|
})
|
||||||
|
.from(schema.menuItemSteps)
|
||||||
|
.innerJoin(
|
||||||
|
schema.machineTags,
|
||||||
|
eq(schema.menuItemSteps.machineTagId, schema.machineTags.id),
|
||||||
|
)
|
||||||
|
.orderBy(schema.menuItemSteps.sequence)
|
||||||
|
|
||||||
|
const itemAllergens = await db
|
||||||
|
.select({
|
||||||
|
menuItemId: schema.menuItemAllergens.menuItemId,
|
||||||
|
id: schema.allergens.id,
|
||||||
|
name: schema.allergens.name,
|
||||||
|
})
|
||||||
|
.from(schema.menuItemAllergens)
|
||||||
|
.innerJoin(
|
||||||
|
schema.allergens,
|
||||||
|
eq(schema.menuItemAllergens.allergenId, schema.allergens.id),
|
||||||
|
)
|
||||||
|
|
||||||
|
return items.map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
description: item.description,
|
||||||
|
priceCents: item.priceCents,
|
||||||
|
options: options
|
||||||
|
.filter((option) => option.menuItemId === item.id)
|
||||||
|
.map((option) => ({
|
||||||
|
id: option.id,
|
||||||
|
name: option.name,
|
||||||
|
priceDeltaCents: option.priceDeltaCents,
|
||||||
|
})),
|
||||||
|
steps: steps
|
||||||
|
.filter((step) => step.menuItemId === item.id)
|
||||||
|
.map(({ menuItemId: _menuItemId, ...step }) => step),
|
||||||
|
allergens: itemAllergens
|
||||||
|
.filter((allergen) => allergen.menuItemId === item.id)
|
||||||
|
.map(({ id, name }) => ({ id, name })),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createMenuItem = async (
|
||||||
|
db: Db,
|
||||||
|
brandId: number,
|
||||||
|
input: MenuItemInput,
|
||||||
|
): Promise<number> => {
|
||||||
|
assertHasSteps(input)
|
||||||
|
|
||||||
|
return db.transaction(async (tx) => {
|
||||||
|
const [item] = await tx
|
||||||
|
.insert(schema.menuItems)
|
||||||
|
.values({
|
||||||
|
brandId,
|
||||||
|
name: input.name,
|
||||||
|
description: input.description ?? null,
|
||||||
|
priceCents: input.priceCents,
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
if (input.options.length > 0) {
|
||||||
|
await tx.insert(schema.menuItemOptions).values(
|
||||||
|
input.options.map((option) => ({
|
||||||
|
menuItemId: item.id,
|
||||||
|
name: option.name,
|
||||||
|
priceDeltaCents: option.priceDeltaCents,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx.insert(schema.menuItemSteps).values(
|
||||||
|
input.steps.map((step, index) => ({
|
||||||
|
menuItemId: item.id,
|
||||||
|
sequence: index + 1,
|
||||||
|
name: step.name,
|
||||||
|
machineTagId: step.machineTagId,
|
||||||
|
estimatedDurationSeconds: step.estimatedDurationSeconds,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (input.allergenIds.length > 0) {
|
||||||
|
await tx.insert(schema.menuItemAllergens).values(
|
||||||
|
input.allergenIds.map((allergenId) => ({
|
||||||
|
menuItemId: item.id,
|
||||||
|
allergenId,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return item.id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options, Steps, and Allergens are replaced wholesale on every edit rather
|
||||||
|
// than diffed — simplest correct behavior for a form that resubmits its
|
||||||
|
// full list each time, and safe because placed Order Items snapshot their
|
||||||
|
// own copy (see orderItemSteps/orderItemOptions) so this never rewrites
|
||||||
|
// order history.
|
||||||
|
export const updateMenuItem = async (
|
||||||
|
db: Db,
|
||||||
|
menuItemId: number,
|
||||||
|
input: MenuItemInput,
|
||||||
|
): Promise<void> => {
|
||||||
|
assertHasSteps(input)
|
||||||
|
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
|
await tx
|
||||||
|
.update(schema.menuItems)
|
||||||
|
.set({
|
||||||
|
name: input.name,
|
||||||
|
description: input.description ?? null,
|
||||||
|
priceCents: input.priceCents,
|
||||||
|
})
|
||||||
|
.where(eq(schema.menuItems.id, menuItemId))
|
||||||
|
|
||||||
|
await tx
|
||||||
|
.delete(schema.menuItemOptions)
|
||||||
|
.where(eq(schema.menuItemOptions.menuItemId, menuItemId))
|
||||||
|
await tx
|
||||||
|
.delete(schema.menuItemSteps)
|
||||||
|
.where(eq(schema.menuItemSteps.menuItemId, menuItemId))
|
||||||
|
await tx
|
||||||
|
.delete(schema.menuItemAllergens)
|
||||||
|
.where(eq(schema.menuItemAllergens.menuItemId, menuItemId))
|
||||||
|
|
||||||
|
if (input.options.length > 0) {
|
||||||
|
await tx.insert(schema.menuItemOptions).values(
|
||||||
|
input.options.map((option) => ({
|
||||||
|
menuItemId,
|
||||||
|
name: option.name,
|
||||||
|
priceDeltaCents: option.priceDeltaCents,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx.insert(schema.menuItemSteps).values(
|
||||||
|
input.steps.map((step, index) => ({
|
||||||
|
menuItemId,
|
||||||
|
sequence: index + 1,
|
||||||
|
name: step.name,
|
||||||
|
machineTagId: step.machineTagId,
|
||||||
|
estimatedDurationSeconds: step.estimatedDurationSeconds,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (input.allergenIds.length > 0) {
|
||||||
|
await tx.insert(schema.menuItemAllergens).values(
|
||||||
|
input.allergenIds.map((allergenId) => ({ menuItemId, allergenId })),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const isForeignKeyViolation = (error: unknown) =>
|
||||||
|
typeof error === 'object' &&
|
||||||
|
error !== null &&
|
||||||
|
'cause' in error &&
|
||||||
|
typeof error.cause === 'object' &&
|
||||||
|
error.cause !== null &&
|
||||||
|
'code' in error.cause &&
|
||||||
|
error.cause.code === '23503'
|
||||||
|
|
||||||
|
export const deleteMenuItem = async (
|
||||||
|
db: Db,
|
||||||
|
menuItemId: number,
|
||||||
|
): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
|
await tx
|
||||||
|
.delete(schema.menuItemOptions)
|
||||||
|
.where(eq(schema.menuItemOptions.menuItemId, menuItemId))
|
||||||
|
await tx
|
||||||
|
.delete(schema.menuItemSteps)
|
||||||
|
.where(eq(schema.menuItemSteps.menuItemId, menuItemId))
|
||||||
|
await tx
|
||||||
|
.delete(schema.menuItemAllergens)
|
||||||
|
.where(eq(schema.menuItemAllergens.menuItemId, menuItemId))
|
||||||
|
// No ON DELETE CASCADE from orderItems.menuItemId — this throws (and
|
||||||
|
// rolls back the deletes above) if any Order ever referenced it.
|
||||||
|
await tx.delete(schema.menuItems).where(eq(schema.menuItems.id, menuItemId))
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
if (isForeignKeyViolation(error)) throw new MenuItemHasOrdersError()
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,144 @@
|
|||||||
|
@use '../../styles/variables' as *;
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 2.25rem;
|
||||||
|
width: 2.25rem;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid $color-border;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: $color-background;
|
||||||
|
color: $color-text-muted;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $color-surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
height: 1.25rem;
|
||||||
|
width: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulse {
|
||||||
|
animation: pulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatarImage {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatarInitial {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $color-text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + #{$space-2});
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 11rem;
|
||||||
|
padding: $space-2;
|
||||||
|
border: 1px solid $color-border;
|
||||||
|
border-radius: $radius;
|
||||||
|
background: $color-background;
|
||||||
|
box-shadow: 0 8px 24px rgb(0 0 0 / 12%);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuLabel {
|
||||||
|
padding: $space-2;
|
||||||
|
border-bottom: 1px solid $color-border;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $color-text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuButton {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $space-2;
|
||||||
|
width: 100%;
|
||||||
|
padding: $space-2;
|
||||||
|
border: none;
|
||||||
|
border-radius: calc($radius - 2px);
|
||||||
|
background: none;
|
||||||
|
color: $color-text;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: left;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $color-surface;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginButton {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin: $space-1 0;
|
||||||
|
padding: $space-2;
|
||||||
|
border: none;
|
||||||
|
border-radius: calc($radius - 2px);
|
||||||
|
background: $color-primary;
|
||||||
|
color: $color-primary-contrast;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.signupLink {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: $space-1 $space-2;
|
||||||
|
color: $color-text-muted;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuIcon {
|
||||||
|
height: 1.1rem;
|
||||||
|
width: 1.1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
import { Link, useRouter, useRouterState } from '@tanstack/react-router'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
import { authClient } from '#/lib/auth-client'
|
||||||
|
import { useBasket } from '#/lib/basket'
|
||||||
|
import { clearGuestOrderRefs } from '#/lib/guestOrders'
|
||||||
|
|
||||||
|
import styles from './header-user.module.scss'
|
||||||
|
|
||||||
|
const AccountIcon = () => (
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
|
||||||
|
<circle cx="12" cy="8" r="4" strokeWidth="1.5" />
|
||||||
|
<path d="M4 20c0-4 3.5-7 8-7s8 3 8 7" strokeWidth="1.5" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const SettingsIcon = () => (
|
||||||
|
<svg
|
||||||
|
className={styles.menuIcon}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.75"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1Z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const OrdersIcon = () => (
|
||||||
|
<svg
|
||||||
|
className={styles.menuIcon}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.75"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M9 3h6l1 3H8l1-3Z" />
|
||||||
|
<path d="M4 6h16l-1.2 13.2A2 2 0 0 1 16.81 21H7.19a2 2 0 0 1-2-1.8L4 6Z" />
|
||||||
|
<path d="M9 11h6" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const AdminIcon = () => (
|
||||||
|
<svg
|
||||||
|
className={styles.menuIcon}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.75"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M12 3l7 3v6c0 4.5-3 8-7 9-4-1-7-4.5-7-9V6l7-3Z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const LogOutIcon = () => (
|
||||||
|
<svg
|
||||||
|
className={styles.menuIcon}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.75"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
||||||
|
<path d="M16 17l5-5-5-5" />
|
||||||
|
<path d="M21 12H9" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const BetterAuthHeader = () => {
|
||||||
|
const { data: session, isPending } = authClient.useSession()
|
||||||
|
const { clear: clearBasket } = useBasket()
|
||||||
|
const router = useRouter()
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [imageFailed, setImageFailed] = useState(false)
|
||||||
|
const wrapperRef = useRef<HTMLDivElement>(null)
|
||||||
|
const pathname = useRouterState({
|
||||||
|
select: (state) => state.location.pathname,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setOpen(false)
|
||||||
|
}, [pathname])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setImageFailed(false)
|
||||||
|
}, [session?.user?.image])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return
|
||||||
|
|
||||||
|
const onPointerDown = (event: PointerEvent) => {
|
||||||
|
if (!wrapperRef.current?.contains(event.target as Node)) setOpen(false)
|
||||||
|
}
|
||||||
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('pointerdown', onPointerDown)
|
||||||
|
document.addEventListener('keydown', onKeyDown)
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('pointerdown', onPointerDown)
|
||||||
|
document.removeEventListener('keydown', onKeyDown)
|
||||||
|
}
|
||||||
|
}, [open])
|
||||||
|
|
||||||
|
if (isPending) {
|
||||||
|
return <div className={`${styles.trigger} ${styles.pulse}`} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.wrapper} ref={wrapperRef}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.trigger}
|
||||||
|
onClick={() => setOpen((value) => !value)}
|
||||||
|
aria-haspopup="menu"
|
||||||
|
aria-expanded={open}
|
||||||
|
aria-label={
|
||||||
|
session?.user ? `Account menu for ${session.user.name}` : 'Account'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{session?.user ? (
|
||||||
|
session.user.image && !imageFailed ? (
|
||||||
|
<img
|
||||||
|
src={session.user.image}
|
||||||
|
alt=""
|
||||||
|
className={styles.avatarImage}
|
||||||
|
onError={() => setImageFailed(true)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className={styles.avatarInitial}>
|
||||||
|
{session.user.name.charAt(0).toUpperCase() || 'U'}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<AccountIcon />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{open && (
|
||||||
|
<div className={styles.menu} role="menu">
|
||||||
|
{session?.user ? (
|
||||||
|
<>
|
||||||
|
<p className={styles.menuLabel}>{session.user.name}</p>
|
||||||
|
<Link to="/orders" role="menuitem" className={styles.menuButton}>
|
||||||
|
<OrdersIcon />
|
||||||
|
Orders
|
||||||
|
</Link>
|
||||||
|
<Link to="/account" role="menuitem" className={styles.menuButton}>
|
||||||
|
<SettingsIcon />
|
||||||
|
Account
|
||||||
|
</Link>
|
||||||
|
{session.user.role === 'admin' && (
|
||||||
|
<Link
|
||||||
|
to="/admin"
|
||||||
|
role="menuitem"
|
||||||
|
className={styles.menuButton}
|
||||||
|
>
|
||||||
|
<AdminIcon />
|
||||||
|
Administration
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="menuitem"
|
||||||
|
className={styles.menuButton}
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(false)
|
||||||
|
clearGuestOrderRefs()
|
||||||
|
clearBasket()
|
||||||
|
void authClient.signOut({
|
||||||
|
fetchOptions: {
|
||||||
|
onSuccess: () => void router.invalidate(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LogOutIcon />
|
||||||
|
Sign out
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p className={styles.menuLabel}>Guest</p>
|
||||||
|
<Link to="/orders" role="menuitem" className={styles.menuButton}>
|
||||||
|
<OrdersIcon />
|
||||||
|
Orders
|
||||||
|
</Link>
|
||||||
|
<Link to="/login" role="menuitem" className={styles.loginButton}>
|
||||||
|
Sign in
|
||||||
|
</Link>
|
||||||
|
<Link to="/signup" role="menuitem" className={styles.signupLink}>
|
||||||
|
New here? Sign up
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BetterAuthHeader
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { redirect } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
// Errors cross the server-function boundary as a plain `Error` carrying only
|
||||||
|
// `.message` (TanStack Start's ShallowErrorPlugin drops the subclass), so
|
||||||
|
// requireAdmin's rejection is told apart by message rather than `instanceof`.
|
||||||
|
// Every /admin/* route loader wraps its data fetch in this so a signed-out
|
||||||
|
// or non-admin visitor is redirected to sign in instead of crashing.
|
||||||
|
export const withAdminGuard = async <T>(load: () => Promise<T>): Promise<T> => {
|
||||||
|
try {
|
||||||
|
return await load()
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.message === 'Admin access required') {
|
||||||
|
throw redirect({ to: '/login' })
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { createAuthClient } from 'better-auth/react'
|
||||||
|
import { inferAdditionalFields } from 'better-auth/client/plugins'
|
||||||
|
|
||||||
|
import type { auth } from '#/lib/auth.ts'
|
||||||
|
|
||||||
|
export const authClient = createAuthClient({
|
||||||
|
plugins: [inferAdditionalFields<typeof auth>()],
|
||||||
|
})
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { betterAuth } from 'better-auth'
|
||||||
|
import { tanstackStartCookies } from 'better-auth/tanstack-start'
|
||||||
|
import { Pool } from 'pg'
|
||||||
|
|
||||||
|
import { db } from '#/db/index.ts'
|
||||||
|
import { detachCustomerOrders } from '#/domain/ordering.ts'
|
||||||
|
|
||||||
|
// Each provider is only registered once its credentials are configured, so
|
||||||
|
// local dev without OAuth apps set up still works via email/password.
|
||||||
|
export const auth = betterAuth({
|
||||||
|
database: new Pool({ connectionString: process.env.DATABASE_URL }),
|
||||||
|
emailAndPassword: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
additionalFields: {
|
||||||
|
// `input: false` keeps this out of the public sign-up/update schema so
|
||||||
|
// a client can't self-assign admin — it's only ever set by a direct DB
|
||||||
|
// update (see src/db/seed-admin.ts).
|
||||||
|
role: {
|
||||||
|
type: 'string',
|
||||||
|
defaultValue: 'customer',
|
||||||
|
input: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// No email-sending set up anywhere in this app (no verification flow,
|
||||||
|
// no password-reset email either), so a changed address is accepted
|
||||||
|
// immediately rather than left pending on a confirmation link that
|
||||||
|
// could never be sent.
|
||||||
|
changeEmail: {
|
||||||
|
enabled: true,
|
||||||
|
updateEmailWithoutVerification: true,
|
||||||
|
},
|
||||||
|
deleteUser: {
|
||||||
|
enabled: true,
|
||||||
|
beforeDelete: async (user) => {
|
||||||
|
await detachCustomerOrders(db, user.id)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
socialProviders: {
|
||||||
|
...(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET
|
||||||
|
? {
|
||||||
|
google: {
|
||||||
|
clientId: process.env.GOOGLE_CLIENT_ID,
|
||||||
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
...(process.env.APPLE_CLIENT_ID && process.env.APPLE_CLIENT_SECRET
|
||||||
|
? {
|
||||||
|
apple: {
|
||||||
|
clientId: process.env.APPLE_CLIENT_ID,
|
||||||
|
clientSecret: process.env.APPLE_CLIENT_SECRET,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
plugins: [tanstackStartCookies()],
|
||||||
|
})
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'ghost-kitchen-basket'
|
||||||
|
|
||||||
|
export type BasketLine = {
|
||||||
|
menuItemId: number
|
||||||
|
optionIds: Array<number>
|
||||||
|
quantity: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type StoredLines = Partial<Record<string, BasketLine>>
|
||||||
|
|
||||||
|
// Two lines with the same Menu Item and the same set of selected Options
|
||||||
|
// are the same line (their quantities combine) — order of selection
|
||||||
|
// shouldn't matter, so the key sorts the option ids first.
|
||||||
|
const lineKey = (menuItemId: number, optionIds: Array<number>) =>
|
||||||
|
`${menuItemId}:${[...optionIds].sort((a, b) => a - b).join(',')}`
|
||||||
|
|
||||||
|
const isBasketLine = (value: unknown): value is BasketLine =>
|
||||||
|
typeof value === 'object' &&
|
||||||
|
value !== null &&
|
||||||
|
typeof (value as BasketLine).menuItemId === 'number' &&
|
||||||
|
Array.isArray((value as BasketLine).optionIds) &&
|
||||||
|
typeof (value as BasketLine).quantity === 'number'
|
||||||
|
|
||||||
|
type BasketContextValue = {
|
||||||
|
lines: Array<BasketLine>
|
||||||
|
itemCount: number
|
||||||
|
add: (menuItemId: number, optionIds?: Array<number>) => void
|
||||||
|
remove: (menuItemId: number, optionIds?: Array<number>) => void
|
||||||
|
clear: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const BasketContext = createContext<BasketContextValue | null>(null)
|
||||||
|
|
||||||
|
const readStoredLines = (): StoredLines => {
|
||||||
|
try {
|
||||||
|
const raw = window.localStorage.getItem(STORAGE_KEY)
|
||||||
|
if (!raw) return {}
|
||||||
|
const parsed: unknown = JSON.parse(raw)
|
||||||
|
if (typeof parsed !== 'object' || parsed === null) return {}
|
||||||
|
|
||||||
|
const result: StoredLines = {}
|
||||||
|
for (const [key, value] of Object.entries(parsed)) {
|
||||||
|
// Earlier versions stored a flat `Record<menuItemId, quantity>` with
|
||||||
|
// no customization — read a leftover value like that as a plain,
|
||||||
|
// option-free line rather than dropping it.
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
const menuItemId = Number(key)
|
||||||
|
if (!Number.isFinite(menuItemId) || value <= 0) continue
|
||||||
|
result[lineKey(menuItemId, [])] = {
|
||||||
|
menuItemId,
|
||||||
|
optionIds: [],
|
||||||
|
quantity: value,
|
||||||
|
}
|
||||||
|
} else if (isBasketLine(value)) {
|
||||||
|
result[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
} catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BasketProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
const [storedLines, setStoredLines] = useState<StoredLines>({})
|
||||||
|
|
||||||
|
// Read from localStorage after mount, not during initial render, so the
|
||||||
|
// server-rendered and first client-rendered HTML match (no localStorage on
|
||||||
|
// the server) and React doesn't flag a hydration mismatch.
|
||||||
|
useEffect(() => {
|
||||||
|
setStoredLines(readStoredLines())
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(storedLines))
|
||||||
|
} catch {
|
||||||
|
// Unavailable (private mode, quota) — basket still works for this
|
||||||
|
// page load, it just won't persist across a reload.
|
||||||
|
}
|
||||||
|
}, [storedLines])
|
||||||
|
|
||||||
|
const add = (menuItemId: number, optionIds: Array<number> = []) => {
|
||||||
|
const key = lineKey(menuItemId, optionIds)
|
||||||
|
setStoredLines((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[key]: {
|
||||||
|
menuItemId,
|
||||||
|
optionIds,
|
||||||
|
quantity: (prev[key]?.quantity ?? 0) + 1,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const remove = (menuItemId: number, optionIds: Array<number> = []) => {
|
||||||
|
const key = lineKey(menuItemId, optionIds)
|
||||||
|
setStoredLines((prev) => {
|
||||||
|
const current = prev[key]
|
||||||
|
if (!current) return prev
|
||||||
|
if (current.quantity <= 1) {
|
||||||
|
const next = { ...prev }
|
||||||
|
delete next[key]
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
return { ...prev, [key]: { ...current, quantity: current.quantity - 1 } }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const clear = () => setStoredLines({})
|
||||||
|
|
||||||
|
const lines = useMemo(
|
||||||
|
() => Object.values(storedLines).filter((line) => line !== undefined),
|
||||||
|
[storedLines],
|
||||||
|
)
|
||||||
|
const itemCount = useMemo(
|
||||||
|
() => lines.reduce((sum, line) => sum + line.quantity, 0),
|
||||||
|
[lines],
|
||||||
|
)
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({ lines, itemCount, add, remove, clear }),
|
||||||
|
[lines, itemCount],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BasketContext.Provider value={value}>{children}</BasketContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useBasket = () => {
|
||||||
|
const context = useContext(BasketContext)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useBasket must be used within a BasketProvider')
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user