Adds the scheduling core to Prisma (Shop, Location, Method enum, SlotTemplate, SlotOverride, BlackoutDate — IMPLEMENTATION_PLAN.md §4) and Polaris admin screens to manage them: - /app/locations: list/create/edit/delete locations, one-click vertical template seeding (bakery/florist/grocer) - /app/slots: weekly slot template editor, scoped per location - /app/blackouts: blackout dates, scoped to one location or all of them services/templates.server.ts keeps the vertical presets as a pure, unit-tested function (getVerticalTemplate) separate from the thin I/O wrapper (seedVerticalTemplate) that does the actual Prisma writes, per CLAUDE.md's "pure functions, inject data, no I/O in the math" rule. Switched dev DB from SQLite to Postgres (docker-compose.yml, ports 5433/6380 to avoid clashing with other local projects already on 5432/6379): SQLite doesn't support Prisma enums at all, and IMPLEMENTATION_PLAN.md's schema relies on them (Method) plus Postgres-only array fields in later phases (Zone.postalCodes, ProductRule.allowedLocationIds). Only one throwaway migration existed, so switching now avoids compounding the rework later. Also fixed a Remix/Polaris integration issue hit while building these forms: this Polaris version's TextField/Checkbox are fully controlled (no defaultValue/defaultChecked), and `data()` imported from `@remix-run/react` (rather than `@remix-run/node`) breaks useActionData's type inference — both are now handled correctly across the new routes. Verified: lint, typecheck, unit tests (incl. template-seeding fixtures), build, and a live end-to-end run of seedVerticalTemplate against the Postgres container all pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
120 lines
3.6 KiB
Plaintext
120 lines
3.6 KiB
Plaintext
// This is your Prisma schema file,
|
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
|
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
// Note that some adapters may set a maximum length for the String type by default, please ensure your strings are long
|
|
// enough when changing adapters.
|
|
// See https://www.prisma.io/docs/orm/reference/prisma-schema-reference#string for more information
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
model Session {
|
|
id String @id
|
|
shop String
|
|
state String
|
|
isOnline Boolean @default(false)
|
|
scope String?
|
|
expires DateTime?
|
|
accessToken String
|
|
userId BigInt?
|
|
firstName String?
|
|
lastName String?
|
|
email String?
|
|
accountOwner Boolean @default(false)
|
|
locale String?
|
|
collaborator Boolean? @default(false)
|
|
emailVerified Boolean? @default(false)
|
|
refreshToken String?
|
|
refreshTokenExpires DateTime?
|
|
}
|
|
|
|
// --- Scheduling domain (IMPLEMENTATION_PLAN.md §4) -------------------------
|
|
// Every model below is scoped by shopDomain (multi-tenant, per CLAUDE.md).
|
|
// Built incrementally per phase; see the plan for models not yet added
|
|
// (CapacityResource, Zone, ProductRule, Rate, SlotHold, Booking, ...).
|
|
|
|
enum Method {
|
|
SHIPPING
|
|
LOCAL_DELIVERY
|
|
PICKUP
|
|
}
|
|
|
|
model Shop {
|
|
id String @id @default(cuid())
|
|
shopDomain String @unique
|
|
plan String @default("basic") // basic|shopify|advanced|plus
|
|
tier String @default("free") // free|starter|growth|pro
|
|
timezone String @default("UTC")
|
|
settings Json @default("{}") // widget copy, i18n, feature flags
|
|
createdAt DateTime @default(now())
|
|
}
|
|
|
|
model Location {
|
|
id String @id @default(cuid())
|
|
shopDomain String
|
|
name String
|
|
address String
|
|
lat Float?
|
|
lng Float?
|
|
timezone String
|
|
active Boolean @default(true)
|
|
slotTemplates SlotTemplate[]
|
|
overrides SlotOverride[]
|
|
blackouts BlackoutDate[]
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([shopDomain])
|
|
}
|
|
|
|
model SlotTemplate {
|
|
id String @id @default(cuid())
|
|
shopDomain String
|
|
locationId String
|
|
location Location @relation(fields: [locationId], references: [id], onDelete: Cascade)
|
|
method Method
|
|
weekday Int // 0-6 (0 = Sunday), location-local
|
|
startMin Int // minutes from midnight, local
|
|
endMin Int
|
|
capacity Int // default order capacity if no resources
|
|
cutoffMin Int? // cutoff before slot (minutes)
|
|
leadTimeMin Int @default(0)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([shopDomain, locationId, method, weekday])
|
|
}
|
|
|
|
model SlotOverride {
|
|
id String @id @default(cuid())
|
|
shopDomain String
|
|
locationId String
|
|
location Location @relation(fields: [locationId], references: [id], onDelete: Cascade)
|
|
date DateTime // date-only, location-local
|
|
method Method
|
|
closed Boolean @default(false)
|
|
startMin Int?
|
|
endMin Int?
|
|
capacity Int?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([shopDomain, locationId, date])
|
|
}
|
|
|
|
model BlackoutDate {
|
|
id String @id @default(cuid())
|
|
shopDomain String
|
|
locationId String? // null = all locations
|
|
location Location? @relation(fields: [locationId], references: [id], onDelete: Cascade)
|
|
method Method? // null = all methods
|
|
date DateTime
|
|
reason String?
|
|
source String @default("manual") // manual|holiday-import
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([shopDomain, date])
|
|
}
|