Initial commit: Harat's Booking monorepo with deploy setup

This commit is contained in:
Eugenius-Anonymous
2026-06-08 14:36:51 +07:00
commit c97f7fd1a8
89 changed files with 10536 additions and 0 deletions
@@ -0,0 +1,193 @@
-- CreateEnum
CREATE TYPE "Role" AS ENUM ('USER', 'ADMIN');
-- CreateEnum
CREATE TYPE "ReservationStatus" AS ENUM ('PENDING', 'CONFIRMED', 'CANCELLED', 'COMPLETED');
-- CreateEnum
CREATE TYPE "DishAvailability" AS ENUM ('AVAILABLE', 'STOPPED');
-- CreateTable
CREATE TABLE "User" (
"id" SERIAL NOT NULL,
"phone" VARCHAR(20) NOT NULL,
"password" VARCHAR(255) NOT NULL,
"name" VARCHAR(100) NOT NULL,
"surname" VARCHAR(100),
"email" VARCHAR(255),
"role" "Role" NOT NULL DEFAULT 'USER',
"cardToken" VARCHAR(255),
"cardLastFour" VARCHAR(4),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Pub" (
"id" SERIAL NOT NULL,
"address" VARCHAR(255) NOT NULL,
"phone" VARCHAR(20) NOT NULL,
"name" VARCHAR(100) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Pub_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Table" (
"id" SERIAL NOT NULL,
"pubId" INTEGER NOT NULL,
"number" INTEGER NOT NULL,
"capacity" INTEGER NOT NULL,
"posX" DOUBLE PRECISION NOT NULL,
"posY" DOUBLE PRECISION NOT NULL,
"width" DOUBLE PRECISION NOT NULL DEFAULT 60,
"height" DOUBLE PRECISION NOT NULL DEFAULT 60,
"shape" VARCHAR(20) NOT NULL DEFAULT 'rect',
"zone" VARCHAR(50),
CONSTRAINT "Table_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Category" (
"id" SERIAL NOT NULL,
"title" VARCHAR(100) NOT NULL,
CONSTRAINT "Category_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Dish" (
"id" SERIAL NOT NULL,
"title" VARCHAR(100) NOT NULL,
"description" TEXT,
"categoryId" INTEGER NOT NULL,
"grams" INTEGER,
"price" DECIMAL(10,2) NOT NULL,
"photo" VARCHAR(255),
"availability" "DishAvailability" NOT NULL DEFAULT 'AVAILABLE',
CONSTRAINT "Dish_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Menu" (
"id" SERIAL NOT NULL,
"pubId" INTEGER NOT NULL,
"dishId" INTEGER NOT NULL,
CONSTRAINT "Menu_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "LunchMenu" (
"id" SERIAL NOT NULL,
"pubId" INTEGER NOT NULL,
"dishId" INTEGER NOT NULL,
"weekStart" DATE NOT NULL,
"weekEnd" DATE NOT NULL,
CONSTRAINT "LunchMenu_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Billboard" (
"id" SERIAL NOT NULL,
"title" VARCHAR(200) NOT NULL,
"artist" VARCHAR(100) NOT NULL,
"pubId" INTEGER NOT NULL,
"datetime" TIMESTAMP(3) NOT NULL,
"poster" VARCHAR(255),
CONSTRAINT "Billboard_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Reservation" (
"id" SERIAL NOT NULL,
"userId" INTEGER NOT NULL,
"pubId" INTEGER NOT NULL,
"tableId" INTEGER NOT NULL,
"date" DATE NOT NULL,
"time" TIME NOT NULL,
"numPeople" INTEGER NOT NULL,
"status" "ReservationStatus" NOT NULL DEFAULT 'PENDING',
"totalCost" DECIMAL(10,2),
"prepaid" DECIMAL(10,2),
"paymentId" VARCHAR(255),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Reservation_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ReservationDish" (
"id" SERIAL NOT NULL,
"reservationId" INTEGER NOT NULL,
"dishId" INTEGER NOT NULL,
"quantity" INTEGER NOT NULL DEFAULT 1,
"priceAtOrder" DECIMAL(10,2) NOT NULL,
CONSTRAINT "ReservationDish_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_phone_key" ON "User"("phone");
-- CreateIndex
CREATE UNIQUE INDEX "Pub_address_key" ON "Pub"("address");
-- CreateIndex
CREATE UNIQUE INDEX "Table_pubId_number_key" ON "Table"("pubId", "number");
-- CreateIndex
CREATE UNIQUE INDEX "Category_title_key" ON "Category"("title");
-- CreateIndex
CREATE UNIQUE INDEX "Menu_pubId_dishId_key" ON "Menu"("pubId", "dishId");
-- CreateIndex
CREATE UNIQUE INDEX "LunchMenu_pubId_dishId_weekStart_key" ON "LunchMenu"("pubId", "dishId", "weekStart");
-- CreateIndex
CREATE UNIQUE INDEX "ReservationDish_reservationId_dishId_key" ON "ReservationDish"("reservationId", "dishId");
-- AddForeignKey
ALTER TABLE "Table" ADD CONSTRAINT "Table_pubId_fkey" FOREIGN KEY ("pubId") REFERENCES "Pub"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Dish" ADD CONSTRAINT "Dish_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Menu" ADD CONSTRAINT "Menu_pubId_fkey" FOREIGN KEY ("pubId") REFERENCES "Pub"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Menu" ADD CONSTRAINT "Menu_dishId_fkey" FOREIGN KEY ("dishId") REFERENCES "Dish"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "LunchMenu" ADD CONSTRAINT "LunchMenu_pubId_fkey" FOREIGN KEY ("pubId") REFERENCES "Pub"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "LunchMenu" ADD CONSTRAINT "LunchMenu_dishId_fkey" FOREIGN KEY ("dishId") REFERENCES "Dish"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Billboard" ADD CONSTRAINT "Billboard_pubId_fkey" FOREIGN KEY ("pubId") REFERENCES "Pub"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Reservation" ADD CONSTRAINT "Reservation_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Reservation" ADD CONSTRAINT "Reservation_pubId_fkey" FOREIGN KEY ("pubId") REFERENCES "Pub"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Reservation" ADD CONSTRAINT "Reservation_tableId_fkey" FOREIGN KEY ("tableId") REFERENCES "Table"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ReservationDish" ADD CONSTRAINT "ReservationDish_reservationId_fkey" FOREIGN KEY ("reservationId") REFERENCES "Reservation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ReservationDish" ADD CONSTRAINT "ReservationDish_dishId_fkey" FOREIGN KEY ("dishId") REFERENCES "Dish"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
+155
View File
@@ -0,0 +1,155 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum Role {
USER
ADMIN
}
enum ReservationStatus {
PENDING
CONFIRMED
CANCELLED
COMPLETED
}
enum DishAvailability {
AVAILABLE
STOPPED
}
model User {
id Int @id @default(autoincrement())
phone String @unique @db.VarChar(20)
password String @db.VarChar(255)
name String @db.VarChar(100)
surname String? @db.VarChar(100)
email String? @db.VarChar(255)
role Role @default(USER)
cardToken String? @db.VarChar(255)
cardLastFour String? @db.VarChar(4)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
reservations Reservation[]
}
model Pub {
id Int @id @default(autoincrement())
address String @unique @db.VarChar(255)
phone String @db.VarChar(20)
name String @db.VarChar(100)
createdAt DateTime @default(now())
tables Table[]
menus Menu[]
lunchMenus LunchMenu[]
billboards Billboard[]
reservations Reservation[]
}
model Table {
id Int @id @default(autoincrement())
pubId Int
number Int
capacity Int
posX Float
posY Float
width Float @default(60)
height Float @default(60)
shape String @default("rect") @db.VarChar(20)
zone String? @db.VarChar(50)
pub Pub @relation(fields: [pubId], references: [id])
reservations Reservation[]
@@unique([pubId, number])
}
model Category {
id Int @id @default(autoincrement())
title String @unique @db.VarChar(100)
dishes Dish[]
}
model Dish {
id Int @id @default(autoincrement())
title String @db.VarChar(100)
description String? @db.Text
categoryId Int
grams Int?
price Decimal @db.Decimal(10, 2)
photo String? @db.VarChar(255)
availability DishAvailability @default(AVAILABLE)
category Category @relation(fields: [categoryId], references: [id])
menus Menu[]
lunchMenus LunchMenu[]
orderItems ReservationDish[]
}
model Menu {
id Int @id @default(autoincrement())
pubId Int
dishId Int
pub Pub @relation(fields: [pubId], references: [id])
dish Dish @relation(fields: [dishId], references: [id])
@@unique([pubId, dishId])
}
model LunchMenu {
id Int @id @default(autoincrement())
pubId Int
dishId Int
weekStart DateTime @db.Date
weekEnd DateTime @db.Date
pub Pub @relation(fields: [pubId], references: [id])
dish Dish @relation(fields: [dishId], references: [id])
@@unique([pubId, dishId, weekStart])
}
model Billboard {
id Int @id @default(autoincrement())
title String @db.VarChar(200)
artist String @db.VarChar(100)
pubId Int
datetime DateTime
poster String? @db.VarChar(255)
pub Pub @relation(fields: [pubId], references: [id])
}
model Reservation {
id Int @id @default(autoincrement())
userId Int
pubId Int
tableId Int
date DateTime @db.Date
time DateTime @db.Time()
numPeople Int
status ReservationStatus @default(PENDING)
totalCost Decimal? @db.Decimal(10, 2)
prepaid Decimal? @db.Decimal(10, 2)
paymentId String? @db.VarChar(255)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
pub Pub @relation(fields: [pubId], references: [id])
table Table @relation(fields: [tableId], references: [id])
dishes ReservationDish[]
}
model ReservationDish {
id Int @id @default(autoincrement())
reservationId Int
dishId Int
quantity Int @default(1)
priceAtOrder Decimal @db.Decimal(10, 2)
reservation Reservation @relation(fields: [reservationId], references: [id], onDelete: Cascade)
dish Dish @relation(fields: [dishId], references: [id])
@@unique([reservationId, dishId])
}
+282
View File
@@ -0,0 +1,282 @@
import { PrismaClient } from '@prisma/client';
import bcrypt from 'bcryptjs';
const prisma = new PrismaClient();
async function main() {
console.log('🌱 Seeding database...');
// ─── Categories ──────────────────────────────────────
const categories = await Promise.all([
prisma.category.upsert({ where: { title: 'Закуски' }, update: {}, create: { title: 'Закуски' } }),
prisma.category.upsert({ where: { title: 'Салаты' }, update: {}, create: { title: 'Салаты' } }),
prisma.category.upsert({ where: { title: 'Супы' }, update: {}, create: { title: 'Супы' } }),
prisma.category.upsert({ where: { title: 'Горячее' }, update: {}, create: { title: 'Горячее' } }),
prisma.category.upsert({ where: { title: 'Бургеры' }, update: {}, create: { title: 'Бургеры' } }),
prisma.category.upsert({ where: { title: 'Пицца' }, update: {}, create: { title: 'Пицца' } }),
prisma.category.upsert({ where: { title: 'Десерты' }, update: {}, create: { title: 'Десерты' } }),
prisma.category.upsert({ where: { title: 'Пиво' }, update: {}, create: { title: 'Пиво' } }),
prisma.category.upsert({ where: { title: 'Коктейли' }, update: {}, create: { title: 'Коктейли' } }),
prisma.category.upsert({ where: { title: 'Безалкогольные' }, update: {}, create: { title: 'Безалкогольные' } }),
prisma.category.upsert({ where: { title: 'Бизнес-ланч' }, update: {}, create: { title: 'Бизнес-ланч' } }),
]);
const [snacks, salads, soups, main, burgers, pizza, desserts, beer, cocktails, soft, lunch] = categories;
// ─── Pubs ────────────────────────────────────────────
const pub1 = await prisma.pub.upsert({
where: { address: 'ул. Карла Маркса, 26, Иркутск' },
update: {},
create: {
name: "Harat's Irish Pub Иркутск",
address: 'ул. Карла Маркса, 26, Иркутск',
phone: '+7 (3952) 48-68-76',
},
});
const pub2 = await prisma.pub.upsert({
where: { address: 'ул. Ленина, 15, Новосибирск' },
update: {},
create: {
name: "Harat's Irish Pub Новосибирск",
address: 'ул. Ленина, 15, Новосибирск',
phone: '+7 (383) 230-01-20',
},
});
const pub3 = await prisma.pub.upsert({
where: { address: 'Невский проспект, 88, Санкт-Петербург' },
update: {},
create: {
name: "Harat's Irish Pub Санкт-Петербург",
address: 'Невский проспект, 88, Санкт-Петербург',
phone: '+7 (812) 640-16-16',
},
});
const pub4 = await prisma.pub.upsert({
where: { address: 'пр. Мира, 81, Красноярск' },
update: {},
create: {
name: "Harat's Irish Pub Красноярск",
address: 'пр. Мира, 81, Красноярск',
phone: '+7 (391) 223-22-22',
},
});
// ─── Tables for Pub 1 (main pub with full layout) ───
const tablesData = [
// Main hall
{ pubId: pub1.id, number: 1, capacity: 2, posX: 80, posY: 100, width: 55, height: 55, shape: 'rect', zone: 'main' },
{ pubId: pub1.id, number: 2, capacity: 2, posX: 180, posY: 100, width: 55, height: 55, shape: 'rect', zone: 'main' },
{ pubId: pub1.id, number: 3, capacity: 4, posX: 280, posY: 100, width: 70, height: 55, shape: 'rect', zone: 'main' },
{ pubId: pub1.id, number: 4, capacity: 4, posX: 400, posY: 100, width: 70, height: 55, shape: 'rect', zone: 'main' },
{ pubId: pub1.id, number: 5, capacity: 6, posX: 80, posY: 220, width: 90, height: 60, shape: 'rect', zone: 'main' },
{ pubId: pub1.id, number: 6, capacity: 6, posX: 230, posY: 220, width: 90, height: 60, shape: 'rect', zone: 'main' },
{ pubId: pub1.id, number: 7, capacity: 4, posX: 380, posY: 220, width: 70, height: 55, shape: 'rect', zone: 'main' },
// Bar zone
{ pubId: pub1.id, number: 8, capacity: 2, posX: 560, posY: 80, width: 50, height: 50, shape: 'circle', zone: 'bar' },
{ pubId: pub1.id, number: 9, capacity: 2, posX: 560, posY: 160, width: 50, height: 50, shape: 'circle', zone: 'bar' },
{ pubId: pub1.id, number: 10, capacity: 2, posX: 560, posY: 240, width: 50, height: 50, shape: 'circle', zone: 'bar' },
// VIP
{ pubId: pub1.id, number: 11, capacity: 8, posX: 80, posY: 360, width: 120, height: 70, shape: 'rect', zone: 'vip' },
{ pubId: pub1.id, number: 12, capacity: 8, posX: 260, posY: 360, width: 120, height: 70, shape: 'rect', zone: 'vip' },
// Terrace
{ pubId: pub1.id, number: 13, capacity: 4, posX: 460, posY: 360, width: 65, height: 55, shape: 'rect', zone: 'terrace' },
{ pubId: pub1.id, number: 14, capacity: 4, posX: 560, posY: 360, width: 65, height: 55, shape: 'rect', zone: 'terrace' },
];
for (const t of tablesData) {
await prisma.table.upsert({
where: { pubId_number: { pubId: t.pubId, number: t.number } },
update: {},
create: t,
});
}
// Simple tables for pub2, pub3, and pub4
for (const pub of [pub2, pub3, pub4]) {
for (let i = 1; i <= 8; i++) {
await prisma.table.upsert({
where: { pubId_number: { pubId: pub.id, number: i } },
update: {},
create: {
pubId: pub.id,
number: i,
capacity: i <= 4 ? 2 : 4,
posX: 80 + ((i - 1) % 4) * 140,
posY: 100 + Math.floor((i - 1) / 4) * 140,
width: i <= 4 ? 55 : 70,
height: 55,
shape: i <= 4 ? 'circle' : 'rect',
zone: 'main',
},
});
}
}
// ─── Dishes ──────────────────────────────────────────
const dishesData = [
// Snacks
{ title: 'Ирландские колбаски', categoryId: snacks.id, grams: 250, price: 490, description: 'Ассорти из трёх видов домашних колбасок с горчичным соусом' },
{ title: 'Начос с соусами', categoryId: snacks.id, grams: 300, price: 420, description: 'Кукурузные чипсы с гуакамоле, сальсой и сырным соусом' },
{ title: 'Луковые кольца', categoryId: snacks.id, grams: 200, price: 350, description: 'Хрустящие луковые кольца в пивном кляре' },
{ title: 'Куриные крылья BBQ', categoryId: snacks.id, grams: 350, price: 520, description: 'Крылышки в фирменном соусе BBQ с сельдереем' },
{ title: 'Картофель фри', categoryId: snacks.id, grams: 250, price: 280, description: 'Золотистый картофель фри с паприкой' },
{ title: 'Сырные палочки', categoryId: snacks.id, grams: 200, price: 390, description: 'Моцарелла в панировке с томатным соусом' },
// Salads
{ title: 'Цезарь с курицей', categoryId: salads.id, grams: 280, price: 450, description: 'Классический салат с куриной грудкой, пармезаном и гренками' },
{ title: 'Греческий', categoryId: salads.id, grams: 260, price: 380, description: 'Свежие овощи с фетой и оливковым маслом' },
{ title: 'Коул-слоу', categoryId: salads.id, grams: 200, price: 250, description: 'Капустный салат с морковью в сливочной заправке' },
// Soups
{ title: 'Ирландское рагу', categoryId: soups.id, grams: 350, price: 420, description: 'Традиционное рагу из баранины с картофелем и овощами' },
{ title: 'Крем-суп из шампиньонов', categoryId: soups.id, grams: 300, price: 350, description: 'Нежный крем-суп с гренками и трюфельным маслом' },
{ title: 'Томатный суп', categoryId: soups.id, grams: 300, price: 320, description: 'Густой томатный суп с базиликом' },
// Main
{ title: 'Стейк Рибай', categoryId: main.id, grams: 300, price: 1490, description: 'Стейк рибай medium rare с овощами гриль' },
{ title: 'Fish & Chips', categoryId: main.id, grams: 350, price: 590, description: 'Треска в пивном кляре с картофелем фри и соусом тартар' },
{ title: 'Shepherd\'s Pie', categoryId: main.id, grams: 400, price: 520, description: 'Пастуший пирог с бараниной и картофельным пюре' },
{ title: 'Свиная рулька', categoryId: main.id, grams: 500, price: 890, description: 'Томлёная свиная рулька с квашеной капустой' },
// Burgers
{ title: 'Classic Burger', categoryId: burgers.id, grams: 350, price: 550, description: 'Говяжья котлета, чеддер, томат, салат, фирменный соус' },
{ title: 'Irish Burger', categoryId: burgers.id, grams: 400, price: 650, description: 'Двойная котлета, бекон, яйцо, жареный лук, BBQ соус' },
{ title: 'Chicken Burger', categoryId: burgers.id, grams: 330, price: 490, description: 'Куриная котлета, авокадо, руккола, медово-горчичный соус' },
// Pizza
{ title: 'Маргарита', categoryId: pizza.id, grams: 450, price: 490, description: 'Томатный соус, моцарелла, базилик' },
{ title: 'Пепперони', categoryId: pizza.id, grams: 480, price: 590, description: 'Томатный соус, моцарелла, пепперони' },
{ title: 'Четыре сыра', categoryId: pizza.id, grams: 460, price: 620, description: 'Моцарелла, горгонзола, пармезан, дор блю' },
// Desserts
{ title: 'Чизкейк', categoryId: desserts.id, grams: 180, price: 350, description: 'Классический нью-йоркский чизкейк' },
{ title: 'Брауни', categoryId: desserts.id, grams: 150, price: 320, description: 'Шоколадный брауни с ванильным мороженым' },
{ title: 'Apple Crumble', categoryId: desserts.id, grams: 200, price: 380, description: 'Тёплый яблочный крамбл с корицей и мороженым' },
// Beer
{ title: 'Guinness (0.5)', categoryId: beer.id, grams: 500, price: 450, description: 'Ирландский тёмный стаут' },
{ title: 'Kilkenny (0.5)', categoryId: beer.id, grams: 500, price: 420, description: 'Ирландский красный эль' },
{ title: 'Harp Lager (0.5)', categoryId: beer.id, grams: 500, price: 380, description: 'Ирландский лагер' },
{ title: 'Крафт IPA (0.4)', categoryId: beer.id, grams: 400, price: 390, description: 'Крафтовый индийский пэйл эль' },
// Cocktails
{ title: 'Irish Coffee', categoryId: cocktails.id, grams: 250, price: 450, description: 'Кофе, ирландский виски, сливки' },
{ title: 'Whiskey Sour', categoryId: cocktails.id, grams: 200, price: 520, description: 'Виски, лимонный сок, сахарный сироп, яичный белок' },
{ title: 'Mojito', categoryId: cocktails.id, grams: 300, price: 480, description: 'Ром, лайм, мята, содовая, сахар' },
{ title: 'Long Island', categoryId: cocktails.id, grams: 350, price: 590, description: 'Водка, ром, текила, джин, трипл сек, кола' },
// Soft drinks
{ title: 'Лимонад домашний', categoryId: soft.id, grams: 400, price: 250, description: 'Свежевыжатый лимонад с мятой' },
{ title: 'Морс клюквенный', categoryId: soft.id, grams: 400, price: 220, description: 'Домашний морс из свежей клюквы' },
{ title: 'Сок апельсиновый', categoryId: soft.id, grams: 300, price: 280, description: 'Свежевыжатый апельсиновый сок' },
// Lunch
{ title: 'Ланч: Борщ + хлеб', categoryId: lunch.id, grams: 350, price: 220, description: 'Домашний борщ с пампушкой' },
{ title: 'Ланч: Котлета с пюре', categoryId: lunch.id, grams: 300, price: 280, description: 'Куриная котлета с картофельным пюре и подливой' },
{ title: 'Ланч: Паста болоньезе', categoryId: lunch.id, grams: 300, price: 260, description: 'Спагетти с мясным соусом болоньезе' },
{ title: 'Ланч: Комплекс (суп + горячее + напиток)', categoryId: lunch.id, grams: null, price: 390, description: 'Суп дня, горячее дня, компот' },
];
const createdDishes: any[] = [];
for (const d of dishesData) {
const dish = await prisma.dish.upsert({
where: { id: createdDishes.length + 1 },
update: {},
create: d,
});
createdDishes.push(dish);
}
// ─── Menu (link dishes to pubs, except lunch category) ──
for (const pub of [pub1, pub2, pub3, pub4]) {
for (const dish of createdDishes) {
if (dish.categoryId === lunch.id) continue; // lunch is separate
await prisma.menu.upsert({
where: { pubId_dishId: { pubId: pub.id, dishId: dish.id } },
update: {},
create: { pubId: pub.id, dishId: dish.id },
});
}
}
// ─── Lunch Menu (current week) ─────────────────────
const now = new Date();
const monday = new Date(now);
monday.setDate(now.getDate() - ((now.getDay() + 6) % 7));
monday.setHours(0, 0, 0, 0);
const sunday = new Date(monday);
sunday.setDate(monday.getDate() + 6);
const lunchDishes = createdDishes.filter(d => d.categoryId === lunch.id);
for (const pub of [pub1, pub2, pub3, pub4]) {
for (const dish of lunchDishes) {
await prisma.lunchMenu.upsert({
where: { pubId_dishId_weekStart: { pubId: pub.id, dishId: dish.id, weekStart: monday } },
update: {},
create: {
pubId: pub.id,
dishId: dish.id,
weekStart: monday,
weekEnd: sunday,
},
});
}
}
// ─── Billboard ─────────────────────────────────────
const billboards = [
{ title: 'Live Rock Night', artist: 'The Wild Rovers', pubId: pub1.id, datetime: new Date('2026-04-12T20:00:00'), poster: null },
{ title: 'Jazz Friday', artist: 'Smooth Quartet', pubId: pub1.id, datetime: new Date('2026-04-11T19:30:00'), poster: null },
{ title: 'Irish Folk Evening', artist: 'Celtic Storm', pubId: pub1.id, datetime: new Date('2026-04-13T18:00:00'), poster: null },
{ title: 'Stand-Up Comedy', artist: 'Андрей Бебуришвили', pubId: pub2.id, datetime: new Date('2026-04-12T19:00:00'), poster: null },
{ title: 'Karaoke Night', artist: 'DJ Shamrock', pubId: pub2.id, datetime: new Date('2026-04-10T21:00:00'), poster: null },
{ title: 'Acoustic Session', artist: 'Indie Folk Band', pubId: pub3.id, datetime: new Date('2026-04-11T20:00:00'), poster: null },
];
for (const b of billboards) {
await prisma.billboard.create({ data: b });
}
// ─── Admin User ────────────────────────────────────
const hashedPassword = await bcrypt.hash('admin123', 12);
await prisma.user.upsert({
where: { phone: '+79001234567' },
update: {},
create: {
phone: '+79001234567',
password: hashedPassword,
name: 'Администратор',
role: 'ADMIN',
email: 'admin@harats.com',
},
});
// Demo user
const userPassword = await bcrypt.hash('user123', 12);
await prisma.user.upsert({
where: { phone: '+79009876543' },
update: {},
create: {
phone: '+79009876543',
password: userPassword,
name: 'Иван',
surname: 'Петров',
role: 'USER',
email: 'ivan@example.com',
},
});
console.log('✅ Seed complete!');
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});