Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd3aa45e02 |
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "restaurant_tables" ADD COLUMN "image_url" text;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "restaurant_settings" ADD COLUMN "table_display_mode" text DEFAULT 'individual' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "restaurant_settings" ADD COLUMN "table_overview_image_url" text;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "restaurant_tables" ADD COLUMN "marker_x" double precision;--> statement-breakpoint
|
||||||
|
ALTER TABLE "restaurant_tables" ADD COLUMN "marker_y" double precision;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "restaurant_tables" ADD COLUMN "table_number" integer;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "reservations" ADD COLUMN "message" text;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "reservations" ADD COLUMN "customer_phone" text;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "user" ADD COLUMN "phone" text;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "reservations" ADD COLUMN "status" text DEFAULT 'confirmed' NOT NULL;
|
||||||
@@ -0,0 +1,533 @@
|
|||||||
|
{
|
||||||
|
"id": "05eac8a1-b172-4e4d-90d2-a409ecde610a",
|
||||||
|
"prevId": "d03b9103-cb6a-4968-a016-c04270ec176a",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.account": {
|
||||||
|
"name": "account",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"account_id": {
|
||||||
|
"name": "account_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"provider_id": {
|
||||||
|
"name": "provider_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"access_token": {
|
||||||
|
"name": "access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"name": "refresh_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"id_token": {
|
||||||
|
"name": "id_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"access_token_expires_at": {
|
||||||
|
"name": "access_token_expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token_expires_at": {
|
||||||
|
"name": "refresh_token_expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"name": "scope",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"account_user_id_user_id_fk": {
|
||||||
|
"name": "account_user_id_user_id_fk",
|
||||||
|
"tableFrom": "account",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.reservations": {
|
||||||
|
"name": "reservations",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"table_id": {
|
||||||
|
"name": "table_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"customer_name": {
|
||||||
|
"name": "customer_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"customer_email": {
|
||||||
|
"name": "customer_email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"party_size": {
|
||||||
|
"name": "party_size",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"reservation_time": {
|
||||||
|
"name": "reservation_time",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"duration_minutes": {
|
||||||
|
"name": "duration_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 90
|
||||||
|
},
|
||||||
|
"cancel_token": {
|
||||||
|
"name": "cancel_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"reservations_table_id_restaurant_tables_id_fk": {
|
||||||
|
"name": "reservations_table_id_restaurant_tables_id_fk",
|
||||||
|
"tableFrom": "reservations",
|
||||||
|
"tableTo": "restaurant_tables",
|
||||||
|
"columnsFrom": [
|
||||||
|
"table_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"reservations_cancel_token_unique": {
|
||||||
|
"name": "reservations_cancel_token_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"cancel_token"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"reservations_table_id_reservation_time_unique": {
|
||||||
|
"name": "reservations_table_id_reservation_time_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"table_id",
|
||||||
|
"reservation_time"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.restaurant_settings": {
|
||||||
|
"name": "restaurant_settings",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"reservation_interval_minutes": {
|
||||||
|
"name": "reservation_interval_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 15
|
||||||
|
},
|
||||||
|
"reservation_duration_minutes": {
|
||||||
|
"name": "reservation_duration_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 90
|
||||||
|
},
|
||||||
|
"min_lead_time_minutes": {
|
||||||
|
"name": "min_lead_time_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 60
|
||||||
|
},
|
||||||
|
"max_lead_time_days": {
|
||||||
|
"name": "max_lead_time_days",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 60
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.restaurant_tables": {
|
||||||
|
"name": "restaurant_tables",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"seats": {
|
||||||
|
"name": "seats",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"image_url": {
|
||||||
|
"name": "image_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.session": {
|
||||||
|
"name": "session",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"name": "token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"ip_address": {
|
||||||
|
"name": "ip_address",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_agent": {
|
||||||
|
"name": "user_agent",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"impersonated_by": {
|
||||||
|
"name": "impersonated_by",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"session_user_id_user_id_fk": {
|
||||||
|
"name": "session_user_id_user_id_fk",
|
||||||
|
"tableFrom": "session",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"session_token_unique": {
|
||||||
|
"name": "session_token_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"token"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email_verified": {
|
||||||
|
"name": "email_verified",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"name": "image",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"banned": {
|
||||||
|
"name": "banned",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"ban_reason": {
|
||||||
|
"name": "ban_reason",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"ban_expires": {
|
||||||
|
"name": "ban_expires",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"user_email_unique": {
|
||||||
|
"name": "user_email_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.verification": {
|
||||||
|
"name": "verification",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"identifier": {
|
||||||
|
"name": "identifier",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,546 @@
|
|||||||
|
{
|
||||||
|
"id": "71bc6bc2-516c-4af1-8c61-3df7814e9976",
|
||||||
|
"prevId": "05eac8a1-b172-4e4d-90d2-a409ecde610a",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.account": {
|
||||||
|
"name": "account",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"account_id": {
|
||||||
|
"name": "account_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"provider_id": {
|
||||||
|
"name": "provider_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"access_token": {
|
||||||
|
"name": "access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"name": "refresh_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"id_token": {
|
||||||
|
"name": "id_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"access_token_expires_at": {
|
||||||
|
"name": "access_token_expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token_expires_at": {
|
||||||
|
"name": "refresh_token_expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"name": "scope",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"account_user_id_user_id_fk": {
|
||||||
|
"name": "account_user_id_user_id_fk",
|
||||||
|
"tableFrom": "account",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.reservations": {
|
||||||
|
"name": "reservations",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"table_id": {
|
||||||
|
"name": "table_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"customer_name": {
|
||||||
|
"name": "customer_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"customer_email": {
|
||||||
|
"name": "customer_email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"party_size": {
|
||||||
|
"name": "party_size",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"reservation_time": {
|
||||||
|
"name": "reservation_time",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"duration_minutes": {
|
||||||
|
"name": "duration_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 90
|
||||||
|
},
|
||||||
|
"cancel_token": {
|
||||||
|
"name": "cancel_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"reservations_table_id_restaurant_tables_id_fk": {
|
||||||
|
"name": "reservations_table_id_restaurant_tables_id_fk",
|
||||||
|
"tableFrom": "reservations",
|
||||||
|
"tableTo": "restaurant_tables",
|
||||||
|
"columnsFrom": [
|
||||||
|
"table_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"reservations_cancel_token_unique": {
|
||||||
|
"name": "reservations_cancel_token_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"cancel_token"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"reservations_table_id_reservation_time_unique": {
|
||||||
|
"name": "reservations_table_id_reservation_time_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"table_id",
|
||||||
|
"reservation_time"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.restaurant_settings": {
|
||||||
|
"name": "restaurant_settings",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"reservation_interval_minutes": {
|
||||||
|
"name": "reservation_interval_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 15
|
||||||
|
},
|
||||||
|
"reservation_duration_minutes": {
|
||||||
|
"name": "reservation_duration_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 90
|
||||||
|
},
|
||||||
|
"min_lead_time_minutes": {
|
||||||
|
"name": "min_lead_time_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 60
|
||||||
|
},
|
||||||
|
"max_lead_time_days": {
|
||||||
|
"name": "max_lead_time_days",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 60
|
||||||
|
},
|
||||||
|
"table_display_mode": {
|
||||||
|
"name": "table_display_mode",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'individual'"
|
||||||
|
},
|
||||||
|
"table_overview_image_url": {
|
||||||
|
"name": "table_overview_image_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.restaurant_tables": {
|
||||||
|
"name": "restaurant_tables",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"seats": {
|
||||||
|
"name": "seats",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"image_url": {
|
||||||
|
"name": "image_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.session": {
|
||||||
|
"name": "session",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"name": "token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"ip_address": {
|
||||||
|
"name": "ip_address",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_agent": {
|
||||||
|
"name": "user_agent",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"impersonated_by": {
|
||||||
|
"name": "impersonated_by",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"session_user_id_user_id_fk": {
|
||||||
|
"name": "session_user_id_user_id_fk",
|
||||||
|
"tableFrom": "session",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"session_token_unique": {
|
||||||
|
"name": "session_token_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"token"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email_verified": {
|
||||||
|
"name": "email_verified",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"name": "image",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"banned": {
|
||||||
|
"name": "banned",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"ban_reason": {
|
||||||
|
"name": "ban_reason",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"ban_expires": {
|
||||||
|
"name": "ban_expires",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"user_email_unique": {
|
||||||
|
"name": "user_email_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.verification": {
|
||||||
|
"name": "verification",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"identifier": {
|
||||||
|
"name": "identifier",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,558 @@
|
|||||||
|
{
|
||||||
|
"id": "cc3da65b-b162-4866-ac44-b7d177588316",
|
||||||
|
"prevId": "71bc6bc2-516c-4af1-8c61-3df7814e9976",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.account": {
|
||||||
|
"name": "account",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"account_id": {
|
||||||
|
"name": "account_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"provider_id": {
|
||||||
|
"name": "provider_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"access_token": {
|
||||||
|
"name": "access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"name": "refresh_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"id_token": {
|
||||||
|
"name": "id_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"access_token_expires_at": {
|
||||||
|
"name": "access_token_expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token_expires_at": {
|
||||||
|
"name": "refresh_token_expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"name": "scope",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"account_user_id_user_id_fk": {
|
||||||
|
"name": "account_user_id_user_id_fk",
|
||||||
|
"tableFrom": "account",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.reservations": {
|
||||||
|
"name": "reservations",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"table_id": {
|
||||||
|
"name": "table_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"customer_name": {
|
||||||
|
"name": "customer_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"customer_email": {
|
||||||
|
"name": "customer_email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"party_size": {
|
||||||
|
"name": "party_size",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"reservation_time": {
|
||||||
|
"name": "reservation_time",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"duration_minutes": {
|
||||||
|
"name": "duration_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 90
|
||||||
|
},
|
||||||
|
"cancel_token": {
|
||||||
|
"name": "cancel_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"reservations_table_id_restaurant_tables_id_fk": {
|
||||||
|
"name": "reservations_table_id_restaurant_tables_id_fk",
|
||||||
|
"tableFrom": "reservations",
|
||||||
|
"tableTo": "restaurant_tables",
|
||||||
|
"columnsFrom": [
|
||||||
|
"table_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"reservations_cancel_token_unique": {
|
||||||
|
"name": "reservations_cancel_token_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"cancel_token"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"reservations_table_id_reservation_time_unique": {
|
||||||
|
"name": "reservations_table_id_reservation_time_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"table_id",
|
||||||
|
"reservation_time"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.restaurant_settings": {
|
||||||
|
"name": "restaurant_settings",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"reservation_interval_minutes": {
|
||||||
|
"name": "reservation_interval_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 15
|
||||||
|
},
|
||||||
|
"reservation_duration_minutes": {
|
||||||
|
"name": "reservation_duration_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 90
|
||||||
|
},
|
||||||
|
"min_lead_time_minutes": {
|
||||||
|
"name": "min_lead_time_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 60
|
||||||
|
},
|
||||||
|
"max_lead_time_days": {
|
||||||
|
"name": "max_lead_time_days",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 60
|
||||||
|
},
|
||||||
|
"table_display_mode": {
|
||||||
|
"name": "table_display_mode",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'individual'"
|
||||||
|
},
|
||||||
|
"table_overview_image_url": {
|
||||||
|
"name": "table_overview_image_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.restaurant_tables": {
|
||||||
|
"name": "restaurant_tables",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"seats": {
|
||||||
|
"name": "seats",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"image_url": {
|
||||||
|
"name": "image_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"marker_x": {
|
||||||
|
"name": "marker_x",
|
||||||
|
"type": "double precision",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"marker_y": {
|
||||||
|
"name": "marker_y",
|
||||||
|
"type": "double precision",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.session": {
|
||||||
|
"name": "session",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"name": "token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"ip_address": {
|
||||||
|
"name": "ip_address",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_agent": {
|
||||||
|
"name": "user_agent",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"impersonated_by": {
|
||||||
|
"name": "impersonated_by",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"session_user_id_user_id_fk": {
|
||||||
|
"name": "session_user_id_user_id_fk",
|
||||||
|
"tableFrom": "session",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"session_token_unique": {
|
||||||
|
"name": "session_token_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"token"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email_verified": {
|
||||||
|
"name": "email_verified",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"name": "image",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"banned": {
|
||||||
|
"name": "banned",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"ban_reason": {
|
||||||
|
"name": "ban_reason",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"ban_expires": {
|
||||||
|
"name": "ban_expires",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"user_email_unique": {
|
||||||
|
"name": "user_email_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.verification": {
|
||||||
|
"name": "verification",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"identifier": {
|
||||||
|
"name": "identifier",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,564 @@
|
|||||||
|
{
|
||||||
|
"id": "0914058c-53fc-4e93-b703-a907e0ae72a8",
|
||||||
|
"prevId": "cc3da65b-b162-4866-ac44-b7d177588316",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.account": {
|
||||||
|
"name": "account",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"account_id": {
|
||||||
|
"name": "account_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"provider_id": {
|
||||||
|
"name": "provider_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"access_token": {
|
||||||
|
"name": "access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"name": "refresh_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"id_token": {
|
||||||
|
"name": "id_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"access_token_expires_at": {
|
||||||
|
"name": "access_token_expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token_expires_at": {
|
||||||
|
"name": "refresh_token_expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"name": "scope",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"account_user_id_user_id_fk": {
|
||||||
|
"name": "account_user_id_user_id_fk",
|
||||||
|
"tableFrom": "account",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.reservations": {
|
||||||
|
"name": "reservations",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"table_id": {
|
||||||
|
"name": "table_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"customer_name": {
|
||||||
|
"name": "customer_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"customer_email": {
|
||||||
|
"name": "customer_email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"party_size": {
|
||||||
|
"name": "party_size",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"reservation_time": {
|
||||||
|
"name": "reservation_time",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"duration_minutes": {
|
||||||
|
"name": "duration_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 90
|
||||||
|
},
|
||||||
|
"cancel_token": {
|
||||||
|
"name": "cancel_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"reservations_table_id_restaurant_tables_id_fk": {
|
||||||
|
"name": "reservations_table_id_restaurant_tables_id_fk",
|
||||||
|
"tableFrom": "reservations",
|
||||||
|
"tableTo": "restaurant_tables",
|
||||||
|
"columnsFrom": [
|
||||||
|
"table_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"reservations_cancel_token_unique": {
|
||||||
|
"name": "reservations_cancel_token_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"cancel_token"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"reservations_table_id_reservation_time_unique": {
|
||||||
|
"name": "reservations_table_id_reservation_time_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"table_id",
|
||||||
|
"reservation_time"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.restaurant_settings": {
|
||||||
|
"name": "restaurant_settings",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"reservation_interval_minutes": {
|
||||||
|
"name": "reservation_interval_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 15
|
||||||
|
},
|
||||||
|
"reservation_duration_minutes": {
|
||||||
|
"name": "reservation_duration_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 90
|
||||||
|
},
|
||||||
|
"min_lead_time_minutes": {
|
||||||
|
"name": "min_lead_time_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 60
|
||||||
|
},
|
||||||
|
"max_lead_time_days": {
|
||||||
|
"name": "max_lead_time_days",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 60
|
||||||
|
},
|
||||||
|
"table_display_mode": {
|
||||||
|
"name": "table_display_mode",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'individual'"
|
||||||
|
},
|
||||||
|
"table_overview_image_url": {
|
||||||
|
"name": "table_overview_image_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.restaurant_tables": {
|
||||||
|
"name": "restaurant_tables",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"seats": {
|
||||||
|
"name": "seats",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"table_number": {
|
||||||
|
"name": "table_number",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"image_url": {
|
||||||
|
"name": "image_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"marker_x": {
|
||||||
|
"name": "marker_x",
|
||||||
|
"type": "double precision",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"marker_y": {
|
||||||
|
"name": "marker_y",
|
||||||
|
"type": "double precision",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.session": {
|
||||||
|
"name": "session",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"name": "token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"ip_address": {
|
||||||
|
"name": "ip_address",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_agent": {
|
||||||
|
"name": "user_agent",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"impersonated_by": {
|
||||||
|
"name": "impersonated_by",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"session_user_id_user_id_fk": {
|
||||||
|
"name": "session_user_id_user_id_fk",
|
||||||
|
"tableFrom": "session",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"session_token_unique": {
|
||||||
|
"name": "session_token_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"token"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email_verified": {
|
||||||
|
"name": "email_verified",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"name": "image",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"banned": {
|
||||||
|
"name": "banned",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"ban_reason": {
|
||||||
|
"name": "ban_reason",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"ban_expires": {
|
||||||
|
"name": "ban_expires",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"user_email_unique": {
|
||||||
|
"name": "user_email_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.verification": {
|
||||||
|
"name": "verification",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"identifier": {
|
||||||
|
"name": "identifier",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,570 @@
|
|||||||
|
{
|
||||||
|
"id": "c95ecb29-7866-4794-9aad-8a6fba8e7bc4",
|
||||||
|
"prevId": "0914058c-53fc-4e93-b703-a907e0ae72a8",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.account": {
|
||||||
|
"name": "account",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"account_id": {
|
||||||
|
"name": "account_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"provider_id": {
|
||||||
|
"name": "provider_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"access_token": {
|
||||||
|
"name": "access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"name": "refresh_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"id_token": {
|
||||||
|
"name": "id_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"access_token_expires_at": {
|
||||||
|
"name": "access_token_expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token_expires_at": {
|
||||||
|
"name": "refresh_token_expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"name": "scope",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"account_user_id_user_id_fk": {
|
||||||
|
"name": "account_user_id_user_id_fk",
|
||||||
|
"tableFrom": "account",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.reservations": {
|
||||||
|
"name": "reservations",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"table_id": {
|
||||||
|
"name": "table_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"customer_name": {
|
||||||
|
"name": "customer_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"customer_email": {
|
||||||
|
"name": "customer_email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"name": "message",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"party_size": {
|
||||||
|
"name": "party_size",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"reservation_time": {
|
||||||
|
"name": "reservation_time",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"duration_minutes": {
|
||||||
|
"name": "duration_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 90
|
||||||
|
},
|
||||||
|
"cancel_token": {
|
||||||
|
"name": "cancel_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"reservations_table_id_restaurant_tables_id_fk": {
|
||||||
|
"name": "reservations_table_id_restaurant_tables_id_fk",
|
||||||
|
"tableFrom": "reservations",
|
||||||
|
"tableTo": "restaurant_tables",
|
||||||
|
"columnsFrom": [
|
||||||
|
"table_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"reservations_cancel_token_unique": {
|
||||||
|
"name": "reservations_cancel_token_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"cancel_token"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"reservations_table_id_reservation_time_unique": {
|
||||||
|
"name": "reservations_table_id_reservation_time_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"table_id",
|
||||||
|
"reservation_time"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.restaurant_settings": {
|
||||||
|
"name": "restaurant_settings",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"reservation_interval_minutes": {
|
||||||
|
"name": "reservation_interval_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 15
|
||||||
|
},
|
||||||
|
"reservation_duration_minutes": {
|
||||||
|
"name": "reservation_duration_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 90
|
||||||
|
},
|
||||||
|
"min_lead_time_minutes": {
|
||||||
|
"name": "min_lead_time_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 60
|
||||||
|
},
|
||||||
|
"max_lead_time_days": {
|
||||||
|
"name": "max_lead_time_days",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 60
|
||||||
|
},
|
||||||
|
"table_display_mode": {
|
||||||
|
"name": "table_display_mode",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'individual'"
|
||||||
|
},
|
||||||
|
"table_overview_image_url": {
|
||||||
|
"name": "table_overview_image_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.restaurant_tables": {
|
||||||
|
"name": "restaurant_tables",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"seats": {
|
||||||
|
"name": "seats",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"table_number": {
|
||||||
|
"name": "table_number",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"image_url": {
|
||||||
|
"name": "image_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"marker_x": {
|
||||||
|
"name": "marker_x",
|
||||||
|
"type": "double precision",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"marker_y": {
|
||||||
|
"name": "marker_y",
|
||||||
|
"type": "double precision",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.session": {
|
||||||
|
"name": "session",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"name": "token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"ip_address": {
|
||||||
|
"name": "ip_address",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_agent": {
|
||||||
|
"name": "user_agent",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"impersonated_by": {
|
||||||
|
"name": "impersonated_by",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"session_user_id_user_id_fk": {
|
||||||
|
"name": "session_user_id_user_id_fk",
|
||||||
|
"tableFrom": "session",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"session_token_unique": {
|
||||||
|
"name": "session_token_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"token"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email_verified": {
|
||||||
|
"name": "email_verified",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"name": "image",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"banned": {
|
||||||
|
"name": "banned",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"ban_reason": {
|
||||||
|
"name": "ban_reason",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"ban_expires": {
|
||||||
|
"name": "ban_expires",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"user_email_unique": {
|
||||||
|
"name": "user_email_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.verification": {
|
||||||
|
"name": "verification",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"identifier": {
|
||||||
|
"name": "identifier",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,576 @@
|
|||||||
|
{
|
||||||
|
"id": "69162a80-7a1b-480b-85c2-7f11e2f1725c",
|
||||||
|
"prevId": "c95ecb29-7866-4794-9aad-8a6fba8e7bc4",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.account": {
|
||||||
|
"name": "account",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"account_id": {
|
||||||
|
"name": "account_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"provider_id": {
|
||||||
|
"name": "provider_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"access_token": {
|
||||||
|
"name": "access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"name": "refresh_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"id_token": {
|
||||||
|
"name": "id_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"access_token_expires_at": {
|
||||||
|
"name": "access_token_expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token_expires_at": {
|
||||||
|
"name": "refresh_token_expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"name": "scope",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"account_user_id_user_id_fk": {
|
||||||
|
"name": "account_user_id_user_id_fk",
|
||||||
|
"tableFrom": "account",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.reservations": {
|
||||||
|
"name": "reservations",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"table_id": {
|
||||||
|
"name": "table_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"customer_name": {
|
||||||
|
"name": "customer_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"customer_email": {
|
||||||
|
"name": "customer_email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"customer_phone": {
|
||||||
|
"name": "customer_phone",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"name": "message",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"party_size": {
|
||||||
|
"name": "party_size",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"reservation_time": {
|
||||||
|
"name": "reservation_time",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"duration_minutes": {
|
||||||
|
"name": "duration_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 90
|
||||||
|
},
|
||||||
|
"cancel_token": {
|
||||||
|
"name": "cancel_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"reservations_table_id_restaurant_tables_id_fk": {
|
||||||
|
"name": "reservations_table_id_restaurant_tables_id_fk",
|
||||||
|
"tableFrom": "reservations",
|
||||||
|
"tableTo": "restaurant_tables",
|
||||||
|
"columnsFrom": [
|
||||||
|
"table_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"reservations_cancel_token_unique": {
|
||||||
|
"name": "reservations_cancel_token_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"cancel_token"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"reservations_table_id_reservation_time_unique": {
|
||||||
|
"name": "reservations_table_id_reservation_time_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"table_id",
|
||||||
|
"reservation_time"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.restaurant_settings": {
|
||||||
|
"name": "restaurant_settings",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"reservation_interval_minutes": {
|
||||||
|
"name": "reservation_interval_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 15
|
||||||
|
},
|
||||||
|
"reservation_duration_minutes": {
|
||||||
|
"name": "reservation_duration_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 90
|
||||||
|
},
|
||||||
|
"min_lead_time_minutes": {
|
||||||
|
"name": "min_lead_time_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 60
|
||||||
|
},
|
||||||
|
"max_lead_time_days": {
|
||||||
|
"name": "max_lead_time_days",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 60
|
||||||
|
},
|
||||||
|
"table_display_mode": {
|
||||||
|
"name": "table_display_mode",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'individual'"
|
||||||
|
},
|
||||||
|
"table_overview_image_url": {
|
||||||
|
"name": "table_overview_image_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.restaurant_tables": {
|
||||||
|
"name": "restaurant_tables",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"seats": {
|
||||||
|
"name": "seats",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"table_number": {
|
||||||
|
"name": "table_number",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"image_url": {
|
||||||
|
"name": "image_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"marker_x": {
|
||||||
|
"name": "marker_x",
|
||||||
|
"type": "double precision",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"marker_y": {
|
||||||
|
"name": "marker_y",
|
||||||
|
"type": "double precision",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.session": {
|
||||||
|
"name": "session",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"name": "token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"ip_address": {
|
||||||
|
"name": "ip_address",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_agent": {
|
||||||
|
"name": "user_agent",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"impersonated_by": {
|
||||||
|
"name": "impersonated_by",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"session_user_id_user_id_fk": {
|
||||||
|
"name": "session_user_id_user_id_fk",
|
||||||
|
"tableFrom": "session",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"session_token_unique": {
|
||||||
|
"name": "session_token_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"token"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email_verified": {
|
||||||
|
"name": "email_verified",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"name": "image",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"banned": {
|
||||||
|
"name": "banned",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"ban_reason": {
|
||||||
|
"name": "ban_reason",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"ban_expires": {
|
||||||
|
"name": "ban_expires",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"user_email_unique": {
|
||||||
|
"name": "user_email_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.verification": {
|
||||||
|
"name": "verification",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"identifier": {
|
||||||
|
"name": "identifier",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,582 @@
|
|||||||
|
{
|
||||||
|
"id": "b5488545-3b1f-4145-a565-f4fcdc5b3748",
|
||||||
|
"prevId": "69162a80-7a1b-480b-85c2-7f11e2f1725c",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.account": {
|
||||||
|
"name": "account",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"account_id": {
|
||||||
|
"name": "account_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"provider_id": {
|
||||||
|
"name": "provider_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"access_token": {
|
||||||
|
"name": "access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"name": "refresh_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"id_token": {
|
||||||
|
"name": "id_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"access_token_expires_at": {
|
||||||
|
"name": "access_token_expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token_expires_at": {
|
||||||
|
"name": "refresh_token_expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"name": "scope",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"account_user_id_user_id_fk": {
|
||||||
|
"name": "account_user_id_user_id_fk",
|
||||||
|
"tableFrom": "account",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.reservations": {
|
||||||
|
"name": "reservations",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"table_id": {
|
||||||
|
"name": "table_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"customer_name": {
|
||||||
|
"name": "customer_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"customer_email": {
|
||||||
|
"name": "customer_email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"customer_phone": {
|
||||||
|
"name": "customer_phone",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"name": "message",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"party_size": {
|
||||||
|
"name": "party_size",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"reservation_time": {
|
||||||
|
"name": "reservation_time",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"duration_minutes": {
|
||||||
|
"name": "duration_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 90
|
||||||
|
},
|
||||||
|
"cancel_token": {
|
||||||
|
"name": "cancel_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"reservations_table_id_restaurant_tables_id_fk": {
|
||||||
|
"name": "reservations_table_id_restaurant_tables_id_fk",
|
||||||
|
"tableFrom": "reservations",
|
||||||
|
"tableTo": "restaurant_tables",
|
||||||
|
"columnsFrom": [
|
||||||
|
"table_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"reservations_cancel_token_unique": {
|
||||||
|
"name": "reservations_cancel_token_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"cancel_token"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"reservations_table_id_reservation_time_unique": {
|
||||||
|
"name": "reservations_table_id_reservation_time_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"table_id",
|
||||||
|
"reservation_time"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.restaurant_settings": {
|
||||||
|
"name": "restaurant_settings",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"reservation_interval_minutes": {
|
||||||
|
"name": "reservation_interval_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 15
|
||||||
|
},
|
||||||
|
"reservation_duration_minutes": {
|
||||||
|
"name": "reservation_duration_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 90
|
||||||
|
},
|
||||||
|
"min_lead_time_minutes": {
|
||||||
|
"name": "min_lead_time_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 60
|
||||||
|
},
|
||||||
|
"max_lead_time_days": {
|
||||||
|
"name": "max_lead_time_days",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 60
|
||||||
|
},
|
||||||
|
"table_display_mode": {
|
||||||
|
"name": "table_display_mode",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'individual'"
|
||||||
|
},
|
||||||
|
"table_overview_image_url": {
|
||||||
|
"name": "table_overview_image_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.restaurant_tables": {
|
||||||
|
"name": "restaurant_tables",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"seats": {
|
||||||
|
"name": "seats",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"table_number": {
|
||||||
|
"name": "table_number",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"image_url": {
|
||||||
|
"name": "image_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"marker_x": {
|
||||||
|
"name": "marker_x",
|
||||||
|
"type": "double precision",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"marker_y": {
|
||||||
|
"name": "marker_y",
|
||||||
|
"type": "double precision",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.session": {
|
||||||
|
"name": "session",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"name": "token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"ip_address": {
|
||||||
|
"name": "ip_address",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_agent": {
|
||||||
|
"name": "user_agent",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"impersonated_by": {
|
||||||
|
"name": "impersonated_by",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"session_user_id_user_id_fk": {
|
||||||
|
"name": "session_user_id_user_id_fk",
|
||||||
|
"tableFrom": "session",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"session_token_unique": {
|
||||||
|
"name": "session_token_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"token"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email_verified": {
|
||||||
|
"name": "email_verified",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"name": "image",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"banned": {
|
||||||
|
"name": "banned",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"ban_reason": {
|
||||||
|
"name": "ban_reason",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"ban_expires": {
|
||||||
|
"name": "ban_expires",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"name": "phone",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"user_email_unique": {
|
||||||
|
"name": "user_email_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.verification": {
|
||||||
|
"name": "verification",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"identifier": {
|
||||||
|
"name": "identifier",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,589 @@
|
|||||||
|
{
|
||||||
|
"id": "b91348a4-3a6f-4c6b-a7a0-c60c06eeb9f9",
|
||||||
|
"prevId": "b5488545-3b1f-4145-a565-f4fcdc5b3748",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.account": {
|
||||||
|
"name": "account",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"account_id": {
|
||||||
|
"name": "account_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"provider_id": {
|
||||||
|
"name": "provider_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"access_token": {
|
||||||
|
"name": "access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"name": "refresh_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"id_token": {
|
||||||
|
"name": "id_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"access_token_expires_at": {
|
||||||
|
"name": "access_token_expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token_expires_at": {
|
||||||
|
"name": "refresh_token_expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"name": "scope",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"account_user_id_user_id_fk": {
|
||||||
|
"name": "account_user_id_user_id_fk",
|
||||||
|
"tableFrom": "account",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.reservations": {
|
||||||
|
"name": "reservations",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"table_id": {
|
||||||
|
"name": "table_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"customer_name": {
|
||||||
|
"name": "customer_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"customer_email": {
|
||||||
|
"name": "customer_email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"customer_phone": {
|
||||||
|
"name": "customer_phone",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"name": "message",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"party_size": {
|
||||||
|
"name": "party_size",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'confirmed'"
|
||||||
|
},
|
||||||
|
"reservation_time": {
|
||||||
|
"name": "reservation_time",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"duration_minutes": {
|
||||||
|
"name": "duration_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 90
|
||||||
|
},
|
||||||
|
"cancel_token": {
|
||||||
|
"name": "cancel_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"reservations_table_id_restaurant_tables_id_fk": {
|
||||||
|
"name": "reservations_table_id_restaurant_tables_id_fk",
|
||||||
|
"tableFrom": "reservations",
|
||||||
|
"tableTo": "restaurant_tables",
|
||||||
|
"columnsFrom": [
|
||||||
|
"table_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"reservations_cancel_token_unique": {
|
||||||
|
"name": "reservations_cancel_token_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"cancel_token"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"reservations_table_id_reservation_time_unique": {
|
||||||
|
"name": "reservations_table_id_reservation_time_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"table_id",
|
||||||
|
"reservation_time"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.restaurant_settings": {
|
||||||
|
"name": "restaurant_settings",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"reservation_interval_minutes": {
|
||||||
|
"name": "reservation_interval_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 15
|
||||||
|
},
|
||||||
|
"reservation_duration_minutes": {
|
||||||
|
"name": "reservation_duration_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 90
|
||||||
|
},
|
||||||
|
"min_lead_time_minutes": {
|
||||||
|
"name": "min_lead_time_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 60
|
||||||
|
},
|
||||||
|
"max_lead_time_days": {
|
||||||
|
"name": "max_lead_time_days",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 60
|
||||||
|
},
|
||||||
|
"table_display_mode": {
|
||||||
|
"name": "table_display_mode",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'individual'"
|
||||||
|
},
|
||||||
|
"table_overview_image_url": {
|
||||||
|
"name": "table_overview_image_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.restaurant_tables": {
|
||||||
|
"name": "restaurant_tables",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"seats": {
|
||||||
|
"name": "seats",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"table_number": {
|
||||||
|
"name": "table_number",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"image_url": {
|
||||||
|
"name": "image_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"marker_x": {
|
||||||
|
"name": "marker_x",
|
||||||
|
"type": "double precision",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"marker_y": {
|
||||||
|
"name": "marker_y",
|
||||||
|
"type": "double precision",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.session": {
|
||||||
|
"name": "session",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"name": "token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"ip_address": {
|
||||||
|
"name": "ip_address",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_agent": {
|
||||||
|
"name": "user_agent",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"impersonated_by": {
|
||||||
|
"name": "impersonated_by",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"session_user_id_user_id_fk": {
|
||||||
|
"name": "session_user_id_user_id_fk",
|
||||||
|
"tableFrom": "session",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"session_token_unique": {
|
||||||
|
"name": "session_token_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"token"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email_verified": {
|
||||||
|
"name": "email_verified",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"name": "image",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"banned": {
|
||||||
|
"name": "banned",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"ban_reason": {
|
||||||
|
"name": "ban_reason",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"ban_expires": {
|
||||||
|
"name": "ban_expires",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"name": "phone",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"user_email_unique": {
|
||||||
|
"name": "user_email_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.verification": {
|
||||||
|
"name": "verification",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"identifier": {
|
||||||
|
"name": "identifier",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,62 @@
|
|||||||
"when": 1787646264161,
|
"when": 1787646264161,
|
||||||
"tag": "0003_big_jack_flag",
|
"tag": "0003_big_jack_flag",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 4,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1787683584543,
|
||||||
|
"tag": "0004_wide_thunderbolt",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 5,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1787684215918,
|
||||||
|
"tag": "0005_nice_stark_industries",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 6,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1787684576667,
|
||||||
|
"tag": "0006_uneven_captain_cross",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 7,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1787685540508,
|
||||||
|
"tag": "0007_dusty_payback",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 8,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1787686033197,
|
||||||
|
"tag": "0008_even_toxin",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 9,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1787686357931,
|
||||||
|
"tag": "0009_nosy_alice",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 10,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1787688761615,
|
||||||
|
"tag": "0010_common_masque",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 11,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1787690760361,
|
||||||
|
"tag": "0011_cloudy_gwen_stacy",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
label?: string
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
function DotsIcon() {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true">
|
||||||
|
<circle cx="12" cy="5" r="1.8" />
|
||||||
|
<circle cx="12" cy="12" r="1.8" />
|
||||||
|
<circle cx="12" cy="19" r="1.8" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Position = { top: number; right: number }
|
||||||
|
|
||||||
|
// A small "⋮" menu that reveals its children (typically a stack of `.btn`s) in a
|
||||||
|
// dropdown — used to tuck row actions away instead of listing every button inline.
|
||||||
|
// The panel is portaled to <body> and positioned with `position: fixed` from a
|
||||||
|
// measured rect rather than anchored with `position: absolute` in place, since
|
||||||
|
// rows live inside `.table-scroll` (`overflow-x: auto`, which per the CSS spec
|
||||||
|
// forces `overflow-y` to clip too) — an in-place dropdown on the last row or two
|
||||||
|
// would otherwise get cut off at the table's bottom edge.
|
||||||
|
export function ActionMenu({ label = 'Aktionen', children }: Props) {
|
||||||
|
const [position, setPosition] = useState<Position | null>(null)
|
||||||
|
const triggerRef = useRef<HTMLButtonElement>(null)
|
||||||
|
const panelRef = useRef<HTMLDivElement>(null)
|
||||||
|
const open = position !== null
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
setPosition(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
if (open) {
|
||||||
|
close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = triggerRef.current?.getBoundingClientRect()
|
||||||
|
if (!rect) return
|
||||||
|
setPosition({ top: rect.bottom + 4, right: window.innerWidth - rect.right })
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return
|
||||||
|
|
||||||
|
function handleOutsideClick(event: MouseEvent) {
|
||||||
|
const target = event.target as Node
|
||||||
|
if (!triggerRef.current?.contains(target) && !panelRef.current?.contains(target)) {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function handleKeyDown(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Escape') close()
|
||||||
|
}
|
||||||
|
// The menu's position is measured once, on open — rather than track scroll to
|
||||||
|
// keep it glued to the trigger, just close it, matching how most native and
|
||||||
|
// web dropdowns behave when the page moves under them.
|
||||||
|
function handleScroll() {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleOutsideClick)
|
||||||
|
document.addEventListener('keydown', handleKeyDown)
|
||||||
|
window.addEventListener('scroll', handleScroll, true)
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleOutsideClick)
|
||||||
|
document.removeEventListener('keydown', handleKeyDown)
|
||||||
|
window.removeEventListener('scroll', handleScroll, true)
|
||||||
|
}
|
||||||
|
}, [open])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
ref={triggerRef}
|
||||||
|
type="button"
|
||||||
|
className="action-menu-trigger"
|
||||||
|
aria-label={label}
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded={open}
|
||||||
|
onClick={toggle}
|
||||||
|
>
|
||||||
|
<DotsIcon />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{open &&
|
||||||
|
position &&
|
||||||
|
createPortal(
|
||||||
|
<div
|
||||||
|
ref={panelRef}
|
||||||
|
className="action-menu-panel"
|
||||||
|
role="menu"
|
||||||
|
style={{ top: position.top, right: position.right }}
|
||||||
|
onClick={close}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -3,7 +3,11 @@ import { useEffect, useRef } from 'react'
|
|||||||
type Props = {
|
type Props = {
|
||||||
open: boolean
|
open: boolean
|
||||||
title: string
|
title: string
|
||||||
message: string
|
message: React.ReactNode
|
||||||
|
// A final, visually distinct callout for what irreversible actions this
|
||||||
|
// confirms — kept separate from `message` so callers don't have to hand-roll
|
||||||
|
// the emphasis every time (e.g. "This cannot be undone.").
|
||||||
|
warning?: React.ReactNode
|
||||||
confirmLabel?: string
|
confirmLabel?: string
|
||||||
cancelLabel?: string
|
cancelLabel?: string
|
||||||
danger?: boolean
|
danger?: boolean
|
||||||
@@ -11,10 +15,26 @@ type Props = {
|
|||||||
onCancel: () => void
|
onCancel: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function WarningIcon() {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="M12 3.5 21.5 20h-19L12 3.5Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.6"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path d="M12 9.5v4.2" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
|
||||||
|
<circle cx="12" cy="16.7" r="1" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function ConfirmDialog({
|
export function ConfirmDialog({
|
||||||
open,
|
open,
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
|
warning,
|
||||||
confirmLabel = 'Bestätigen',
|
confirmLabel = 'Bestätigen',
|
||||||
cancelLabel = 'Abbrechen',
|
cancelLabel = 'Abbrechen',
|
||||||
danger,
|
danger,
|
||||||
@@ -54,6 +74,12 @@ export function ConfirmDialog({
|
|||||||
<p id="confirm-dialog-message" className="confirm-dialog-message">
|
<p id="confirm-dialog-message" className="confirm-dialog-message">
|
||||||
{message}
|
{message}
|
||||||
</p>
|
</p>
|
||||||
|
{warning && (
|
||||||
|
<div className="confirm-dialog-warning">
|
||||||
|
<WarningIcon />
|
||||||
|
<span>{warning}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="confirm-dialog-actions">
|
<div className="confirm-dialog-actions">
|
||||||
<button type="button" className="btn btn-secondary" onClick={onCancel}>
|
<button type="button" className="btn btn-secondary" onClick={onCancel}>
|
||||||
{cancelLabel}
|
{cancelLabel}
|
||||||
|
|||||||
@@ -133,10 +133,45 @@ export function ReservationDateTimeField({
|
|||||||
|
|
||||||
document.addEventListener('mousedown', handleOutsideClick)
|
document.addEventListener('mousedown', handleOutsideClick)
|
||||||
document.addEventListener('keydown', handleKeyDown)
|
document.addEventListener('keydown', handleKeyDown)
|
||||||
|
|
||||||
|
// Only locks the background on mobile, where the popover becomes a fixed,
|
||||||
|
// full-screen overlay that would otherwise let the page behind it scroll —
|
||||||
|
// on desktop the popover is just a small anchored dropdown, and pinning the
|
||||||
|
// body there only removes the scrollbar and shifts the whole page sideways.
|
||||||
|
// Matches the `@media (max-width: 30rem)` breakpoint the popover-to-modal
|
||||||
|
// CSS switches on.
|
||||||
|
const isMobile = window.matchMedia('(max-width: 30rem)').matches
|
||||||
|
|
||||||
|
if (!isMobile) {
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('mousedown', handleOutsideClick)
|
document.removeEventListener('mousedown', handleOutsideClick)
|
||||||
document.removeEventListener('keydown', handleKeyDown)
|
document.removeEventListener('keydown', handleKeyDown)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// `overflow: hidden` alone doesn't stop touch-driven rubber-band scrolling on
|
||||||
|
// iOS Safari, so the body is pinned in place instead and the scroll position
|
||||||
|
// restored after.
|
||||||
|
const scrollY = window.scrollY
|
||||||
|
const body = document.body.style
|
||||||
|
const previousStyle = { position: body.position, top: body.top, left: body.left, right: body.right }
|
||||||
|
body.position = 'fixed'
|
||||||
|
body.top = `-${scrollY}px`
|
||||||
|
body.left = '0'
|
||||||
|
body.right = '0'
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleOutsideClick)
|
||||||
|
document.removeEventListener('keydown', handleKeyDown)
|
||||||
|
body.position = previousStyle.position
|
||||||
|
body.top = previousStyle.top
|
||||||
|
body.left = previousStyle.left
|
||||||
|
body.right = previousStyle.right
|
||||||
|
// `behavior: 'instant'` is required, not just the default — the page sets
|
||||||
|
// `scroll-behavior: smooth` globally, which would otherwise animate this
|
||||||
|
// restore from the top of the page instead of snapping back in place.
|
||||||
|
window.scrollTo({ top: scrollY, left: 0, behavior: 'instant' })
|
||||||
|
}
|
||||||
// Only re-run when the popover opens/closes — reopening should reset to the
|
// Only re-run when the popover opens/closes — reopening should reset to the
|
||||||
// currently selected month, not follow every keystroke in the text field.
|
// currently selected month, not follow every keystroke in the text field.
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@@ -250,7 +285,14 @@ export function ReservationDateTimeField({
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isCalendarOpen && (
|
{isCalendarOpen && (
|
||||||
<div className="date-popover" role="dialog" aria-label="Datum auswählen">
|
<div className="date-popover-backdrop" onClick={() => setIsCalendarOpen(false)}>
|
||||||
|
<div
|
||||||
|
className="date-popover"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="Datum auswählen"
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
<div className="date-popover-header">
|
<div className="date-popover-header">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -291,6 +333,7 @@ export function ReservationDateTimeField({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
import { ActionMenu } from '#/components/action-menu'
|
||||||
|
import { RESERVATION_STATUS_LABELS, type ReservationStatus, type getReservations } from '#/lib/reservations'
|
||||||
|
|
||||||
|
type ReservationRow = Awaited<ReturnType<typeof getReservations>>[number]
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
reservation: ReservationRow | null
|
||||||
|
onClose: () => void
|
||||||
|
onStatusChange: (status: ReservationStatus) => void
|
||||||
|
isUpdatingStatus?: boolean
|
||||||
|
onEdit: () => void
|
||||||
|
onDelete: () => void
|
||||||
|
isDeleting?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const STATUS_OPTIONS = Object.keys(RESERVATION_STATUS_LABELS) as ReservationStatus[]
|
||||||
|
|
||||||
|
const dateTimeFormatter = new Intl.DateTimeFormat('de-DE', {
|
||||||
|
dateStyle: 'full',
|
||||||
|
timeStyle: 'short',
|
||||||
|
})
|
||||||
|
|
||||||
|
export function ReservationDetailDialog({
|
||||||
|
reservation,
|
||||||
|
onClose,
|
||||||
|
onStatusChange,
|
||||||
|
isUpdatingStatus,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
isDeleting,
|
||||||
|
}: Props) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!reservation) return
|
||||||
|
|
||||||
|
function handleKeyDown(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Escape') onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeyDown)
|
||||||
|
return () => document.removeEventListener('keydown', handleKeyDown)
|
||||||
|
}, [reservation, onClose])
|
||||||
|
|
||||||
|
if (!reservation) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="confirm-dialog-backdrop" onClick={onClose}>
|
||||||
|
<div
|
||||||
|
className="confirm-dialog reservation-detail-dialog"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="reservation-detail-title"
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="confirm-dialog-header">
|
||||||
|
<h2 id="reservation-detail-title" className="confirm-dialog-title">
|
||||||
|
Reservierung von {reservation.customerName}
|
||||||
|
</h2>
|
||||||
|
<ActionMenu label={`Aktionen für Reservierung von ${reservation.customerName}`}>
|
||||||
|
<button type="button" className="btn btn-secondary" onClick={onEdit}>
|
||||||
|
Bearbeiten
|
||||||
|
</button>
|
||||||
|
<button type="button" className="btn btn-danger" disabled={isDeleting} onClick={onDelete}>
|
||||||
|
{isDeleting ? 'Löschen …' : 'Löschen'}
|
||||||
|
</button>
|
||||||
|
</ActionMenu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="status-options">
|
||||||
|
{STATUS_OPTIONS.map((status) => (
|
||||||
|
<button
|
||||||
|
key={status}
|
||||||
|
type="button"
|
||||||
|
className={`status-badge status-option status-${status}${
|
||||||
|
reservation.status === status ? ' is-active' : ''
|
||||||
|
}`}
|
||||||
|
disabled={isUpdatingStatus}
|
||||||
|
onClick={() => onStatusChange(status)}
|
||||||
|
>
|
||||||
|
{RESERVATION_STATUS_LABELS[status]}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl className="detail-list">
|
||||||
|
{reservation.tableNumber !== null && (
|
||||||
|
<div>
|
||||||
|
<dt>Tischnummer</dt>
|
||||||
|
<dd>{reservation.tableNumber}</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<dt>Tisch</dt>
|
||||||
|
<dd>{reservation.tableName}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Datum & Uhrzeit</dt>
|
||||||
|
<dd>{dateTimeFormatter.format(new Date(reservation.reservationTime))}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Personen</dt>
|
||||||
|
<dd>{reservation.partySize}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>E-Mail</dt>
|
||||||
|
<dd>
|
||||||
|
<a href={`mailto:${reservation.customerEmail}`}>{reservation.customerEmail}</a>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
{reservation.customerPhone && (
|
||||||
|
<div>
|
||||||
|
<dt>Telefon</dt>
|
||||||
|
<dd>
|
||||||
|
<a href={`tel:${reservation.customerPhone}`}>{reservation.customerPhone}</a>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
{reservation.message && (
|
||||||
|
<div className="detail-message">
|
||||||
|
<span className="detail-message-label">Nachricht</span>
|
||||||
|
<p>{reservation.message}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="confirm-dialog-actions">
|
||||||
|
<button type="button" className="btn btn-secondary" onClick={onClose}>
|
||||||
|
Schließen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
import type { listStaffMembers } from '#/lib/staff'
|
||||||
|
|
||||||
|
type StaffMember = Awaited<ReturnType<typeof listStaffMembers>>[number]
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
member: StaffMember | null
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateFormatter = new Intl.DateTimeFormat('de-DE', {
|
||||||
|
dateStyle: 'medium',
|
||||||
|
timeStyle: 'short',
|
||||||
|
})
|
||||||
|
|
||||||
|
export function StaffMemberDetailDialog({ member, onClose }: Props) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!member) return
|
||||||
|
|
||||||
|
function handleKeyDown(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Escape') onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeyDown)
|
||||||
|
return () => document.removeEventListener('keydown', handleKeyDown)
|
||||||
|
}, [member, onClose])
|
||||||
|
|
||||||
|
if (!member) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="confirm-dialog-backdrop" onClick={onClose}>
|
||||||
|
<div
|
||||||
|
className="confirm-dialog reservation-detail-dialog"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="staff-member-detail-title"
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
|
<h2 id="staff-member-detail-title" className="confirm-dialog-title">
|
||||||
|
{member.name}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<dl className="detail-list">
|
||||||
|
<div>
|
||||||
|
<dt>E-Mail</dt>
|
||||||
|
<dd>
|
||||||
|
<a href={`mailto:${member.email}`}>{member.email}</a>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Telefon</dt>
|
||||||
|
<dd>{member.phone ? <a href={`tel:${member.phone}`}>{member.phone}</a> : '—'}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Rolle</dt>
|
||||||
|
<dd>{member.role === 'admin' ? 'Admin' : 'Mitarbeiter'}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Status</dt>
|
||||||
|
<dd>{member.banned ? 'Gesperrt' : 'Aktiv'}</dd>
|
||||||
|
</div>
|
||||||
|
{member.banned && member.banReason && (
|
||||||
|
<div>
|
||||||
|
<dt>Sperrgrund</dt>
|
||||||
|
<dd>{member.banReason}</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<dt>Angelegt am</dt>
|
||||||
|
<dd>{dateFormatter.format(new Date(member.createdAt))}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<div className="confirm-dialog-actions">
|
||||||
|
<button type="button" className="btn btn-secondary" onClick={onClose}>
|
||||||
|
Schließen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -16,6 +16,16 @@ export function StaffTabs({ role }: Props) {
|
|||||||
>
|
>
|
||||||
Reservierungen
|
Reservierungen
|
||||||
</Link>
|
</Link>
|
||||||
|
{role === 'admin' && (
|
||||||
|
<Link
|
||||||
|
className="staff-tab"
|
||||||
|
to="/staff/tables"
|
||||||
|
activeProps={{ className: 'is-active' }}
|
||||||
|
viewTransition
|
||||||
|
>
|
||||||
|
Tische
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
{role === 'admin' && (
|
{role === 'admin' && (
|
||||||
<Link
|
<Link
|
||||||
className="staff-tab"
|
className="staff-tab"
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { RESERVATION_STATUS_LABELS, type ReservationStatus } from '#/lib/reservations'
|
||||||
|
|
||||||
|
export function StatusBadge({ status }: { status: ReservationStatus }) {
|
||||||
|
return <span className={`status-badge status-${status}`}>{RESERVATION_STATUS_LABELS[status]}</span>
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
import type { getTables } from '#/lib/reservations'
|
||||||
|
|
||||||
|
type TableRow = Awaited<ReturnType<typeof getTables>>[number]
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
table: TableRow | null
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TableDetailDialog({ table, onClose }: Props) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!table) return
|
||||||
|
|
||||||
|
function handleKeyDown(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Escape') onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeyDown)
|
||||||
|
return () => document.removeEventListener('keydown', handleKeyDown)
|
||||||
|
}, [table, onClose])
|
||||||
|
|
||||||
|
if (!table) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="confirm-dialog-backdrop" onClick={onClose}>
|
||||||
|
<div
|
||||||
|
className="confirm-dialog reservation-detail-dialog"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="table-detail-title"
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
|
<h2 id="table-detail-title" className="confirm-dialog-title">
|
||||||
|
{table.name}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{table.imageUrl && <img className="table-preview" src={table.imageUrl} alt="" />}
|
||||||
|
|
||||||
|
<dl className="detail-list">
|
||||||
|
<div>
|
||||||
|
<dt>Tischnummer</dt>
|
||||||
|
<dd>{table.tableNumber ?? '—'}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Name</dt>
|
||||||
|
<dd>{table.name}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Plätze</dt>
|
||||||
|
<dd>{table.seats}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Marker</dt>
|
||||||
|
<dd>{table.markerX !== null && table.markerY !== null ? 'Gesetzt' : 'Nicht gesetzt'}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<div className="confirm-dialog-actions">
|
||||||
|
<button type="button" className="btn btn-secondary" onClick={onClose}>
|
||||||
|
Schließen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+36
-1
@@ -1,10 +1,24 @@
|
|||||||
import { sql } from 'drizzle-orm'
|
import { sql } from 'drizzle-orm'
|
||||||
import { boolean, integer, pgTable, serial, text, timestamp, unique } from 'drizzle-orm/pg-core'
|
import { boolean, doublePrecision, integer, pgTable, serial, text, timestamp, unique } from 'drizzle-orm/pg-core'
|
||||||
|
|
||||||
export const restaurantTables = pgTable('restaurant_tables', {
|
export const restaurantTables = pgTable('restaurant_tables', {
|
||||||
id: serial('id').primaryKey(),
|
id: serial('id').primaryKey(),
|
||||||
name: text('name').notNull(),
|
name: text('name').notNull(),
|
||||||
seats: integer('seats').notNull(),
|
seats: integer('seats').notNull(),
|
||||||
|
// The number shown to guests (on the table's marker, etc.) — distinct from
|
||||||
|
// `name`, which can be a description like "Eck Tisch". Nullable so existing
|
||||||
|
// rows created before this field don't need a value until staff set one.
|
||||||
|
tableNumber: integer('table_number'),
|
||||||
|
// A data: URL of the uploaded photo, stored inline rather than as a file on disk —
|
||||||
|
// there's no separate object storage in this app, and this keeps the image tied
|
||||||
|
// to the row with no extra infra. Null until staff upload one.
|
||||||
|
imageUrl: text('image_url'),
|
||||||
|
// Where this table's pin sits on the settings overview photo, as a percentage
|
||||||
|
// (0-100) of that image's width/height — resolution-independent, so the same
|
||||||
|
// marker lines up whether it's rendered in the admin editor or on the public
|
||||||
|
// booking page. Both null until staff place one; always set together.
|
||||||
|
markerX: doublePrecision('marker_x'),
|
||||||
|
markerY: doublePrecision('marker_y'),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const reservations = pgTable(
|
export const reservations = pgTable(
|
||||||
@@ -16,7 +30,20 @@ export const reservations = pgTable(
|
|||||||
.references(() => restaurantTables.id, { onDelete: 'cascade' }),
|
.references(() => restaurantTables.id, { onDelete: 'cascade' }),
|
||||||
customerName: text('customer_name').notNull(),
|
customerName: text('customer_name').notNull(),
|
||||||
customerEmail: text('customer_email').notNull(),
|
customerEmail: text('customer_email').notNull(),
|
||||||
|
// Nullable so existing rows created before this field don't need a value —
|
||||||
|
// required going forward by the create/update zod schema instead.
|
||||||
|
customerPhone: text('customer_phone'),
|
||||||
|
// An optional free-text note from whoever booked — allergies, a birthday, a
|
||||||
|
// seating preference. Null rather than '' when nothing was entered.
|
||||||
|
message: text('message'),
|
||||||
partySize: integer('party_size').notNull(),
|
partySize: integer('party_size').notNull(),
|
||||||
|
// 'confirmed' | 'no_show' | 'completed' — an app-level enum (validated by the
|
||||||
|
// zod schema in lib/reservations.ts, not a pg enum), matching the pattern
|
||||||
|
// already used for restaurant_settings.table_display_mode. Every reservation
|
||||||
|
// is confirmed at booking time, so that's the default; staff mark the other
|
||||||
|
// two afterward. Cancelling still deletes the row (and emails the guest) —
|
||||||
|
// this doesn't introduce a "cancelled" status.
|
||||||
|
status: text('status').notNull().default('confirmed'),
|
||||||
reservationTime: timestamp('reservation_time', { withTimezone: true }).notNull(),
|
reservationTime: timestamp('reservation_time', { withTimezone: true }).notNull(),
|
||||||
// Captured from the reservation-duration setting at booking time, so a later
|
// Captured from the reservation-duration setting at booking time, so a later
|
||||||
// change to that setting doesn't retroactively change how long past bookings
|
// change to that setting doesn't retroactively change how long past bookings
|
||||||
@@ -38,6 +65,11 @@ export const restaurantSettings = pgTable('restaurant_settings', {
|
|||||||
reservationDurationMinutes: integer('reservation_duration_minutes').notNull().default(90),
|
reservationDurationMinutes: integer('reservation_duration_minutes').notNull().default(90),
|
||||||
minLeadTimeMinutes: integer('min_lead_time_minutes').notNull().default(60),
|
minLeadTimeMinutes: integer('min_lead_time_minutes').notNull().default(60),
|
||||||
maxLeadTimeDays: integer('max_lead_time_days').notNull().default(60),
|
maxLeadTimeDays: integer('max_lead_time_days').notNull().default(60),
|
||||||
|
// Whether the booking form shows a photo of the selected table ('individual') or
|
||||||
|
// a single always-visible photo of the whole room ('overview') — an app-level
|
||||||
|
// enum, validated by the zod schema in lib/settings.ts rather than a pg enum.
|
||||||
|
tableDisplayMode: text('table_display_mode').notNull().default('individual'),
|
||||||
|
tableOverviewImageUrl: text('table_overview_image_url'),
|
||||||
})
|
})
|
||||||
|
|
||||||
// Better Auth's standard schema (staff accounts) — see https://www.better-auth.com/docs/concepts/database
|
// Better Auth's standard schema (staff accounts) — see https://www.better-auth.com/docs/concepts/database
|
||||||
@@ -54,6 +86,9 @@ export const user = pgTable('user', {
|
|||||||
banned: boolean('banned'),
|
banned: boolean('banned'),
|
||||||
banReason: text('ban_reason'),
|
banReason: text('ban_reason'),
|
||||||
banExpires: timestamp('ban_expires'),
|
banExpires: timestamp('ban_expires'),
|
||||||
|
// Registered with Better Auth as a `user.additionalFields` entry in
|
||||||
|
// auth-instance.ts — not part of its base schema, same as `role`/`banned` above.
|
||||||
|
phone: text('phone'),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const session = pgTable('session', {
|
export const session = pgTable('session', {
|
||||||
|
|||||||
@@ -24,6 +24,19 @@ function createAuth() {
|
|||||||
// Staff accounts are provisioned via the seed script, not public self sign-up.
|
// Staff accounts are provisioned via the seed script, not public self sign-up.
|
||||||
disableSignUp: true,
|
disableSignUp: true,
|
||||||
},
|
},
|
||||||
|
user: {
|
||||||
|
additionalFields: {
|
||||||
|
// `required` here only governs Better Auth's own self-service sign-up/
|
||||||
|
// update-user flows, which this app doesn't use for staff — accounts go
|
||||||
|
// through admin.createUser, whose `data` field isn't schema-validated by
|
||||||
|
// Better Auth, so the actual "required" enforcement lives in the staff
|
||||||
|
// team UI (staff.team.tsx) instead.
|
||||||
|
phone: {
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
session: {
|
session: {
|
||||||
expiresIn: 60 * 60 * 12,
|
expiresIn: 60 * 60 * 12,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ function getResendClient() {
|
|||||||
type ReservationEmailInfo = {
|
type ReservationEmailInfo = {
|
||||||
customerName: string
|
customerName: string
|
||||||
customerEmail: string
|
customerEmail: string
|
||||||
|
message?: string | null
|
||||||
partySize: number
|
partySize: number
|
||||||
reservationTime: Date
|
reservationTime: Date
|
||||||
cancelToken: string
|
cancelToken: string
|
||||||
@@ -131,6 +132,19 @@ function renderReservationEmailHtml(
|
|||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
${
|
||||||
|
reservation.message
|
||||||
|
? `
|
||||||
|
<tr>
|
||||||
|
<td style="padding-top:16px;">
|
||||||
|
<div style="color:${COLORS.textMuted};font:400 12px/1.4 'Helvetica Neue',Arial,sans-serif;letter-spacing:0.03em;text-transform:uppercase;padding-bottom:4px;">
|
||||||
|
Nachricht
|
||||||
|
</div>
|
||||||
|
<div style="color:${COLORS.text};font-size:14px;">${escapeHtml(reservation.message)}</div>
|
||||||
|
</td>
|
||||||
|
</tr>`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
${
|
${
|
||||||
showActions
|
showActions
|
||||||
? `
|
? `
|
||||||
@@ -190,6 +204,7 @@ function renderReservationEmailText(
|
|||||||
`Tisch: ${reservation.table.name}`,
|
`Tisch: ${reservation.table.name}`,
|
||||||
`Datum & Uhrzeit: ${formattedTime}`,
|
`Datum & Uhrzeit: ${formattedTime}`,
|
||||||
`Personen: ${reservation.partySize}`,
|
`Personen: ${reservation.partySize}`,
|
||||||
|
...(reservation.message ? [`Nachricht: ${reservation.message}`] : []),
|
||||||
'',
|
'',
|
||||||
...(showActions
|
...(showActions
|
||||||
? [
|
? [
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
// Shared between the customer reservation form (lib/reservations.ts) and the
|
||||||
|
// staff phone field (routes/staff.team.tsx), so "a valid phone number" means
|
||||||
|
// the same thing everywhere in the app.
|
||||||
|
export const phoneSchema = z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.max(30, 'Enter a valid phone number')
|
||||||
|
// Optional leading `+` plus digits and common separators (spaces, dashes,
|
||||||
|
// dots, parentheses) — permissive about international formats, but the
|
||||||
|
// character class alone would still accept e.g. "()---" or "+", so the
|
||||||
|
// refine below requires it to actually contain a real number of digits.
|
||||||
|
.regex(/^\+?[0-9()\-.\s]+$/, 'Enter a valid phone number')
|
||||||
|
.refine((value) => (value.match(/\d/g)?.length ?? 0) >= 6, 'Enter a valid phone number')
|
||||||
+172
-5
@@ -11,7 +11,10 @@ import {
|
|||||||
sendReservationUpdateEmail,
|
sendReservationUpdateEmail,
|
||||||
} from '#/lib/email'
|
} from '#/lib/email'
|
||||||
import { isOnReservationInterval, isWithinOpeningHours } from '#/lib/opening-hours'
|
import { isOnReservationInterval, isWithinOpeningHours } from '#/lib/opening-hours'
|
||||||
|
import { phoneSchema } from '#/lib/phone'
|
||||||
|
import { zodValidator } from '#/lib/server-validator'
|
||||||
import { loadReservationSettings } from '#/lib/settings'
|
import { loadReservationSettings } from '#/lib/settings'
|
||||||
|
import { dataUrlImageSchema } from '#/lib/table-image'
|
||||||
|
|
||||||
function formatLeadTime(minutes: number) {
|
function formatLeadTime(minutes: number) {
|
||||||
if (minutes % 60 === 0) {
|
if (minutes % 60 === 0) {
|
||||||
@@ -77,6 +80,120 @@ export const getTables = createServerFn({ method: 'GET' }).handler(async () => {
|
|||||||
return db.select().from(restaurantTables).orderBy(restaurantTables.name)
|
return db.select().from(restaurantTables).orderBy(restaurantTables.name)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const tableSchema = z.object({
|
||||||
|
name: z.string().trim().min(1, 'Name ist erforderlich').max(120),
|
||||||
|
tableNumber: z.number().int().positive('Tischnummer muss positiv sein'),
|
||||||
|
seats: z.number().int().min(1, 'Mindestens 1 Platz').max(50),
|
||||||
|
imageUrl: dataUrlImageSchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const createTable = createServerFn({ method: 'POST' })
|
||||||
|
.validator(zodValidator(tableSchema))
|
||||||
|
.handler(async ({ data }) => {
|
||||||
|
const session = await requireStaffSession()
|
||||||
|
if (session.user.role !== 'admin') {
|
||||||
|
throw new Error('Nur Admins dürfen Tische verwalten')
|
||||||
|
}
|
||||||
|
|
||||||
|
const [table] = await db.insert(restaurantTables).values(data).returning()
|
||||||
|
return table
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateTableSchema = tableSchema.extend({
|
||||||
|
id: z.number().int().positive(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const updateTable = createServerFn({ method: 'POST' })
|
||||||
|
.validator(zodValidator(updateTableSchema))
|
||||||
|
.handler(async ({ data }) => {
|
||||||
|
const session = await requireStaffSession()
|
||||||
|
if (session.user.role !== 'admin') {
|
||||||
|
throw new Error('Nur Admins dürfen Tische verwalten')
|
||||||
|
}
|
||||||
|
|
||||||
|
const [updated] = await db
|
||||||
|
.update(restaurantTables)
|
||||||
|
.set({ name: data.name, tableNumber: data.tableNumber, seats: data.seats, imageUrl: data.imageUrl })
|
||||||
|
.where(eq(restaurantTables.id, data.id))
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
if (!updated) {
|
||||||
|
throw new Error('Tisch wurde nicht gefunden')
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated
|
||||||
|
})
|
||||||
|
|
||||||
|
const deleteTableSchema = z.object({
|
||||||
|
id: z.number().int().positive(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const deleteTable = createServerFn({ method: 'POST' })
|
||||||
|
.validator(zodValidator(deleteTableSchema))
|
||||||
|
.handler(async ({ data }) => {
|
||||||
|
const session = await requireStaffSession()
|
||||||
|
if (session.user.role !== 'admin') {
|
||||||
|
throw new Error('Nur Admins dürfen Tische verwalten')
|
||||||
|
}
|
||||||
|
|
||||||
|
// The FK from reservations to this table cascades on delete, which would
|
||||||
|
// silently wipe out (and never notify) any guest booked on it. Deleting
|
||||||
|
// reservations individually first goes through deleteReservation, which
|
||||||
|
// does send a cancellation email.
|
||||||
|
const [existingReservation] = await db
|
||||||
|
.select({ id: reservations.id })
|
||||||
|
.from(reservations)
|
||||||
|
.where(eq(reservations.tableId, data.id))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (existingReservation) {
|
||||||
|
throw new Error(
|
||||||
|
'Tisch kann nicht gelöscht werden, solange Reservierungen dafür bestehen — bitte zuerst verschieben oder löschen',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [deleted] = await db.delete(restaurantTables).where(eq(restaurantTables.id, data.id)).returning()
|
||||||
|
|
||||||
|
if (!deleted) {
|
||||||
|
throw new Error('Tisch wurde nicht gefunden')
|
||||||
|
}
|
||||||
|
|
||||||
|
return deleted
|
||||||
|
})
|
||||||
|
|
||||||
|
const setTableMarkerSchema = z.object({
|
||||||
|
id: z.number().int().positive(),
|
||||||
|
// Both coordinates set together (a marker) or both cleared (none) — there's no
|
||||||
|
// meaningful state for only one of them being present.
|
||||||
|
marker: z
|
||||||
|
.object({
|
||||||
|
x: z.number().min(0).max(100),
|
||||||
|
y: z.number().min(0).max(100),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const setTableMarker = createServerFn({ method: 'POST' })
|
||||||
|
.validator(zodValidator(setTableMarkerSchema))
|
||||||
|
.handler(async ({ data }) => {
|
||||||
|
const session = await requireStaffSession()
|
||||||
|
if (session.user.role !== 'admin') {
|
||||||
|
throw new Error('Nur Admins dürfen Tische verwalten')
|
||||||
|
}
|
||||||
|
|
||||||
|
const [updated] = await db
|
||||||
|
.update(restaurantTables)
|
||||||
|
.set({ markerX: data.marker?.x ?? null, markerY: data.marker?.y ?? null })
|
||||||
|
.where(eq(restaurantTables.id, data.id))
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
if (!updated) {
|
||||||
|
throw new Error('Tisch wurde nicht gefunden')
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated
|
||||||
|
})
|
||||||
|
|
||||||
export const getReservations = createServerFn({ method: 'GET' }).handler(async () => {
|
export const getReservations = createServerFn({ method: 'GET' }).handler(async () => {
|
||||||
await requireStaffSession()
|
await requireStaffSession()
|
||||||
|
|
||||||
@@ -85,9 +202,13 @@ export const getReservations = createServerFn({ method: 'GET' }).handler(async (
|
|||||||
id: reservations.id,
|
id: reservations.id,
|
||||||
tableId: reservations.tableId,
|
tableId: reservations.tableId,
|
||||||
tableName: restaurantTables.name,
|
tableName: restaurantTables.name,
|
||||||
|
tableNumber: restaurantTables.tableNumber,
|
||||||
customerName: reservations.customerName,
|
customerName: reservations.customerName,
|
||||||
customerEmail: reservations.customerEmail,
|
customerEmail: reservations.customerEmail,
|
||||||
|
customerPhone: reservations.customerPhone,
|
||||||
|
message: reservations.message,
|
||||||
partySize: reservations.partySize,
|
partySize: reservations.partySize,
|
||||||
|
status: reservations.status,
|
||||||
reservationTime: reservations.reservationTime,
|
reservationTime: reservations.reservationTime,
|
||||||
durationMinutes: reservations.durationMinutes,
|
durationMinutes: reservations.durationMinutes,
|
||||||
})
|
})
|
||||||
@@ -100,6 +221,13 @@ const createReservationSchema = z.object({
|
|||||||
tableId: z.number().int().positive(),
|
tableId: z.number().int().positive(),
|
||||||
customerName: z.string().trim().min(1, 'Name is required').max(120),
|
customerName: z.string().trim().min(1, 'Name is required').max(120),
|
||||||
customerEmail: z.string().trim().email('Enter a valid email'),
|
customerEmail: z.string().trim().email('Enter a valid email'),
|
||||||
|
customerPhone: phoneSchema,
|
||||||
|
message: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.max(500, 'Message must be at most 500 characters')
|
||||||
|
.optional()
|
||||||
|
.transform((value) => value || null),
|
||||||
partySize: z.number().int().min(1, 'At least 1 guest').max(20),
|
partySize: z.number().int().min(1, 'At least 1 guest').max(20),
|
||||||
reservationTime: z.string().refine((value) => !Number.isNaN(Date.parse(value)), {
|
reservationTime: z.string().refine((value) => !Number.isNaN(Date.parse(value)), {
|
||||||
message: 'Enter a valid date and time',
|
message: 'Enter a valid date and time',
|
||||||
@@ -107,7 +235,7 @@ const createReservationSchema = z.object({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const createReservation = createServerFn({ method: 'POST' })
|
export const createReservation = createServerFn({ method: 'POST' })
|
||||||
.validator(createReservationSchema)
|
.validator(zodValidator(createReservationSchema))
|
||||||
.handler(async ({ data }) => {
|
.handler(async ({ data }) => {
|
||||||
const settings = await loadReservationSettings()
|
const settings = await loadReservationSettings()
|
||||||
const reservationTime = new Date(data.reservationTime)
|
const reservationTime = new Date(data.reservationTime)
|
||||||
@@ -148,6 +276,8 @@ export const createReservation = createServerFn({ method: 'POST' })
|
|||||||
tableId: data.tableId,
|
tableId: data.tableId,
|
||||||
customerName: data.customerName,
|
customerName: data.customerName,
|
||||||
customerEmail: data.customerEmail,
|
customerEmail: data.customerEmail,
|
||||||
|
customerPhone: data.customerPhone,
|
||||||
|
message: data.message,
|
||||||
durationMinutes: settings.reservationDurationMinutes,
|
durationMinutes: settings.reservationDurationMinutes,
|
||||||
partySize: data.partySize,
|
partySize: data.partySize,
|
||||||
reservationTime,
|
reservationTime,
|
||||||
@@ -157,6 +287,7 @@ export const createReservation = createServerFn({ method: 'POST' })
|
|||||||
await sendReservationConfirmationEmail({
|
await sendReservationConfirmationEmail({
|
||||||
customerName: reservation.customerName,
|
customerName: reservation.customerName,
|
||||||
customerEmail: reservation.customerEmail,
|
customerEmail: reservation.customerEmail,
|
||||||
|
message: reservation.message,
|
||||||
partySize: reservation.partySize,
|
partySize: reservation.partySize,
|
||||||
reservationTime: reservation.reservationTime,
|
reservationTime: reservation.reservationTime,
|
||||||
cancelToken: reservation.cancelToken,
|
cancelToken: reservation.cancelToken,
|
||||||
@@ -171,7 +302,7 @@ const updateReservationSchema = createReservationSchema.extend({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const updateReservation = createServerFn({ method: 'POST' })
|
export const updateReservation = createServerFn({ method: 'POST' })
|
||||||
.validator(updateReservationSchema)
|
.validator(zodValidator(updateReservationSchema))
|
||||||
.handler(async ({ data }) => {
|
.handler(async ({ data }) => {
|
||||||
await requireStaffSession()
|
await requireStaffSession()
|
||||||
|
|
||||||
@@ -215,6 +346,8 @@ export const updateReservation = createServerFn({ method: 'POST' })
|
|||||||
tableId: data.tableId,
|
tableId: data.tableId,
|
||||||
customerName: data.customerName,
|
customerName: data.customerName,
|
||||||
customerEmail: data.customerEmail,
|
customerEmail: data.customerEmail,
|
||||||
|
customerPhone: data.customerPhone,
|
||||||
|
message: data.message,
|
||||||
partySize: data.partySize,
|
partySize: data.partySize,
|
||||||
reservationTime,
|
reservationTime,
|
||||||
durationMinutes: settings.reservationDurationMinutes,
|
durationMinutes: settings.reservationDurationMinutes,
|
||||||
@@ -229,6 +362,7 @@ export const updateReservation = createServerFn({ method: 'POST' })
|
|||||||
await sendReservationUpdateEmail({
|
await sendReservationUpdateEmail({
|
||||||
customerName: updated.customerName,
|
customerName: updated.customerName,
|
||||||
customerEmail: updated.customerEmail,
|
customerEmail: updated.customerEmail,
|
||||||
|
message: updated.message,
|
||||||
partySize: updated.partySize,
|
partySize: updated.partySize,
|
||||||
reservationTime: updated.reservationTime,
|
reservationTime: updated.reservationTime,
|
||||||
cancelToken: updated.cancelToken,
|
cancelToken: updated.cancelToken,
|
||||||
@@ -238,12 +372,45 @@ export const updateReservation = createServerFn({ method: 'POST' })
|
|||||||
return updated
|
return updated
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const reservationStatusSchema = z.enum(['confirmed', 'no_show', 'completed'])
|
||||||
|
|
||||||
|
export type ReservationStatus = z.infer<typeof reservationStatusSchema>
|
||||||
|
|
||||||
|
export const RESERVATION_STATUS_LABELS: Record<ReservationStatus, string> = {
|
||||||
|
confirmed: 'Bestätigt',
|
||||||
|
no_show: 'Nicht erschienen',
|
||||||
|
completed: 'Abgeschlossen',
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateReservationStatusSchema = z.object({
|
||||||
|
id: z.number().int().positive(),
|
||||||
|
status: reservationStatusSchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const updateReservationStatus = createServerFn({ method: 'POST' })
|
||||||
|
.validator(zodValidator(updateReservationStatusSchema))
|
||||||
|
.handler(async ({ data }) => {
|
||||||
|
await requireStaffSession()
|
||||||
|
|
||||||
|
const [updated] = await db
|
||||||
|
.update(reservations)
|
||||||
|
.set({ status: data.status })
|
||||||
|
.where(eq(reservations.id, data.id))
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
if (!updated) {
|
||||||
|
throw new Error('Reservation not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated
|
||||||
|
})
|
||||||
|
|
||||||
const deleteReservationSchema = z.object({
|
const deleteReservationSchema = z.object({
|
||||||
id: z.number().int().positive(),
|
id: z.number().int().positive(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const deleteReservation = createServerFn({ method: 'POST' })
|
export const deleteReservation = createServerFn({ method: 'POST' })
|
||||||
.validator(deleteReservationSchema)
|
.validator(zodValidator(deleteReservationSchema))
|
||||||
.handler(async ({ data }) => {
|
.handler(async ({ data }) => {
|
||||||
await requireStaffSession()
|
await requireStaffSession()
|
||||||
|
|
||||||
@@ -277,7 +444,7 @@ const getReservationByCancelTokenSchema = z.object({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const getReservationByCancelToken = createServerFn({ method: 'GET' })
|
export const getReservationByCancelToken = createServerFn({ method: 'GET' })
|
||||||
.validator(getReservationByCancelTokenSchema)
|
.validator(zodValidator(getReservationByCancelTokenSchema))
|
||||||
.handler(async ({ data }) => {
|
.handler(async ({ data }) => {
|
||||||
const [reservation] = await db
|
const [reservation] = await db
|
||||||
.select({
|
.select({
|
||||||
@@ -298,7 +465,7 @@ const cancelReservationByTokenSchema = z.object({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const cancelReservationByToken = createServerFn({ method: 'POST' })
|
export const cancelReservationByToken = createServerFn({ method: 'POST' })
|
||||||
.validator(cancelReservationByTokenSchema)
|
.validator(zodValidator(cancelReservationByTokenSchema))
|
||||||
.handler(async ({ data }) => {
|
.handler(async ({ data }) => {
|
||||||
const [deleted] = await db
|
const [deleted] = await db
|
||||||
.delete(reservations)
|
.delete(reservations)
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { z, ZodType } from 'zod'
|
||||||
|
|
||||||
|
// `createServerFn().validator(schema)` passes a zod schema straight through to
|
||||||
|
// TanStack Start, which on failure throws `new Error(JSON.stringify(issues))` —
|
||||||
|
// the raw issue array, not a message meant for a user to read. Wrapping the
|
||||||
|
// schema in a plain function (`.validator(zodValidator(schema))`) routes
|
||||||
|
// validation through this instead, so a rejected field surfaces its own zod
|
||||||
|
// `.message` (e.g. "Enter a valid phone number") rather than a JSON blob.
|
||||||
|
export function zodValidator<Schema extends ZodType>(schema: Schema) {
|
||||||
|
return (input: unknown): z.infer<Schema> => {
|
||||||
|
const result = schema.safeParse(input)
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error.issues[0]?.message ?? 'Invalid input')
|
||||||
|
}
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-1
@@ -5,6 +5,8 @@ import { z } from 'zod'
|
|||||||
import { db } from '#/db'
|
import { db } from '#/db'
|
||||||
import { restaurantSettings } from '#/db/schema'
|
import { restaurantSettings } from '#/db/schema'
|
||||||
import { requireStaffSession } from '#/lib/auth'
|
import { requireStaffSession } from '#/lib/auth'
|
||||||
|
import { zodValidator } from '#/lib/server-validator'
|
||||||
|
import { dataUrlImageSchema } from '#/lib/table-image'
|
||||||
|
|
||||||
// Reads never require a session — the public booking form needs the interval and
|
// Reads never require a session — the public booking form needs the interval and
|
||||||
// lead time to generate its own time-slot grid, just like getTables(). Exported
|
// lead time to generate its own time-slot grid, just like getTables(). Exported
|
||||||
@@ -31,10 +33,12 @@ const updateReservationSettingsSchema = z.object({
|
|||||||
reservationDurationMinutes: z.number().int().min(15).max(480),
|
reservationDurationMinutes: z.number().int().min(15).max(480),
|
||||||
minLeadTimeMinutes: z.number().int().min(0).max(10080),
|
minLeadTimeMinutes: z.number().int().min(0).max(10080),
|
||||||
maxLeadTimeDays: z.number().int().min(1).max(365),
|
maxLeadTimeDays: z.number().int().min(1).max(365),
|
||||||
|
tableDisplayMode: z.enum(['individual', 'overview']),
|
||||||
|
tableOverviewImageUrl: dataUrlImageSchema,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const updateReservationSettings = createServerFn({ method: 'POST' })
|
export const updateReservationSettings = createServerFn({ method: 'POST' })
|
||||||
.validator(updateReservationSettingsSchema)
|
.validator(zodValidator(updateReservationSettingsSchema))
|
||||||
.handler(async ({ data }) => {
|
.handler(async ({ data }) => {
|
||||||
console.log('[settings] updateReservationSettings called with', data)
|
console.log('[settings] updateReservationSettings called with', data)
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -11,5 +11,9 @@ export const listStaffMembers = createServerFn({ method: 'GET' }).handler(async
|
|||||||
query: { sortBy: 'createdAt', sortDirection: 'asc', limit: 100 },
|
query: { sortBy: 'createdAt', sortDirection: 'asc', limit: 100 },
|
||||||
})
|
})
|
||||||
|
|
||||||
return users
|
// `listUsers`'s return type doesn't pick up `user.additionalFields` from the
|
||||||
|
// auth config the way the actual response body does — `phone` is present at
|
||||||
|
// runtime (Better Auth includes every configured additional field on every
|
||||||
|
// user-returning endpoint) but missing from admin.d.ts's `UserWithRole` type.
|
||||||
|
return users as Array<(typeof users)[number] & { phone: string | null }>
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
// Generous enough for a photo, small enough to keep rows (and the customer-facing
|
||||||
|
// page that loads them up front) from bloating — these are data: URLs stored
|
||||||
|
// inline, not uploads to disk-backed storage, which this app doesn't have.
|
||||||
|
export const MAX_TABLE_IMAGE_BYTES = 2_000_000
|
||||||
|
|
||||||
|
export const ACCEPTED_IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/webp']
|
||||||
|
|
||||||
|
export const dataUrlImageSchema = z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.max(Math.ceil((MAX_TABLE_IMAGE_BYTES * 4) / 3) + 100)
|
||||||
|
.regex(/^data:image\/(png|jpeg|jpg|webp);base64,/, 'Bild muss als PNG, JPEG oder WebP hochgeladen werden')
|
||||||
|
.nullable()
|
||||||
|
|
||||||
|
export function readImageAsDataUrl(file: File): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = () => resolve(reader.result as string)
|
||||||
|
reader.onerror = () => reject(reader.error ?? new Error('Datei konnte nicht gelesen werden'))
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import { Route as ImpressumRouteImport } from './routes/impressum'
|
|||||||
import { Route as StaffIndexRouteImport } from './routes/staff.index'
|
import { Route as StaffIndexRouteImport } from './routes/staff.index'
|
||||||
import { Route as StaffLoginRouteImport } from './routes/staff.login'
|
import { Route as StaffLoginRouteImport } from './routes/staff.login'
|
||||||
import { Route as StaffSettingsRouteImport } from './routes/staff.settings'
|
import { Route as StaffSettingsRouteImport } from './routes/staff.settings'
|
||||||
|
import { Route as StaffTablesRouteImport } from './routes/staff.tables'
|
||||||
import { Route as StaffTeamRouteImport } from './routes/staff.team'
|
import { Route as StaffTeamRouteImport } from './routes/staff.team'
|
||||||
import { Route as StornierenTokenRouteImport } from './routes/stornieren.$token'
|
import { Route as StornierenTokenRouteImport } from './routes/stornieren.$token'
|
||||||
|
|
||||||
@@ -42,6 +43,11 @@ const StaffSettingsRoute = StaffSettingsRouteImport.update({
|
|||||||
path: '/staff/settings',
|
path: '/staff/settings',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const StaffTablesRoute = StaffTablesRouteImport.update({
|
||||||
|
id: '/staff/tables',
|
||||||
|
path: '/staff/tables',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const StaffTeamRoute = StaffTeamRouteImport.update({
|
const StaffTeamRoute = StaffTeamRouteImport.update({
|
||||||
id: '/staff/team',
|
id: '/staff/team',
|
||||||
path: '/staff/team',
|
path: '/staff/team',
|
||||||
@@ -58,6 +64,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/impressum': typeof ImpressumRoute
|
'/impressum': typeof ImpressumRoute
|
||||||
'/staff/login': typeof StaffLoginRoute
|
'/staff/login': typeof StaffLoginRoute
|
||||||
'/staff/settings': typeof StaffSettingsRoute
|
'/staff/settings': typeof StaffSettingsRoute
|
||||||
|
'/staff/tables': typeof StaffTablesRoute
|
||||||
'/staff/team': typeof StaffTeamRoute
|
'/staff/team': typeof StaffTeamRoute
|
||||||
'/stornieren/$token': typeof StornierenTokenRoute
|
'/stornieren/$token': typeof StornierenTokenRoute
|
||||||
'/staff/': typeof StaffIndexRoute
|
'/staff/': typeof StaffIndexRoute
|
||||||
@@ -67,6 +74,7 @@ export interface FileRoutesByTo {
|
|||||||
'/impressum': typeof ImpressumRoute
|
'/impressum': typeof ImpressumRoute
|
||||||
'/staff/login': typeof StaffLoginRoute
|
'/staff/login': typeof StaffLoginRoute
|
||||||
'/staff/settings': typeof StaffSettingsRoute
|
'/staff/settings': typeof StaffSettingsRoute
|
||||||
|
'/staff/tables': typeof StaffTablesRoute
|
||||||
'/staff/team': typeof StaffTeamRoute
|
'/staff/team': typeof StaffTeamRoute
|
||||||
'/stornieren/$token': typeof StornierenTokenRoute
|
'/stornieren/$token': typeof StornierenTokenRoute
|
||||||
'/staff': typeof StaffIndexRoute
|
'/staff': typeof StaffIndexRoute
|
||||||
@@ -77,6 +85,7 @@ export interface FileRoutesById {
|
|||||||
'/impressum': typeof ImpressumRoute
|
'/impressum': typeof ImpressumRoute
|
||||||
'/staff/login': typeof StaffLoginRoute
|
'/staff/login': typeof StaffLoginRoute
|
||||||
'/staff/settings': typeof StaffSettingsRoute
|
'/staff/settings': typeof StaffSettingsRoute
|
||||||
|
'/staff/tables': typeof StaffTablesRoute
|
||||||
'/staff/team': typeof StaffTeamRoute
|
'/staff/team': typeof StaffTeamRoute
|
||||||
'/stornieren/$token': typeof StornierenTokenRoute
|
'/stornieren/$token': typeof StornierenTokenRoute
|
||||||
'/staff/': typeof StaffIndexRoute
|
'/staff/': typeof StaffIndexRoute
|
||||||
@@ -88,6 +97,7 @@ export interface FileRouteTypes {
|
|||||||
| '/impressum'
|
| '/impressum'
|
||||||
| '/staff/login'
|
| '/staff/login'
|
||||||
| '/staff/settings'
|
| '/staff/settings'
|
||||||
|
| '/staff/tables'
|
||||||
| '/staff/team'
|
| '/staff/team'
|
||||||
| '/stornieren/$token'
|
| '/stornieren/$token'
|
||||||
| '/staff/'
|
| '/staff/'
|
||||||
@@ -97,6 +107,7 @@ export interface FileRouteTypes {
|
|||||||
| '/impressum'
|
| '/impressum'
|
||||||
| '/staff/login'
|
| '/staff/login'
|
||||||
| '/staff/settings'
|
| '/staff/settings'
|
||||||
|
| '/staff/tables'
|
||||||
| '/staff/team'
|
| '/staff/team'
|
||||||
| '/stornieren/$token'
|
| '/stornieren/$token'
|
||||||
| '/staff'
|
| '/staff'
|
||||||
@@ -106,6 +117,7 @@ export interface FileRouteTypes {
|
|||||||
| '/impressum'
|
| '/impressum'
|
||||||
| '/staff/login'
|
| '/staff/login'
|
||||||
| '/staff/settings'
|
| '/staff/settings'
|
||||||
|
| '/staff/tables'
|
||||||
| '/staff/team'
|
| '/staff/team'
|
||||||
| '/stornieren/$token'
|
| '/stornieren/$token'
|
||||||
| '/staff/'
|
| '/staff/'
|
||||||
@@ -116,6 +128,7 @@ export interface RootRouteChildren {
|
|||||||
ImpressumRoute: typeof ImpressumRoute
|
ImpressumRoute: typeof ImpressumRoute
|
||||||
StaffLoginRoute: typeof StaffLoginRoute
|
StaffLoginRoute: typeof StaffLoginRoute
|
||||||
StaffSettingsRoute: typeof StaffSettingsRoute
|
StaffSettingsRoute: typeof StaffSettingsRoute
|
||||||
|
StaffTablesRoute: typeof StaffTablesRoute
|
||||||
StaffTeamRoute: typeof StaffTeamRoute
|
StaffTeamRoute: typeof StaffTeamRoute
|
||||||
StornierenTokenRoute: typeof StornierenTokenRoute
|
StornierenTokenRoute: typeof StornierenTokenRoute
|
||||||
StaffIndexRoute: typeof StaffIndexRoute
|
StaffIndexRoute: typeof StaffIndexRoute
|
||||||
@@ -158,6 +171,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof StaffSettingsRouteImport
|
preLoaderRoute: typeof StaffSettingsRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/staff/tables': {
|
||||||
|
id: '/staff/tables'
|
||||||
|
path: '/staff/tables'
|
||||||
|
fullPath: '/staff/tables'
|
||||||
|
preLoaderRoute: typeof StaffTablesRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/staff/team': {
|
'/staff/team': {
|
||||||
id: '/staff/team'
|
id: '/staff/team'
|
||||||
path: '/staff/team'
|
path: '/staff/team'
|
||||||
@@ -180,6 +200,7 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
ImpressumRoute: ImpressumRoute,
|
ImpressumRoute: ImpressumRoute,
|
||||||
StaffLoginRoute: StaffLoginRoute,
|
StaffLoginRoute: StaffLoginRoute,
|
||||||
StaffSettingsRoute: StaffSettingsRoute,
|
StaffSettingsRoute: StaffSettingsRoute,
|
||||||
|
StaffTablesRoute: StaffTablesRoute,
|
||||||
StaffTeamRoute: StaffTeamRoute,
|
StaffTeamRoute: StaffTeamRoute,
|
||||||
StornierenTokenRoute: StornierenTokenRoute,
|
StornierenTokenRoute: StornierenTokenRoute,
|
||||||
StaffIndexRoute: StaffIndexRoute,
|
StaffIndexRoute: StaffIndexRoute,
|
||||||
|
|||||||
+65
-4
@@ -38,6 +38,8 @@ function Home() {
|
|||||||
const [tableId, setTableId] = useState<number | string>(tables[0]?.id ?? '')
|
const [tableId, setTableId] = useState<number | string>(tables[0]?.id ?? '')
|
||||||
const [customerName, setCustomerName] = useState('')
|
const [customerName, setCustomerName] = useState('')
|
||||||
const [customerEmail, setCustomerEmail] = useState('')
|
const [customerEmail, setCustomerEmail] = useState('')
|
||||||
|
const [customerPhone, setCustomerPhone] = useState('')
|
||||||
|
const [message, setMessage] = useState('')
|
||||||
const [partySize, setPartySize] = useState(2)
|
const [partySize, setPartySize] = useState(2)
|
||||||
const [reservationTime, setReservationTime] = useState(() => {
|
const [reservationTime, setReservationTime] = useState(() => {
|
||||||
const slot = getNextAvailableSlot(
|
const slot = getNextAvailableSlot(
|
||||||
@@ -51,6 +53,7 @@ function Home() {
|
|||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
const todayName = getTodayLabelBerlin()
|
const todayName = getTodayLabelBerlin()
|
||||||
|
const selectedTable = tables.find((table) => String(table.id) === String(tableId))
|
||||||
|
|
||||||
async function handleSubmit(event: React.FormEvent) {
|
async function handleSubmit(event: React.FormEvent) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -74,6 +77,8 @@ function Home() {
|
|||||||
tableId: Number(tableId),
|
tableId: Number(tableId),
|
||||||
customerName,
|
customerName,
|
||||||
customerEmail,
|
customerEmail,
|
||||||
|
customerPhone,
|
||||||
|
message,
|
||||||
partySize: Number(partySize),
|
partySize: Number(partySize),
|
||||||
reservationTime: new Date(reservationTime).toISOString(),
|
reservationTime: new Date(reservationTime).toISOString(),
|
||||||
},
|
},
|
||||||
@@ -82,6 +87,8 @@ function Home() {
|
|||||||
setSuccess('Vielen Dank! Ihre Reservierung ist bestätigt.')
|
setSuccess('Vielen Dank! Ihre Reservierung ist bestätigt.')
|
||||||
setCustomerName('')
|
setCustomerName('')
|
||||||
setCustomerEmail('')
|
setCustomerEmail('')
|
||||||
|
setCustomerPhone('')
|
||||||
|
setMessage('')
|
||||||
setPartySize(2)
|
setPartySize(2)
|
||||||
await router.invalidate()
|
await router.invalidate()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -98,11 +105,11 @@ function Home() {
|
|||||||
<section className="hero" id="top">
|
<section className="hero" id="top">
|
||||||
<FlameIcon />
|
<FlameIcon />
|
||||||
<h1>
|
<h1>
|
||||||
Steakhaus <span>bei Mirko</span>
|
Vom Feuer verwöhnt, <span>mit Liebe serviert</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p>
|
<p>
|
||||||
Steaks vom Grill, gute Gesellschaft und ein gemütlicher Abend mitten in Meppen. Reservieren
|
Dicke Steaks, knisterndes Holzkohlefeuer und Gelächter am Nachbartisch – so schmeckt ein
|
||||||
Sie Ihren Tisch – wir freuen uns auf Sie.
|
guter Abend mitten in Meppen. Reservieren Sie Ihren Tisch, wir legen schon auf.
|
||||||
</p>
|
</p>
|
||||||
<div className="hero-actions">
|
<div className="hero-actions">
|
||||||
<a className="btn btn-primary" href="#reservieren">
|
<a className="btn btn-primary" href="#reservieren">
|
||||||
@@ -158,6 +165,36 @@ function Home() {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
{settings.tableDisplayMode === 'overview' ? (
|
||||||
|
settings.tableOverviewImageUrl && (
|
||||||
|
<div className="table-overview-wrap">
|
||||||
|
<img className="table-overview-image" src={settings.tableOverviewImageUrl} alt="Übersicht unserer Tische" />
|
||||||
|
{tables
|
||||||
|
.filter((table) => table.markerX !== null && table.markerY !== null)
|
||||||
|
.map((table) => (
|
||||||
|
<button
|
||||||
|
key={table.id}
|
||||||
|
type="button"
|
||||||
|
className={`table-marker${String(table.id) === String(tableId) ? ' is-selected' : ''}`}
|
||||||
|
style={{ left: `${table.markerX}%`, top: `${table.markerY}%` }}
|
||||||
|
onClick={() => setTableId(table.id)}
|
||||||
|
aria-label={`${table.name} auswählen`}
|
||||||
|
>
|
||||||
|
<span className="table-marker-label">{table.tableNumber ?? '?'}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
selectedTable?.imageUrl && (
|
||||||
|
<img
|
||||||
|
className="table-preview"
|
||||||
|
src={selectedTable.imageUrl}
|
||||||
|
alt={`Ansicht von ${selectedTable.name}`}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label>
|
<label>
|
||||||
Name
|
Name
|
||||||
@@ -181,6 +218,19 @@ function Home() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
|
<label>
|
||||||
|
Telefon
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
inputMode="tel"
|
||||||
|
pattern="[+]?[0-9()\-.\s]{6,30}"
|
||||||
|
title="Nur Ziffern, Leerzeichen und + ( ) - ."
|
||||||
|
value={customerPhone}
|
||||||
|
onChange={(e) => setCustomerPhone(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
Anzahl Personen
|
Anzahl Personen
|
||||||
<input
|
<input
|
||||||
@@ -193,6 +243,7 @@ function Home() {
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
Datum & Uhrzeit
|
Datum & Uhrzeit
|
||||||
@@ -204,7 +255,17 @@ function Home() {
|
|||||||
maxLeadTimeDays={settings.maxLeadTimeDays}
|
maxLeadTimeDays={settings.maxLeadTimeDays}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
|
||||||
|
<label>
|
||||||
|
Nachricht (optional)
|
||||||
|
<textarea
|
||||||
|
rows={3}
|
||||||
|
maxLength={500}
|
||||||
|
placeholder="Allergien, ein besonderer Anlass, Sitzwünsche …"
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
<p className="field-hint">
|
<p className="field-hint">
|
||||||
Verfügbare Uhrzeiten richten sich nach unseren{' '}
|
Verfügbare Uhrzeiten richten sich nach unseren{' '}
|
||||||
|
|||||||
+115
-22
@@ -4,17 +4,22 @@ import { createFileRoute, Link, redirect, useRouter } from '@tanstack/react-rout
|
|||||||
import { ConfirmDialog } from '#/components/confirm-dialog'
|
import { ConfirmDialog } from '#/components/confirm-dialog'
|
||||||
import { ReservationCharts } from '#/components/reservation-charts'
|
import { ReservationCharts } from '#/components/reservation-charts'
|
||||||
import { ReservationDateTimeField } from '#/components/reservation-datetime-field'
|
import { ReservationDateTimeField } from '#/components/reservation-datetime-field'
|
||||||
|
import { ReservationDetailDialog } from '#/components/reservation-detail-dialog'
|
||||||
import { StaffTabs } from '#/components/staff-tabs'
|
import { StaffTabs } from '#/components/staff-tabs'
|
||||||
|
import { StatusBadge } from '#/components/status-badge'
|
||||||
import { getCurrentStaffUser } from '#/lib/auth'
|
import { getCurrentStaffUser } from '#/lib/auth'
|
||||||
import { authClient } from '#/lib/auth-client'
|
import { authClient } from '#/lib/auth-client'
|
||||||
import { formatDateOnly, formatDateTimeLocal } from '#/lib/datetime'
|
import { formatDateOnly, formatDateTimeLocal } from '#/lib/datetime'
|
||||||
import { generateTimeOptions, getNextAvailableSlot, getOpeningRangesForWeekday } from '#/lib/opening-hours'
|
import { generateTimeOptions, getNextAvailableSlot, getOpeningRangesForWeekday } from '#/lib/opening-hours'
|
||||||
import {
|
import {
|
||||||
|
RESERVATION_STATUS_LABELS,
|
||||||
createReservation,
|
createReservation,
|
||||||
deleteReservation,
|
deleteReservation,
|
||||||
getReservations,
|
getReservations,
|
||||||
getTables,
|
getTables,
|
||||||
updateReservation,
|
updateReservation,
|
||||||
|
updateReservationStatus,
|
||||||
|
type ReservationStatus,
|
||||||
} from '#/lib/reservations'
|
} from '#/lib/reservations'
|
||||||
import { getReservationSettings } from '#/lib/settings'
|
import { getReservationSettings } from '#/lib/settings'
|
||||||
|
|
||||||
@@ -54,6 +59,8 @@ type FormState = {
|
|||||||
tableId: number | string
|
tableId: number | string
|
||||||
customerName: string
|
customerName: string
|
||||||
customerEmail: string
|
customerEmail: string
|
||||||
|
customerPhone: string
|
||||||
|
message: string
|
||||||
partySize: number
|
partySize: number
|
||||||
reservationTime: string
|
reservationTime: string
|
||||||
}
|
}
|
||||||
@@ -70,6 +77,8 @@ function emptyForm(defaultTableId: number | string, settings: SettingsRow): Form
|
|||||||
tableId: defaultTableId,
|
tableId: defaultTableId,
|
||||||
customerName: '',
|
customerName: '',
|
||||||
customerEmail: '',
|
customerEmail: '',
|
||||||
|
customerPhone: '',
|
||||||
|
message: '',
|
||||||
partySize: 2,
|
partySize: 2,
|
||||||
reservationTime: slot ? `${slot.date}T${slot.time}` : '',
|
reservationTime: slot ? `${slot.date}T${slot.time}` : '',
|
||||||
}
|
}
|
||||||
@@ -86,9 +95,12 @@ function StaffDashboard() {
|
|||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
const [deletingId, setDeletingId] = useState<number | null>(null)
|
const [deletingId, setDeletingId] = useState<number | null>(null)
|
||||||
const [pendingDelete, setPendingDelete] = useState<ReservationRow | null>(null)
|
const [pendingDelete, setPendingDelete] = useState<ReservationRow | null>(null)
|
||||||
|
const [viewingReservation, setViewingReservation] = useState<ReservationRow | null>(null)
|
||||||
|
const [isUpdatingStatus, setIsUpdatingStatus] = useState(false)
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
const [tableFilter, setTableFilter] = useState('all')
|
const [tableFilter, setTableFilter] = useState('all')
|
||||||
const [timeFilter, setTimeFilter] = useState<'all' | 'today' | 'upcoming' | 'past'>('all')
|
const [timeFilter, setTimeFilter] = useState<'all' | 'today' | 'upcoming' | 'past'>('all')
|
||||||
|
const [statusFilter, setStatusFilter] = useState<'all' | ReservationStatus>('all')
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const todaysReservations = reservations.filter((r) => isSameDay(new Date(r.reservationTime), now))
|
const todaysReservations = reservations.filter((r) => isSameDay(new Date(r.reservationTime), now))
|
||||||
@@ -97,6 +109,7 @@ function StaffDashboard() {
|
|||||||
|
|
||||||
const filteredReservations = reservations.filter((r) => {
|
const filteredReservations = reservations.filter((r) => {
|
||||||
if (tableFilter !== 'all' && String(r.tableId) !== tableFilter) return false
|
if (tableFilter !== 'all' && String(r.tableId) !== tableFilter) return false
|
||||||
|
if (statusFilter !== 'all' && r.status !== statusFilter) return false
|
||||||
|
|
||||||
const reservationTime = new Date(r.reservationTime)
|
const reservationTime = new Date(r.reservationTime)
|
||||||
if (timeFilter === 'today' && !isSameDay(reservationTime, now)) return false
|
if (timeFilter === 'today' && !isSameDay(reservationTime, now)) return false
|
||||||
@@ -105,7 +118,7 @@ function StaffDashboard() {
|
|||||||
|
|
||||||
const query = searchQuery.trim().toLowerCase()
|
const query = searchQuery.trim().toLowerCase()
|
||||||
if (query) {
|
if (query) {
|
||||||
const haystack = `${r.customerName} ${r.customerEmail} ${r.tableName}`.toLowerCase()
|
const haystack = `${r.customerName} ${r.customerEmail} ${r.customerPhone ?? ''} ${r.tableName} ${r.tableNumber ?? ''}`.toLowerCase()
|
||||||
if (!haystack.includes(query)) return false
|
if (!haystack.includes(query)) return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,6 +157,8 @@ function StaffDashboard() {
|
|||||||
tableId: reservation.tableId,
|
tableId: reservation.tableId,
|
||||||
customerName: reservation.customerName,
|
customerName: reservation.customerName,
|
||||||
customerEmail: reservation.customerEmail,
|
customerEmail: reservation.customerEmail,
|
||||||
|
customerPhone: reservation.customerPhone ?? '',
|
||||||
|
message: reservation.message ?? '',
|
||||||
partySize: reservation.partySize,
|
partySize: reservation.partySize,
|
||||||
reservationTime: formatDateTimeLocal(new Date(reservation.reservationTime)),
|
reservationTime: formatDateTimeLocal(new Date(reservation.reservationTime)),
|
||||||
})
|
})
|
||||||
@@ -178,6 +193,8 @@ function StaffDashboard() {
|
|||||||
tableId: Number(form.tableId),
|
tableId: Number(form.tableId),
|
||||||
customerName: form.customerName,
|
customerName: form.customerName,
|
||||||
customerEmail: form.customerEmail,
|
customerEmail: form.customerEmail,
|
||||||
|
customerPhone: form.customerPhone,
|
||||||
|
message: form.message,
|
||||||
partySize: Number(form.partySize),
|
partySize: Number(form.partySize),
|
||||||
reservationTime: new Date(form.reservationTime).toISOString(),
|
reservationTime: new Date(form.reservationTime).toISOString(),
|
||||||
}
|
}
|
||||||
@@ -223,6 +240,23 @@ function StaffDashboard() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleStatusChange(reservation: ReservationRow, status: ReservationStatus) {
|
||||||
|
setIsUpdatingStatus(true)
|
||||||
|
setError(null)
|
||||||
|
setSuccess(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateReservationStatus({ data: { id: reservation.id, status } })
|
||||||
|
setViewingReservation((current) => (current ? { ...current, status } : current))
|
||||||
|
setSuccess(`Status wurde auf „${RESERVATION_STATUS_LABELS[status]}“ geändert.`)
|
||||||
|
await router.invalidate()
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Etwas ist schiefgelaufen')
|
||||||
|
} finally {
|
||||||
|
setIsUpdatingStatus(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleLogout() {
|
async function handleLogout() {
|
||||||
await authClient.signOut()
|
await authClient.signOut()
|
||||||
await router.navigate({ to: '/staff/login' })
|
await router.navigate({ to: '/staff/login' })
|
||||||
@@ -320,6 +354,19 @@ function StaffDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
|
<label>
|
||||||
|
Telefon
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
inputMode="tel"
|
||||||
|
pattern="[+]?[0-9()\-.\s]{6,30}"
|
||||||
|
title="Nur Ziffern, Leerzeichen und + ( ) - ."
|
||||||
|
value={form.customerPhone}
|
||||||
|
onChange={(e) => setForm((f) => ({ ...f, customerPhone: e.target.value }))}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
Anzahl Personen
|
Anzahl Personen
|
||||||
<input
|
<input
|
||||||
@@ -332,6 +379,7 @@ function StaffDashboard() {
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
Datum & Uhrzeit
|
Datum & Uhrzeit
|
||||||
@@ -344,7 +392,16 @@ function StaffDashboard() {
|
|||||||
maxLeadTimeDays={settings.maxLeadTimeDays}
|
maxLeadTimeDays={settings.maxLeadTimeDays}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
|
||||||
|
<label>
|
||||||
|
Nachricht (optional)
|
||||||
|
<textarea
|
||||||
|
rows={2}
|
||||||
|
maxLength={500}
|
||||||
|
value={form.message}
|
||||||
|
onChange={(e) => setForm((f) => ({ ...f, message: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
<div className="form-actions">
|
<div className="form-actions">
|
||||||
<button type="submit" disabled={isSubmitting}>
|
<button type="submit" disabled={isSubmitting}>
|
||||||
@@ -377,7 +434,7 @@ function StaffDashboard() {
|
|||||||
<option value="all">Alle Tische</option>
|
<option value="all">Alle Tische</option>
|
||||||
{tables.map((table) => (
|
{tables.map((table) => (
|
||||||
<option key={table.id} value={String(table.id)}>
|
<option key={table.id} value={String(table.id)}>
|
||||||
{table.name}
|
{table.tableNumber === null ? table.name : `${table.tableNumber} — ${table.name}`}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
@@ -391,6 +448,18 @@ function StaffDashboard() {
|
|||||||
<option value="upcoming">Anstehend</option>
|
<option value="upcoming">Anstehend</option>
|
||||||
<option value="past">Vergangen</option>
|
<option value="past">Vergangen</option>
|
||||||
</select>
|
</select>
|
||||||
|
<select
|
||||||
|
value={statusFilter}
|
||||||
|
onChange={(e) => setStatusFilter(e.target.value as typeof statusFilter)}
|
||||||
|
aria-label="Nach Status filtern"
|
||||||
|
>
|
||||||
|
<option value="all">Alle Status</option>
|
||||||
|
{Object.entries(RESERVATION_STATUS_LABELS).map(([status, label]) => (
|
||||||
|
<option key={status} value={status}>
|
||||||
|
{label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -402,42 +471,44 @@ function StaffDashboard() {
|
|||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="table-scroll">
|
<div className="table-scroll">
|
||||||
<table className="reservations-table">
|
<table className="reservations-table reservations-table-clickable">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Datum & Uhrzeit</th>
|
<th>Datum & Uhrzeit</th>
|
||||||
|
<th>Nr.</th>
|
||||||
<th>Tisch</th>
|
<th>Tisch</th>
|
||||||
<th>Kunde</th>
|
<th>Kunde</th>
|
||||||
<th>Personen</th>
|
<th>Personen</th>
|
||||||
<th></th>
|
<th>Status</th>
|
||||||
|
<th>Nachricht</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{filteredReservations.map((reservation) => (
|
{filteredReservations.map((reservation) => (
|
||||||
<tr key={reservation.id} className={editingId === reservation.id ? 'is-editing' : undefined}>
|
<tr
|
||||||
|
key={reservation.id}
|
||||||
|
className={editingId === reservation.id ? 'is-editing' : undefined}
|
||||||
|
onClick={() => setViewingReservation(reservation)}
|
||||||
|
>
|
||||||
<td>{dateTimeFormatter.format(new Date(reservation.reservationTime))}</td>
|
<td>{dateTimeFormatter.format(new Date(reservation.reservationTime))}</td>
|
||||||
|
<td>{reservation.tableNumber ?? '—'}</td>
|
||||||
<td>{reservation.tableName}</td>
|
<td>{reservation.tableName}</td>
|
||||||
<td>
|
<td>
|
||||||
{reservation.customerName}
|
{reservation.customerName}
|
||||||
<br />
|
<br />
|
||||||
<span className="muted">{reservation.customerEmail}</span>
|
<span className="muted">{reservation.customerEmail}</span>
|
||||||
|
{reservation.customerPhone && (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
<span className="muted">{reservation.customerPhone}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>{reservation.partySize}</td>
|
<td>{reservation.partySize}</td>
|
||||||
<td>
|
<td>
|
||||||
<div className="table-actions">
|
<StatusBadge status={reservation.status as ReservationStatus} />
|
||||||
<button type="button" className="btn btn-secondary" onClick={() => startEdit(reservation)}>
|
|
||||||
Bearbeiten
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-danger"
|
|
||||||
disabled={deletingId === reservation.id}
|
|
||||||
onClick={() => setPendingDelete(reservation)}
|
|
||||||
>
|
|
||||||
{deletingId === reservation.id ? 'Löschen …' : 'Löschen'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
|
<td className="message-cell">{reservation.message || <span className="muted">—</span>}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -451,15 +522,37 @@ function StaffDashboard() {
|
|||||||
open={pendingDelete !== null}
|
open={pendingDelete !== null}
|
||||||
title="Reservierung löschen"
|
title="Reservierung löschen"
|
||||||
message={
|
message={
|
||||||
pendingDelete
|
pendingDelete && (
|
||||||
? `Reservierung von ${pendingDelete.customerName} wirklich löschen? Der Gast erhält eine E-Mail zur Bestätigung der Stornierung. Dies kann nicht rückgängig gemacht werden.`
|
<>
|
||||||
: ''
|
Reservierung von <strong>{pendingDelete.customerName}</strong> wirklich löschen? Der Gast
|
||||||
|
erhält eine E-Mail zur Bestätigung der Stornierung.
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
warning="Diese Aktion kann nicht rückgängig gemacht werden."
|
||||||
confirmLabel="Löschen"
|
confirmLabel="Löschen"
|
||||||
danger
|
danger
|
||||||
onConfirm={confirmDelete}
|
onConfirm={confirmDelete}
|
||||||
onCancel={() => setPendingDelete(null)}
|
onCancel={() => setPendingDelete(null)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ReservationDetailDialog
|
||||||
|
reservation={viewingReservation}
|
||||||
|
onClose={() => setViewingReservation(null)}
|
||||||
|
onStatusChange={(status) => viewingReservation && handleStatusChange(viewingReservation, status)}
|
||||||
|
isUpdatingStatus={isUpdatingStatus}
|
||||||
|
onEdit={() => {
|
||||||
|
if (!viewingReservation) return
|
||||||
|
startEdit(viewingReservation)
|
||||||
|
setViewingReservation(null)
|
||||||
|
}}
|
||||||
|
onDelete={() => {
|
||||||
|
if (!viewingReservation) return
|
||||||
|
setPendingDelete(viewingReservation)
|
||||||
|
setViewingReservation(null)
|
||||||
|
}}
|
||||||
|
isDeleting={viewingReservation !== null && deletingId === viewingReservation.id}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,35 @@ function ArrowLeftIcon() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function EyeIcon() {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<circle cx="12" cy="12" r="3" stroke="currentColor" strokeWidth="1.5" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function EyeOffIcon() {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<circle cx="12" cy="12" r="3" stroke="currentColor" strokeWidth="1.5" />
|
||||||
|
<path d="M4 4l16 16" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const Route = createFileRoute('/staff/login')({
|
export const Route = createFileRoute('/staff/login')({
|
||||||
component: StaffLogin,
|
component: StaffLogin,
|
||||||
beforeLoad: async () => {
|
beforeLoad: async () => {
|
||||||
@@ -33,6 +62,7 @@ function StaffLogin() {
|
|||||||
|
|
||||||
const [email, setEmail] = useState('')
|
const [email, setEmail] = useState('')
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
@@ -78,12 +108,23 @@ function StaffLogin() {
|
|||||||
|
|
||||||
<label>
|
<label>
|
||||||
Passwort
|
Passwort
|
||||||
|
<div className="password-field">
|
||||||
<input
|
<input
|
||||||
type="password"
|
type={showPassword ? 'text' : 'password'}
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="password-toggle"
|
||||||
|
aria-label={showPassword ? 'Passwort verbergen' : 'Passwort anzeigen'}
|
||||||
|
aria-pressed={showPassword}
|
||||||
|
onClick={() => setShowPassword((show) => !show)}
|
||||||
|
>
|
||||||
|
{showPassword ? <EyeOffIcon /> : <EyeIcon />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<button type="submit" disabled={isSubmitting}>
|
<button type="submit" disabled={isSubmitting}>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { useState } from 'react'
|
import { useRef, useState } from 'react'
|
||||||
import { createFileRoute, Link, redirect, useRouter } from '@tanstack/react-router'
|
import { createFileRoute, Link, redirect, useRouter } from '@tanstack/react-router'
|
||||||
|
|
||||||
import { StaffTabs } from '#/components/staff-tabs'
|
import { StaffTabs } from '#/components/staff-tabs'
|
||||||
import { getCurrentStaffUser } from '#/lib/auth'
|
import { getCurrentStaffUser } from '#/lib/auth'
|
||||||
import { authClient } from '#/lib/auth-client'
|
import { authClient } from '#/lib/auth-client'
|
||||||
|
import { getTables, setTableMarker } from '#/lib/reservations'
|
||||||
import { getReservationSettings, updateReservationSettings } from '#/lib/settings'
|
import { getReservationSettings, updateReservationSettings } from '#/lib/settings'
|
||||||
|
import { ACCEPTED_IMAGE_TYPES, MAX_TABLE_IMAGE_BYTES, readImageAsDataUrl } from '#/lib/table-image'
|
||||||
|
|
||||||
export const Route = createFileRoute('/staff/settings')({
|
export const Route = createFileRoute('/staff/settings')({
|
||||||
component: StaffSettings,
|
component: StaffSettings,
|
||||||
@@ -19,8 +21,8 @@ export const Route = createFileRoute('/staff/settings')({
|
|||||||
return { staffUser }
|
return { staffUser }
|
||||||
},
|
},
|
||||||
loader: async ({ context }) => {
|
loader: async ({ context }) => {
|
||||||
const settings = await getReservationSettings()
|
const [settings, tables] = await Promise.all([getReservationSettings(), getTables()])
|
||||||
return { staffUser: context.staffUser, settings }
|
return { staffUser: context.staffUser, settings, tables }
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -29,21 +31,35 @@ type FormState = {
|
|||||||
reservationDurationMinutes: number
|
reservationDurationMinutes: number
|
||||||
minLeadTimeMinutes: number
|
minLeadTimeMinutes: number
|
||||||
maxLeadTimeDays: number
|
maxLeadTimeDays: number
|
||||||
|
tableDisplayMode: 'individual' | 'overview'
|
||||||
|
tableOverviewImageUrl: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_IMAGE_MB = Math.floor(MAX_TABLE_IMAGE_BYTES / 1_000_000)
|
||||||
|
|
||||||
function StaffSettings() {
|
function StaffSettings() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { staffUser, settings } = Route.useLoaderData()
|
const { staffUser, settings, tables } = Route.useLoaderData()
|
||||||
|
|
||||||
const [form, setForm] = useState<FormState>({
|
const [form, setForm] = useState<FormState>({
|
||||||
reservationIntervalMinutes: settings.reservationIntervalMinutes,
|
reservationIntervalMinutes: settings.reservationIntervalMinutes,
|
||||||
reservationDurationMinutes: settings.reservationDurationMinutes,
|
reservationDurationMinutes: settings.reservationDurationMinutes,
|
||||||
minLeadTimeMinutes: settings.minLeadTimeMinutes,
|
minLeadTimeMinutes: settings.minLeadTimeMinutes,
|
||||||
maxLeadTimeDays: settings.maxLeadTimeDays,
|
maxLeadTimeDays: settings.maxLeadTimeDays,
|
||||||
|
tableDisplayMode: settings.tableDisplayMode === 'overview' ? 'overview' : 'individual',
|
||||||
|
tableOverviewImageUrl: settings.tableOverviewImageUrl,
|
||||||
})
|
})
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [success, setSuccess] = useState<string | null>(null)
|
const [success, setSuccess] = useState<string | null>(null)
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
const overviewFileInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const [markerTableId, setMarkerTableId] = useState<string>('')
|
||||||
|
const [isPlacingMarker, setIsPlacingMarker] = useState(false)
|
||||||
|
const [markerError, setMarkerError] = useState<string | null>(null)
|
||||||
|
const [markerSuccess, setMarkerSuccess] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const markerTable = tables.find((table) => String(table.id) === markerTableId)
|
||||||
|
const tablesWithoutMarker = tables.filter((table) => table.markerX === null || table.markerY === null)
|
||||||
|
|
||||||
async function handleLogout() {
|
async function handleLogout() {
|
||||||
await authClient.signOut()
|
await authClient.signOut()
|
||||||
@@ -51,6 +67,67 @@ function StaffSettings() {
|
|||||||
await router.invalidate()
|
await router.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleOverviewImageChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
const file = event.target.files?.[0]
|
||||||
|
event.target.value = ''
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
|
if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) {
|
||||||
|
setError('Bild muss PNG, JPEG oder WebP sein')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size > MAX_TABLE_IMAGE_BYTES) {
|
||||||
|
setError(`Bild ist zu groß (max. ${MAX_IMAGE_MB} MB)`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(null)
|
||||||
|
const dataUrl = await readImageAsDataUrl(file)
|
||||||
|
setForm((f) => ({ ...f, tableOverviewImageUrl: dataUrl }))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePlaceMarker(event: React.MouseEvent<HTMLDivElement>) {
|
||||||
|
if (!markerTable) {
|
||||||
|
setMarkerError('Bitte zuerst einen Tisch für den Marker auswählen')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = event.currentTarget.getBoundingClientRect()
|
||||||
|
const x = Math.min(100, Math.max(0, ((event.clientX - rect.left) / rect.width) * 100))
|
||||||
|
const y = Math.min(100, Math.max(0, ((event.clientY - rect.top) / rect.height) * 100))
|
||||||
|
|
||||||
|
setMarkerError(null)
|
||||||
|
setMarkerSuccess(null)
|
||||||
|
setIsPlacingMarker(true)
|
||||||
|
try {
|
||||||
|
await setTableMarker({ data: { id: markerTable.id, marker: { x, y } } })
|
||||||
|
setMarkerSuccess(`Marker für ${markerTable.name} wurde gesetzt.`)
|
||||||
|
await router.invalidate()
|
||||||
|
} catch (err) {
|
||||||
|
setMarkerError(err instanceof Error ? err.message : 'Etwas ist schiefgelaufen')
|
||||||
|
} finally {
|
||||||
|
setIsPlacingMarker(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleClearMarker() {
|
||||||
|
if (!markerTable) return
|
||||||
|
|
||||||
|
setMarkerError(null)
|
||||||
|
setMarkerSuccess(null)
|
||||||
|
setIsPlacingMarker(true)
|
||||||
|
try {
|
||||||
|
await setTableMarker({ data: { id: markerTable.id, marker: null } })
|
||||||
|
setMarkerSuccess(`Marker für ${markerTable.name} wurde entfernt.`)
|
||||||
|
await router.invalidate()
|
||||||
|
} catch (err) {
|
||||||
|
setMarkerError(err instanceof Error ? err.message : 'Etwas ist schiefgelaufen')
|
||||||
|
} finally {
|
||||||
|
setIsPlacingMarker(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSubmit(event: React.FormEvent) {
|
async function handleSubmit(event: React.FormEvent) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
setError(null)
|
setError(null)
|
||||||
@@ -161,6 +238,62 @@ function StaffSettings() {
|
|||||||
Wie weit im Voraus reserviert werden kann, z. B. 60 für „höchstens 60 Tage im Voraus“.
|
Wie weit im Voraus reserviert werden kann, z. B. 60 für „höchstens 60 Tage im Voraus“.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<fieldset className="table-display-fieldset">
|
||||||
|
<legend>Tischfotos im Buchungsformular</legend>
|
||||||
|
<label className="radio-option">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="tableDisplayMode"
|
||||||
|
checked={form.tableDisplayMode === 'individual'}
|
||||||
|
onChange={() => setForm((f) => ({ ...f, tableDisplayMode: 'individual' }))}
|
||||||
|
/>
|
||||||
|
Foto des gewählten Tisches
|
||||||
|
</label>
|
||||||
|
<label className="radio-option">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="tableDisplayMode"
|
||||||
|
checked={form.tableDisplayMode === 'overview'}
|
||||||
|
onChange={() => setForm((f) => ({ ...f, tableDisplayMode: 'overview' }))}
|
||||||
|
/>
|
||||||
|
Ein Übersichtsfoto für alle Tische
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<p className="field-hint">
|
||||||
|
„Foto des gewählten Tisches“ zeigt das jeweilige Foto aus der Tischverwaltung, sobald ein
|
||||||
|
Tisch ausgewählt wird. „Übersichtsfoto“ zeigt stattdessen immer dasselbe Bild, z. B. eine
|
||||||
|
Aufnahme des ganzen Raums.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{form.tableDisplayMode === 'overview' && (
|
||||||
|
<>
|
||||||
|
<label>
|
||||||
|
Übersichtsfoto
|
||||||
|
<div className="table-image-field">
|
||||||
|
{form.tableOverviewImageUrl && (
|
||||||
|
<img className="table-image-preview" src={form.tableOverviewImageUrl} alt="" />
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
ref={overviewFileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/png,image/jpeg,image/webp"
|
||||||
|
onChange={handleOverviewImageChange}
|
||||||
|
/>
|
||||||
|
{form.tableOverviewImageUrl && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={() => setForm((f) => ({ ...f, tableOverviewImageUrl: null }))}
|
||||||
|
>
|
||||||
|
Bild entfernen
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<p className="field-hint">PNG, JPEG oder WebP, max. {MAX_IMAGE_MB} MB.</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<button type="submit" disabled={isSubmitting}>
|
<button type="submit" disabled={isSubmitting}>
|
||||||
{isSubmitting ? 'Wird gespeichert …' : 'Speichern'}
|
{isSubmitting ? 'Wird gespeichert …' : 'Speichern'}
|
||||||
</button>
|
</button>
|
||||||
@@ -169,6 +302,71 @@ function StaffSettings() {
|
|||||||
{success && <p className="message message-success">{success}</p>}
|
{success && <p className="message message-success">{success}</p>}
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{settings.tableDisplayMode === 'overview' && settings.tableOverviewImageUrl && (
|
||||||
|
<section className="staff-form-section">
|
||||||
|
<h2 className="section-heading staff-subheading">Tisch-Marker</h2>
|
||||||
|
<p className="field-hint">
|
||||||
|
Wählen Sie einen Tisch aus und klicken Sie dann auf das gespeicherte Übersichtsfoto, um
|
||||||
|
seinen Marker zu setzen oder zu verschieben.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="reservation-form">
|
||||||
|
<label>
|
||||||
|
Marker setzen für
|
||||||
|
<select value={markerTableId} onChange={(e) => setMarkerTableId(e.target.value)}>
|
||||||
|
<option value="" disabled>
|
||||||
|
Tisch auswählen
|
||||||
|
</option>
|
||||||
|
{tables.map((table) => (
|
||||||
|
<option key={table.id} value={table.id}>
|
||||||
|
{table.tableNumber === null ? table.name : `${table.tableNumber} — ${table.name}`}
|
||||||
|
{table.markerX === null || table.markerY === null ? '' : ' ✓'}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`table-overview-wrap${isPlacingMarker ? ' is-busy' : ''}`}
|
||||||
|
onClick={handlePlaceMarker}
|
||||||
|
>
|
||||||
|
<img className="table-overview-image" src={settings.tableOverviewImageUrl} alt="" />
|
||||||
|
{tables
|
||||||
|
.filter((table) => table.markerX !== null && table.markerY !== null)
|
||||||
|
.map((table) => (
|
||||||
|
<span
|
||||||
|
key={table.id}
|
||||||
|
className={`table-marker${String(table.id) === markerTableId ? ' is-active' : ''}`}
|
||||||
|
style={{ left: `${table.markerX}%`, top: `${table.markerY}%` }}
|
||||||
|
>
|
||||||
|
<span className="table-marker-label">{table.tableNumber ?? '?'}</span>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{markerTable && (markerTable.markerX !== null || markerTable.markerY !== null) && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
disabled={isPlacingMarker}
|
||||||
|
onClick={handleClearMarker}
|
||||||
|
>
|
||||||
|
Marker für {markerTable.name} entfernen
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tablesWithoutMarker.length > 0 && (
|
||||||
|
<p className="field-hint">
|
||||||
|
Noch ohne Marker: {tablesWithoutMarker.map((table) => table.name).join(', ')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{markerError && <p className="message message-error">{markerError}</p>}
|
||||||
|
{markerSuccess && <p className="message message-success">{markerSuccess}</p>}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,338 @@
|
|||||||
|
import { useRef, useState } from 'react'
|
||||||
|
import { createFileRoute, Link, redirect, useRouter } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
import { ConfirmDialog } from '#/components/confirm-dialog'
|
||||||
|
import { StaffTabs } from '#/components/staff-tabs'
|
||||||
|
import { TableDetailDialog } from '#/components/table-detail-dialog'
|
||||||
|
import { getCurrentStaffUser } from '#/lib/auth'
|
||||||
|
import { authClient } from '#/lib/auth-client'
|
||||||
|
import { createTable, deleteTable, getTables, updateTable } from '#/lib/reservations'
|
||||||
|
import { ACCEPTED_IMAGE_TYPES, MAX_TABLE_IMAGE_BYTES, readImageAsDataUrl } from '#/lib/table-image'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/staff/tables')({
|
||||||
|
component: StaffTables,
|
||||||
|
beforeLoad: async () => {
|
||||||
|
const staffUser = await getCurrentStaffUser()
|
||||||
|
if (!staffUser) {
|
||||||
|
throw redirect({ to: '/staff/login' })
|
||||||
|
}
|
||||||
|
if (staffUser.role !== 'admin') {
|
||||||
|
throw redirect({ to: '/staff' })
|
||||||
|
}
|
||||||
|
return { staffUser }
|
||||||
|
},
|
||||||
|
loader: async ({ context }) => {
|
||||||
|
const tables = await getTables()
|
||||||
|
return { staffUser: context.staffUser, tables }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
type TableRow = Awaited<ReturnType<typeof getTables>>[number]
|
||||||
|
|
||||||
|
type FormState = {
|
||||||
|
name: string
|
||||||
|
tableNumber: number
|
||||||
|
seats: number
|
||||||
|
imageUrl: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
function emptyForm(): FormState {
|
||||||
|
return { name: '', tableNumber: 1, seats: 2, imageUrl: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_IMAGE_MB = Math.floor(MAX_TABLE_IMAGE_BYTES / 1_000_000)
|
||||||
|
|
||||||
|
function StaffTables() {
|
||||||
|
const router = useRouter()
|
||||||
|
const { staffUser, tables } = Route.useLoaderData()
|
||||||
|
|
||||||
|
const [editingId, setEditingId] = useState<number | null>(null)
|
||||||
|
const [form, setForm] = useState<FormState>(emptyForm)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [success, setSuccess] = useState<string | null>(null)
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
const [deletingId, setDeletingId] = useState<number | null>(null)
|
||||||
|
const [pendingDelete, setPendingDelete] = useState<TableRow | null>(null)
|
||||||
|
const [viewingTable, setViewingTable] = useState<TableRow | null>(null)
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
function startEdit(table: TableRow) {
|
||||||
|
setEditingId(table.id)
|
||||||
|
setForm({
|
||||||
|
name: table.name,
|
||||||
|
tableNumber: table.tableNumber ?? 0,
|
||||||
|
seats: table.seats,
|
||||||
|
imageUrl: table.imageUrl,
|
||||||
|
})
|
||||||
|
setError(null)
|
||||||
|
setSuccess(null)
|
||||||
|
if (fileInputRef.current) fileInputRef.current.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelEdit() {
|
||||||
|
setEditingId(null)
|
||||||
|
setForm(emptyForm())
|
||||||
|
setError(null)
|
||||||
|
if (fileInputRef.current) fileInputRef.current.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleImageChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
const file = event.target.files?.[0]
|
||||||
|
event.target.value = ''
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
|
if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) {
|
||||||
|
setError('Bild muss PNG, JPEG oder WebP sein')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size > MAX_TABLE_IMAGE_BYTES) {
|
||||||
|
setError(`Bild ist zu groß (max. ${MAX_IMAGE_MB} MB)`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(null)
|
||||||
|
const dataUrl = await readImageAsDataUrl(file)
|
||||||
|
setForm((f) => ({ ...f, imageUrl: dataUrl }))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit(event: React.FormEvent) {
|
||||||
|
event.preventDefault()
|
||||||
|
setError(null)
|
||||||
|
setSuccess(null)
|
||||||
|
setIsSubmitting(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
name: form.name,
|
||||||
|
tableNumber: Number(form.tableNumber),
|
||||||
|
seats: Number(form.seats),
|
||||||
|
imageUrl: form.imageUrl,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editingId) {
|
||||||
|
await updateTable({ data: { id: editingId, ...payload } })
|
||||||
|
setSuccess('Tisch wurde aktualisiert.')
|
||||||
|
} else {
|
||||||
|
await createTable({ data: payload })
|
||||||
|
setSuccess('Tisch wurde angelegt.')
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelEdit()
|
||||||
|
await router.invalidate()
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Etwas ist schiefgelaufen')
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmDelete() {
|
||||||
|
const table = pendingDelete
|
||||||
|
if (!table) return
|
||||||
|
|
||||||
|
setPendingDelete(null)
|
||||||
|
setDeletingId(table.id)
|
||||||
|
setError(null)
|
||||||
|
setSuccess(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteTable({ data: { id: table.id } })
|
||||||
|
if (editingId === table.id) cancelEdit()
|
||||||
|
setSuccess('Tisch wurde gelöscht.')
|
||||||
|
await router.invalidate()
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Etwas ist schiefgelaufen')
|
||||||
|
} finally {
|
||||||
|
setDeletingId(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLogout() {
|
||||||
|
await authClient.signOut()
|
||||||
|
await router.navigate({ to: '/staff/login' })
|
||||||
|
await router.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="staff-page">
|
||||||
|
<div className="staff-nav">
|
||||||
|
<header className="staff-topbar">
|
||||||
|
<Link className="wordmark" to="/">
|
||||||
|
Steakhaus <span>bei Mirko</span>
|
||||||
|
</Link>
|
||||||
|
<div className="staff-topbar-right">
|
||||||
|
<span>Angemeldet als {staffUser.name}</span>
|
||||||
|
<button type="button" className="btn btn-secondary" onClick={handleLogout}>
|
||||||
|
Abmelden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="staff-tabs-bar">
|
||||||
|
<StaffTabs role={staffUser.role} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<main className="page staff-main">
|
||||||
|
<h1 className="section-heading">Tische</h1>
|
||||||
|
|
||||||
|
<section className="staff-form-section">
|
||||||
|
<h2 className="section-heading staff-subheading">
|
||||||
|
{editingId ? 'Tisch bearbeiten' : 'Neuer Tisch'}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<form className="reservation-form" onSubmit={handleSubmit}>
|
||||||
|
<div className="form-row">
|
||||||
|
<label>
|
||||||
|
Tischnummer
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
inputMode="numeric"
|
||||||
|
min={1}
|
||||||
|
value={form.tableNumber}
|
||||||
|
onChange={(e) => setForm((f) => ({ ...f, tableNumber: Number(e.target.value) }))}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Plätze
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
inputMode="numeric"
|
||||||
|
min={1}
|
||||||
|
max={50}
|
||||||
|
value={form.seats}
|
||||||
|
onChange={(e) => setForm((f) => ({ ...f, seats: Number(e.target.value) }))}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Name
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={form.name}
|
||||||
|
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<p className="field-hint">
|
||||||
|
Die Tischnummer wird auf dem Marker angezeigt; der Name ist nur für die Verwaltung hier.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Foto
|
||||||
|
<div className="table-image-field">
|
||||||
|
{form.imageUrl && <img className="table-image-preview" src={form.imageUrl} alt="" />}
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/png,image/jpeg,image/webp"
|
||||||
|
onChange={handleImageChange}
|
||||||
|
/>
|
||||||
|
{form.imageUrl && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={() => setForm((f) => ({ ...f, imageUrl: null }))}
|
||||||
|
>
|
||||||
|
Bild entfernen
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<p className="field-hint">
|
||||||
|
Zeigt Gästen im Buchungsformular, wie der Tisch aussieht. PNG, JPEG oder WebP, max.{' '}
|
||||||
|
{MAX_IMAGE_MB} MB.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="form-actions">
|
||||||
|
<button type="submit" disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? 'Wird gespeichert …' : editingId ? 'Speichern' : 'Anlegen'}
|
||||||
|
</button>
|
||||||
|
{editingId && (
|
||||||
|
<button type="button" className="btn btn-secondary" onClick={cancelEdit}>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <p className="message message-error">{error}</p>}
|
||||||
|
{success && <p className="message message-success">{success}</p>}
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="staff-table-section">
|
||||||
|
{tables.length === 0 ? (
|
||||||
|
<p className="empty-state">Noch keine Tische vorhanden.</p>
|
||||||
|
) : (
|
||||||
|
<div className="table-scroll">
|
||||||
|
<table className="reservations-table reservations-table-clickable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>Nr.</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Plätze</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{tables.map((table) => (
|
||||||
|
<tr
|
||||||
|
key={table.id}
|
||||||
|
className={editingId === table.id ? 'is-editing' : undefined}
|
||||||
|
onClick={() => setViewingTable(table)}
|
||||||
|
>
|
||||||
|
<td>
|
||||||
|
{table.imageUrl ? (
|
||||||
|
<img className="table-thumb" src={table.imageUrl} alt="" />
|
||||||
|
) : (
|
||||||
|
<span className="table-thumb table-thumb-empty" aria-hidden="true" />
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>{table.tableNumber ?? '—'}</td>
|
||||||
|
<td>{table.name}</td>
|
||||||
|
<td>{table.seats}</td>
|
||||||
|
<td onClick={(event) => event.stopPropagation()}>
|
||||||
|
<div className="table-actions">
|
||||||
|
<button type="button" className="btn btn-secondary" onClick={() => startEdit(table)}>
|
||||||
|
Bearbeiten
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-danger"
|
||||||
|
disabled={deletingId === table.id}
|
||||||
|
onClick={() => setPendingDelete(table)}
|
||||||
|
>
|
||||||
|
{deletingId === table.id ? 'Löschen …' : 'Löschen'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
open={pendingDelete !== null}
|
||||||
|
title="Tisch löschen"
|
||||||
|
message={pendingDelete && <>Tisch <strong>{pendingDelete.name}</strong> wirklich löschen?</>}
|
||||||
|
warning="Diese Aktion kann nicht rückgängig gemacht werden."
|
||||||
|
confirmLabel="Löschen"
|
||||||
|
danger
|
||||||
|
onConfirm={confirmDelete}
|
||||||
|
onCancel={() => setPendingDelete(null)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TableDetailDialog table={viewingTable} onClose={() => setViewingTable(null)} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { createFileRoute, Link, redirect, useRouter } from '@tanstack/react-router'
|
import { createFileRoute, Link, redirect, useRouter } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
import { ConfirmDialog } from '#/components/confirm-dialog'
|
||||||
|
import { StaffMemberDetailDialog } from '#/components/staff-member-detail-dialog'
|
||||||
import { StaffTabs } from '#/components/staff-tabs'
|
import { StaffTabs } from '#/components/staff-tabs'
|
||||||
import { getCurrentStaffUser } from '#/lib/auth'
|
import { getCurrentStaffUser } from '#/lib/auth'
|
||||||
import { authClient } from '#/lib/auth-client'
|
import { authClient } from '#/lib/auth-client'
|
||||||
|
import { phoneSchema } from '#/lib/phone'
|
||||||
import { listStaffMembers } from '#/lib/staff'
|
import { listStaffMembers } from '#/lib/staff'
|
||||||
|
|
||||||
export const Route = createFileRoute('/staff/team')({
|
export const Route = createFileRoute('/staff/team')({
|
||||||
@@ -29,12 +32,13 @@ type StaffMember = Awaited<ReturnType<typeof listStaffMembers>>[number]
|
|||||||
type FormState = {
|
type FormState = {
|
||||||
name: string
|
name: string
|
||||||
email: string
|
email: string
|
||||||
|
phone: string
|
||||||
password: string
|
password: string
|
||||||
role: 'admin' | 'user'
|
role: 'admin' | 'user'
|
||||||
}
|
}
|
||||||
|
|
||||||
function emptyForm(): FormState {
|
function emptyForm(): FormState {
|
||||||
return { name: '', email: '', password: '', role: 'user' }
|
return { name: '', email: '', phone: '', password: '', role: 'user' }
|
||||||
}
|
}
|
||||||
|
|
||||||
function StaffTeam() {
|
function StaffTeam() {
|
||||||
@@ -46,11 +50,20 @@ function StaffTeam() {
|
|||||||
const [success, setSuccess] = useState<string | null>(null)
|
const [success, setSuccess] = useState<string | null>(null)
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
const [busyId, setBusyId] = useState<string | null>(null)
|
const [busyId, setBusyId] = useState<string | null>(null)
|
||||||
|
const [viewingMember, setViewingMember] = useState<StaffMember | null>(null)
|
||||||
|
const [pendingRemove, setPendingRemove] = useState<StaffMember | null>(null)
|
||||||
|
|
||||||
async function handleCreate(event: React.FormEvent) {
|
async function handleCreate(event: React.FormEvent) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
setError(null)
|
setError(null)
|
||||||
setSuccess(null)
|
setSuccess(null)
|
||||||
|
|
||||||
|
const phoneResult = phoneSchema.safeParse(form.phone)
|
||||||
|
if (!phoneResult.success) {
|
||||||
|
setError('Bitte geben Sie eine gültige Telefonnummer an')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
|
|
||||||
const { error: createError } = await authClient.admin.createUser({
|
const { error: createError } = await authClient.admin.createUser({
|
||||||
@@ -58,6 +71,7 @@ function StaffTeam() {
|
|||||||
email: form.email,
|
email: form.email,
|
||||||
password: form.password,
|
password: form.password,
|
||||||
role: form.role,
|
role: form.role,
|
||||||
|
data: { phone: phoneResult.data },
|
||||||
})
|
})
|
||||||
|
|
||||||
if (createError) {
|
if (createError) {
|
||||||
@@ -72,11 +86,11 @@ function StaffTeam() {
|
|||||||
await router.invalidate()
|
await router.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRemove(member: StaffMember) {
|
async function confirmRemove() {
|
||||||
if (!window.confirm(`Zugang für ${member.name} wirklich entfernen?`)) {
|
const member = pendingRemove
|
||||||
return
|
if (!member) return
|
||||||
}
|
|
||||||
|
|
||||||
|
setPendingRemove(null)
|
||||||
setBusyId(member.id)
|
setBusyId(member.id)
|
||||||
setError(null)
|
setError(null)
|
||||||
setSuccess(null)
|
setSuccess(null)
|
||||||
@@ -136,6 +150,38 @@ function StaffTeam() {
|
|||||||
await router.invalidate()
|
await router.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleSetPhone(member: StaffMember) {
|
||||||
|
const newPhone = window.prompt(`Telefonnummer für ${member.name}:`, member.phone ?? '')
|
||||||
|
|
||||||
|
if (newPhone === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const phoneResult = phoneSchema.safeParse(newPhone)
|
||||||
|
if (!phoneResult.success) {
|
||||||
|
setError('Bitte geben Sie eine gültige Telefonnummer an')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setBusyId(member.id)
|
||||||
|
setError(null)
|
||||||
|
setSuccess(null)
|
||||||
|
|
||||||
|
const { error: phoneError } = await authClient.admin.updateUser({
|
||||||
|
userId: member.id,
|
||||||
|
data: { phone: phoneResult.data },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (phoneError) {
|
||||||
|
setError(phoneError.message ?? 'Etwas ist schiefgelaufen')
|
||||||
|
} else {
|
||||||
|
setSuccess(`Telefonnummer für ${member.name} wurde aktualisiert.`)
|
||||||
|
await router.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
setBusyId(null)
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSetPassword(member: StaffMember) {
|
async function handleSetPassword(member: StaffMember) {
|
||||||
const newPassword = window.prompt(`Neues Passwort für ${member.name} (mind. 8 Zeichen):`)
|
const newPassword = window.prompt(`Neues Passwort für ${member.name} (mind. 8 Zeichen):`)
|
||||||
|
|
||||||
@@ -210,6 +256,19 @@ function StaffTeam() {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Telefon
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
inputMode="tel"
|
||||||
|
pattern="[+]?[0-9()\-.\s]{6,30}"
|
||||||
|
title="Nur Ziffern, Leerzeichen und + ( ) - ."
|
||||||
|
value={form.phone}
|
||||||
|
onChange={(e) => setForm((f) => ({ ...f, phone: e.target.value }))}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label>
|
<label>
|
||||||
Passwort
|
Passwort
|
||||||
@@ -245,11 +304,12 @@ function StaffTeam() {
|
|||||||
|
|
||||||
<section className="staff-table-section">
|
<section className="staff-table-section">
|
||||||
<div className="table-scroll">
|
<div className="table-scroll">
|
||||||
<table className="reservations-table">
|
<table className="reservations-table reservations-table-clickable">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>E-Mail</th>
|
<th>E-Mail</th>
|
||||||
|
<th>Telefon</th>
|
||||||
<th>Rolle</th>
|
<th>Rolle</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
@@ -257,12 +317,13 @@ function StaffTeam() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{members.map((member) => (
|
{members.map((member) => (
|
||||||
<tr key={member.id}>
|
<tr key={member.id} onClick={() => setViewingMember(member)}>
|
||||||
<td>{member.name}</td>
|
<td>{member.name}</td>
|
||||||
<td>{member.email}</td>
|
<td>{member.email}</td>
|
||||||
|
<td>{member.phone || <span className="muted">—</span>}</td>
|
||||||
<td>{member.role === 'admin' ? 'Admin' : 'Mitarbeiter'}</td>
|
<td>{member.role === 'admin' ? 'Admin' : 'Mitarbeiter'}</td>
|
||||||
<td>{member.banned ? 'Gesperrt' : 'Aktiv'}</td>
|
<td>{member.banned ? 'Gesperrt' : 'Aktiv'}</td>
|
||||||
<td>
|
<td onClick={(event) => event.stopPropagation()}>
|
||||||
<div className="table-actions">
|
<div className="table-actions">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -272,6 +333,14 @@ function StaffTeam() {
|
|||||||
>
|
>
|
||||||
{member.role === 'admin' ? 'Zu Mitarbeiter machen' : 'Zu Admin machen'}
|
{member.role === 'admin' ? 'Zu Mitarbeiter machen' : 'Zu Admin machen'}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
disabled={busyId === member.id}
|
||||||
|
onClick={() => handleSetPhone(member)}
|
||||||
|
>
|
||||||
|
Telefon bearbeiten
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-secondary"
|
className="btn btn-secondary"
|
||||||
@@ -292,7 +361,7 @@ function StaffTeam() {
|
|||||||
type="button"
|
type="button"
|
||||||
className="btn btn-danger"
|
className="btn btn-danger"
|
||||||
disabled={busyId === member.id || member.id === staffUser.id}
|
disabled={busyId === member.id || member.id === staffUser.id}
|
||||||
onClick={() => handleRemove(member)}
|
onClick={() => setPendingRemove(member)}
|
||||||
>
|
>
|
||||||
Entfernen
|
Entfernen
|
||||||
</button>
|
</button>
|
||||||
@@ -305,6 +374,19 @@ function StaffTeam() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<StaffMemberDetailDialog member={viewingMember} onClose={() => setViewingMember(null)} />
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
open={pendingRemove !== null}
|
||||||
|
title="Zugang entfernen"
|
||||||
|
message={pendingRemove && <>Zugang für <strong>{pendingRemove.name}</strong> wirklich entfernen?</>}
|
||||||
|
warning="Diese Aktion kann nicht rückgängig gemacht werden."
|
||||||
|
confirmLabel="Entfernen"
|
||||||
|
danger
|
||||||
|
onConfirm={confirmRemove}
|
||||||
|
onCancel={() => setPendingRemove(null)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+486
-19
@@ -80,13 +80,6 @@ a {
|
|||||||
max-width: 120rem
|
max-width: 120rem
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 30rem) {
|
|
||||||
.site-header-inner {
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.wordmark {
|
.wordmark {
|
||||||
font-family: 'Fraunces', Georgia, serif;
|
font-family: 'Fraunces', Georgia, serif;
|
||||||
font-size: 1.15rem;
|
font-size: 1.15rem;
|
||||||
@@ -149,8 +142,8 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hero-flame {
|
.hero-flame {
|
||||||
width: 2.5rem;
|
width: 4rem;
|
||||||
height: 2.5rem;
|
height: 4rem;
|
||||||
margin: 0 auto 1.5rem;
|
margin: 0 auto 1.5rem;
|
||||||
color: var(--ember);
|
color: var(--ember);
|
||||||
}
|
}
|
||||||
@@ -376,6 +369,46 @@ a {
|
|||||||
anchor-name: --anchor-date-time;
|
anchor-name: --anchor-date-time;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.password-field {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-field input {
|
||||||
|
padding-right: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-toggle {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 0.3rem;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1.9rem;
|
||||||
|
height: 1.9rem;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-toggle:hover {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-toggle svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-popover-backdrop {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
.date-popover {
|
.date-popover {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
position-anchor: --anchor-date-time;
|
position-anchor: --anchor-date-time;
|
||||||
@@ -389,6 +422,52 @@ a {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 30rem) {
|
||||||
|
.date-popover-backdrop {
|
||||||
|
display: flex;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 100;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-popover {
|
||||||
|
position: static;
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
box-shadow: 0 20px 45px rgba(0, 0, 0, 0.45);
|
||||||
|
animation: confirm-dialog-fade-in 0.15s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-popover-header {
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-popover-header button {
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-popover-weekdays span,
|
||||||
|
.date-popover-grid button {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-popover-grid button {
|
||||||
|
aspect-ratio: unset;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.date-popover-header {
|
.date-popover-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -412,7 +491,7 @@ a {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-size: 0.9rem;
|
font-size: 1.2rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -482,7 +561,8 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.reservation-form input,
|
.reservation-form input,
|
||||||
.reservation-form select {
|
.reservation-form select,
|
||||||
|
.reservation-form textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
@@ -494,21 +574,68 @@ a {
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reservation-form textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 4.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.reservation-form input:focus,
|
.reservation-form input:focus,
|
||||||
.reservation-form select:focus {
|
.reservation-form select:focus,
|
||||||
|
.reservation-form textarea:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--gold);
|
border-color: var(--gold);
|
||||||
box-shadow: 0 0 0 2px rgba(201, 161, 90, 0.25);
|
box-shadow: 0 0 0 2px rgba(201, 161, 90, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Overrides the generic `.reservation-form input` box styling above — that rule
|
||||||
|
assumes text-like inputs and would otherwise stretch radios/checkboxes into
|
||||||
|
full-width bordered rectangles instead of native controls. */
|
||||||
|
.reservation-form input[type='radio'],
|
||||||
|
.reservation-form input[type='checkbox'] {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
accent-color: var(--gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-display-fieldset {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-display-fieldset legend {
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reservation-form .radio-option {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
/* `:not(.btn)` keeps this scoped to the plain submit buttons — anything already
|
/* `:not(.btn)` keeps this scoped to the plain submit buttons — anything already
|
||||||
using the `.btn` component (Abbrechen, Bearbeiten, Löschen, …) styles itself
|
using the `.btn` component (Abbrechen, Bearbeiten, Löschen, …) styles itself
|
||||||
and shouldn't be overridden here (a previous version of this rule beat
|
and shouldn't be overridden here (a previous version of this rule beat
|
||||||
`.btn-secondary` on specificity and left its hover text unreadable).
|
`.btn-secondary` on specificity and left its hover text unreadable).
|
||||||
`:not(.date-popover *):not(.date-picker-trigger)` keeps it off the
|
`:not(.date-popover *):not(.date-picker-trigger):not(.table-marker):not(.password-toggle)`
|
||||||
calendar's own buttons — they live inside this form too, and without this
|
keeps it off the calendar's own buttons, the overview-photo markers, and the
|
||||||
exclusion this rule's specificity beats the calendar's button styles. */
|
show-password toggle — they live inside this form too, and without this
|
||||||
.reservation-form button:not(.btn):not(.date-popover *):not(.date-picker-trigger) {
|
exclusion this rule's specificity beats their own button styles. */
|
||||||
|
.reservation-form
|
||||||
|
button:not(.btn):not(.date-popover *):not(.date-picker-trigger):not(.table-marker):not(.password-toggle) {
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -520,11 +647,15 @@ a {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reservation-form button:not(.btn):not(.date-popover *):not(.date-picker-trigger):hover:not(:disabled) {
|
.reservation-form
|
||||||
|
button:not(.btn):not(.date-popover *):not(.date-picker-trigger):not(.table-marker):not(.password-toggle):hover:not(
|
||||||
|
:disabled
|
||||||
|
) {
|
||||||
background: var(--gold-bright);
|
background: var(--gold-bright);
|
||||||
}
|
}
|
||||||
|
|
||||||
.reservation-form button:not(.btn):not(.date-popover *):not(.date-picker-trigger):disabled {
|
.reservation-form
|
||||||
|
button:not(.btn):not(.date-popover *):not(.date-picker-trigger):not(.table-marker):not(.password-toggle):disabled {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
@@ -667,6 +798,11 @@ a {
|
|||||||
max-width: 68rem;
|
max-width: 68rem;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0 1.5rem;
|
padding: 0 1.5rem;
|
||||||
|
overflow: scroll hidden;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.staff-tab {
|
.staff-tab {
|
||||||
@@ -988,6 +1124,10 @@ a {
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.reservations-table {
|
.reservations-table {
|
||||||
@@ -1020,11 +1160,84 @@ a {
|
|||||||
background: rgba(243, 234, 217, 0.06);
|
background: rgba(243, 234, 217, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reservations-table-clickable tbody tr:hover {
|
||||||
|
background: rgba(243, 234, 217, 0.05);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.reservations-table .muted {
|
.reservations-table .muted {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reservations-table .message-cell {
|
||||||
|
max-width: 16rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 9.5rem;
|
||||||
|
padding: 0.2rem 0.6rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.status-confirmed {
|
||||||
|
background: rgba(201, 161, 90, 0.15);
|
||||||
|
color: var(--gold-bright);
|
||||||
|
border: 1px solid rgba(201, 161, 90, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.status-no_show {
|
||||||
|
background: #3a1414;
|
||||||
|
color: #f3a9a9;
|
||||||
|
border: 1px solid #5c1f1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.status-completed {
|
||||||
|
background: #17301c;
|
||||||
|
color: #a9d9b4;
|
||||||
|
border: 1px solid #23502c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-options {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin: 0 0 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-option {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.4rem 0.75rem;
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* `.status-badge.status-option` — matches `.status-badge.status-confirmed` (etc.)
|
||||||
|
on specificity so this actually wins and overrides their `border`, rather than
|
||||||
|
losing to them by cascade order. */
|
||||||
|
.status-badge.status-option {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-option:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-option.is-active {
|
||||||
|
opacity: 1;
|
||||||
|
/* `currentColor` picks up each status's own `color` (gold/red/green) instead
|
||||||
|
of one fixed ring color that wouldn't match every status. */
|
||||||
|
box-shadow: 0 0 0 2px currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
.table-actions {
|
.table-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
@@ -1036,6 +1249,167 @@ a {
|
|||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-menu-trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-menu-trigger:hover,
|
||||||
|
.action-menu-trigger[aria-expanded='true'] {
|
||||||
|
background: var(--line);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* `top`/`right` are set inline per instance (src/components/action-menu.tsx) —
|
||||||
|
the panel is portaled to <body> and positioned from a measured rect so it
|
||||||
|
isn't clipped by `.table-scroll`'s (forced) `overflow-y: auto`. `z-index` is
|
||||||
|
above `.confirm-dialog-backdrop` (100) since the menu can now be opened from
|
||||||
|
inside the reservation detail modal, not just from a table row. */
|
||||||
|
.action-menu-panel {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 110;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.35rem;
|
||||||
|
min-width: 11rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: var(--bg-raised);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-menu-panel .btn {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
font: inherit;
|
||||||
|
padding: 0.5rem 0.7rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-thumb {
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-thumb-empty {
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px dashed var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-image-field {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-image-field input[type='file'] {
|
||||||
|
flex: 1 1 12rem;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0.4rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-image-preview {
|
||||||
|
width: 4.5rem;
|
||||||
|
height: 4.5rem;
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
object-fit: cover;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-preview {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 14rem;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A fixed aspect ratio (rather than sizing to the photo's own dimensions) so a
|
||||||
|
marker's percentage position lines up with the same spot on the image whether
|
||||||
|
it's rendered here, in the admin editor, or at a different screen width —
|
||||||
|
`object-fit: contain` (not `cover`) keeps the whole photo, and thus every
|
||||||
|
marker coordinate, on-screen instead of letting some get cropped away. */
|
||||||
|
.table-overview-wrap {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: var(--bg);
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-overview-wrap.is-busy {
|
||||||
|
cursor: wait;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-overview-image {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A classic map-pin: a square with three rounded corners (50% 50% 50% 0 leaves
|
||||||
|
the bottom-left one sharp), rotated -45deg around that exact sharp corner so
|
||||||
|
the corner itself — not the box center — lands on the `left`/`top` coordinate
|
||||||
|
and becomes the pin's tip, with the round "head" swinging up above it. */
|
||||||
|
.table-marker {
|
||||||
|
position: absolute;
|
||||||
|
width: 1.6rem;
|
||||||
|
height: 1.6rem;
|
||||||
|
transform: translate(0, -100%) rotate(-45deg);
|
||||||
|
transform-origin: 0 100%;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50% 50% 50% 0;
|
||||||
|
padding: 0;
|
||||||
|
background: var(--bg-raised);
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.45), inset 0 0 0 1.5px var(--gold);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-marker-label {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.62rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-marker.is-selected,
|
||||||
|
.table-marker.is-active {
|
||||||
|
background: var(--gold);
|
||||||
|
transform: translate(0, -100%) rotate(-45deg) scale(1.2);
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-marker.is-selected .table-marker-label,
|
||||||
|
.table-marker.is-active .table-marker-label {
|
||||||
|
color: #201709;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-color: #5c1f1f;
|
border-color: #5c1f1f;
|
||||||
@@ -1089,11 +1463,52 @@ a {
|
|||||||
margin: 0 0 0.75rem;
|
margin: 0 0 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.confirm-dialog-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-dialog-header .confirm-dialog-title {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.confirm-dialog-message {
|
.confirm-dialog-message {
|
||||||
margin: 0 0 1.5rem;
|
margin: 0 0 1.25rem;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.confirm-dialog-message strong {
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-dialog-warning {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.6rem;
|
||||||
|
margin: 0 0 1.5rem;
|
||||||
|
padding: 0.75rem 0.9rem;
|
||||||
|
border: 1px solid #5c1f1f;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
background: rgba(168, 64, 44, 0.14);
|
||||||
|
color: #f3a9a9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-dialog-warning svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 0.1rem;
|
||||||
|
color: #f3a9a9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-dialog-warning span {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
.confirm-dialog-actions {
|
.confirm-dialog-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@@ -1105,6 +1520,58 @@ a {
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reservation-detail-dialog {
|
||||||
|
max-width: 28rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reservation-detail-dialog .table-preview {
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.6rem;
|
||||||
|
margin: 0 0 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-list > div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
padding-bottom: 0.6rem;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-list dt {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-list dd {
|
||||||
|
margin: 0;
|
||||||
|
text-align: right;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-message {
|
||||||
|
margin: 0 0 1.5rem;
|
||||||
|
padding-top: 0.6rem;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-message-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-message p {
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
|||||||
Reference in New Issue
Block a user