feat: добавление новых файлов

modified:   client/package.json
modified:   client/src/components/Footer.tsx
new file:   client/src/components/LocalizedDateInput.tsx
new file:   client/src/components/LocalizedTimeInput.tsx
modified:   client/src/index.css
new file:   client/src/types/html2pdf.d.ts
new file:   client/src/utils/dateTimeFormat.ts
new file:   client/src/utils/receiptPdf.ts
modified:   server/prisma/seed.ts
This commit is contained in:
2026-06-25 01:49:46 +07:00
parent fb8c7ce76a
commit 5fd0f04fdc
9 changed files with 553 additions and 20 deletions
+2 -1
View File
@@ -17,7 +17,8 @@
"react-konva": "^19.0.0", "react-konva": "^19.0.0",
"react-router-dom": "^7.1.0", "react-router-dom": "^7.1.0",
"zustand": "^5.0.0", "zustand": "^5.0.0",
"@tanstack/react-query": "^5.62.0" "@tanstack/react-query": "^5.62.0",
"html2pdf.js": "^0.10.2"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^19.0.0", "@types/react": "^19.0.0",
+4 -5
View File
@@ -12,7 +12,7 @@ export const Footer: React.FC = () => {
<div className="grid grid-3 gap-xl"> <div className="grid grid-3 gap-xl">
<div> <div>
<h4 style={{ color: 'var(--color-gold-500)', marginBottom: 'var(--space-md)' }}>Harat's Irish Pub</h4> <h4 style={{ color: 'var(--color-gold-500)', marginBottom: 'var(--space-md)' }}>Harat's Irish Pub</h4>
<p className="text-sm text-secondary" style={{ marginBottom: 'var(--space-sm)' }}>ООО «Хэрат'с»</p> <p className="text-sm text-secondary" style={{ marginBottom: 'var(--space-sm)' }}>ООО «Бар-С»</p>
<p className="text-sm text-secondary"> <p className="text-sm text-secondary">
Настоящий ирландский паб с атмосферой "cozy chaos". Место, где нет правил, кроме одного — отдыхать душой. Настоящий ирландский паб с атмосферой "cozy chaos". Место, где нет правил, кроме одного — отдыхать душой.
</p> </p>
@@ -21,11 +21,11 @@ export const Footer: React.FC = () => {
<div> <div>
<h4 style={{ color: 'var(--color-gold-500)', marginBottom: 'var(--space-md)' }}>Контакты</h4> <h4 style={{ color: 'var(--color-gold-500)', marginBottom: 'var(--space-md)' }}>Контакты</h4>
<ul style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-sm)' }} className="text-sm text-secondary"> <ul style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-sm)' }} className="text-sm text-secondary">
<li>📞 8 (800) 775-03-39 (Customer Service)</li> <li>📞 8 (800) 775-03-39 </li>
<li>✉️ <a href="mailto:info@harats.com" style={{ color: 'var(--color-green-400)' }}>info@harats.com</a></li> <li>✉️ <a href="mailto:info@harats.com" style={{ color: 'var(--color-green-400)' }}>info@harats.com</a></li>
<li style={{ marginTop: 'var(--space-sm)' }}> <li style={{ marginTop: 'var(--space-sm)' }}>
📍 Главный офис:<br /> 📍 Главный офис:<br />
664033, Россия, Иркутская область, Иркутск, ул. Майская, 20 660075, г.Красноярск, ул.Красной Гвардии 24, помещение 28
</li> </li>
</ul> </ul>
</div> </div>
@@ -33,8 +33,7 @@ export const Footer: React.FC = () => {
<div> <div>
<h4 style={{ color: 'var(--color-gold-500)', marginBottom: 'var(--space-md)' }}>Информация</h4> <h4 style={{ color: 'var(--color-gold-500)', marginBottom: 'var(--space-md)' }}>Информация</h4>
<ul style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-sm)' }} className="text-sm text-secondary"> <ul style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-sm)' }} className="text-sm text-secondary">
<li><a href="#" style={{ transition: 'color var(--transition-fast)' }} onMouseOver={(e) => e.currentTarget.style.color='white'} onMouseOut={(e) => e.currentTarget.style.color=''}>О компании</a></li> <li><a href="https://www.rusprofile.ru/id/6098580" style={{ transition: 'color var(--transition-fast)' }} onMouseOver={(e) => e.currentTarget.style.color='white'} onMouseOut={(e) => e.currentTarget.style.color=''}>О компании</a></li>
<li><a href="#" style={{ transition: 'color var(--transition-fast)' }} onMouseOver={(e) => e.currentTarget.style.color='white'} onMouseOut={(e) => e.currentTarget.style.color=''}>Франшиза</a></li>
<li><a href="#" style={{ transition: 'color var(--transition-fast)' }} onMouseOver={(e) => e.currentTarget.style.color='white'} onMouseOut={(e) => e.currentTarget.style.color=''}>Поставщикам</a></li> <li><a href="#" style={{ transition: 'color var(--transition-fast)' }} onMouseOver={(e) => e.currentTarget.style.color='white'} onMouseOut={(e) => e.currentTarget.style.color=''}>Поставщикам</a></li>
<li><a href="#" style={{ transition: 'color var(--transition-fast)' }} onMouseOver={(e) => e.currentTarget.style.color='white'} onMouseOut={(e) => e.currentTarget.style.color=''}>Реклама</a></li> <li><a href="#" style={{ transition: 'color var(--transition-fast)' }} onMouseOver={(e) => e.currentTarget.style.color='white'} onMouseOut={(e) => e.currentTarget.style.color=''}>Реклама</a></li>
</ul> </ul>
@@ -0,0 +1,47 @@
import React, { useId, useRef } from 'react';
import { isoDateToDisplay } from '@/utils/dateTimeFormat';
interface LocalizedDateInputProps {
label: string;
value: string;
min?: string;
onChange: (value: string) => void;
}
export const LocalizedDateInput: React.FC<LocalizedDateInputProps> = ({
label,
value,
min,
onChange,
}) => {
const inputId = useId();
const inputRef = useRef<HTMLInputElement>(null);
const displayValue = value ? isoDateToDisplay(value) : 'дд/мм/гггг';
return (
<div className="input-group">
<label htmlFor={inputId} className="input-label">{label}</label>
<div
className="localized-picker-input"
onClick={() => inputRef.current?.showPicker?.()}
>
<span
className={`localized-picker-input__overlay ${value ? '' : 'is-placeholder'}`}
aria-hidden="true"
>
{displayValue}
</span>
<input
id={inputId}
ref={inputRef}
type="date"
className="input localized-picker-input__native"
lang="ru-RU"
value={value}
min={min}
onChange={(e) => onChange(e.target.value)}
/>
</div>
</div>
);
};
@@ -0,0 +1,129 @@
import React, { useEffect, useId, useRef, useState } from 'react';
import { formatTime24 } from '@/utils/dateTimeFormat';
interface LocalizedTimeInputProps {
label: string;
value: string;
onChange: (value: string) => void;
}
const HOURS = Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0'));
const MINUTES = Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0'));
function parseTime(value: string): { hours: string; minutes: string } {
if (!/^\d{2}:\d{2}$/.test(value)) {
return { hours: '12', minutes: '00' };
}
const [hours, minutes] = value.split(':');
return { hours, minutes };
}
export const LocalizedTimeInput: React.FC<LocalizedTimeInputProps> = ({
label,
value,
onChange,
}) => {
const inputId = useId();
const containerRef = useRef<HTMLDivElement>(null);
const hoursRef = useRef<HTMLDivElement>(null);
const minutesRef = useRef<HTMLDivElement>(null);
const [open, setOpen] = useState(false);
const parsed = parseTime(value);
const [draftHours, setDraftHours] = useState(parsed.hours);
const [draftMinutes, setDraftMinutes] = useState(parsed.minutes);
useEffect(() => {
const next = parseTime(value);
setDraftHours(next.hours);
setDraftMinutes(next.minutes);
}, [value]);
useEffect(() => {
if (!open) return;
const handleClickOutside = (event: MouseEvent) => {
if (!containerRef.current?.contains(event.target as Node)) {
setOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [open]);
useEffect(() => {
if (!open) return;
requestAnimationFrame(() => {
hoursRef.current?.querySelector('.time-picker__option.is-selected')?.scrollIntoView({ block: 'center' });
minutesRef.current?.querySelector('.time-picker__option.is-selected')?.scrollIntoView({ block: 'center' });
});
}, [open, draftHours, draftMinutes]);
const commit = (hours: string, minutes: string) => {
onChange(`${hours}:${minutes}`);
};
const selectHour = (hours: string) => {
setDraftHours(hours);
commit(hours, draftMinutes);
};
const selectMinute = (minutes: string) => {
setDraftMinutes(minutes);
commit(draftHours, minutes);
};
const displayValue = value ? formatTime24(value) : 'чч:мм';
return (
<div className="input-group" ref={containerRef}>
<label htmlFor={inputId} className="input-label">{label}</label>
<div className="time-picker">
<button
id={inputId}
type="button"
className="input time-picker__trigger"
aria-haspopup="listbox"
aria-expanded={open}
onClick={() => setOpen((prev) => !prev)}
>
<span className={value ? '' : 'time-picker__placeholder'}>{displayValue}</span>
</button>
{open && (
<div className="time-picker__dropdown" role="listbox" aria-label="Выбор времени">
<div className="time-picker__column" ref={hoursRef} aria-label="Часы">
{HOURS.map((hour) => (
<button
key={hour}
type="button"
role="option"
aria-selected={hour === draftHours}
className={`time-picker__option ${hour === draftHours ? 'is-selected' : ''}`}
onClick={() => selectHour(hour)}
>
{hour}
</button>
))}
</div>
<div className="time-picker__column" ref={minutesRef} aria-label="Минуты">
{MINUTES.map((minute) => (
<button
key={minute}
type="button"
role="option"
aria-selected={minute === draftMinutes}
className={`time-picker__option ${minute === draftMinutes ? 'is-selected' : ''}`}
onClick={() => selectMinute(minute)}
>
{minute}
</button>
))}
</div>
</div>
)}
</div>
</div>
);
};
+116
View File
@@ -320,6 +320,122 @@ select.input {
padding-right: 40px; padding-right: 40px;
} }
.localized-picker-input {
position: relative;
cursor: pointer;
}
.localized-picker-input__overlay {
position: absolute;
inset: 0;
z-index: 2;
display: flex;
align-items: center;
padding: 12px 16px;
pointer-events: none;
color: var(--color-text-primary);
font-size: 0.9375rem;
}
.localized-picker-input__overlay.is-placeholder {
color: var(--color-text-muted);
}
.localized-picker-input__native {
position: relative;
z-index: 1;
color: transparent;
caret-color: transparent;
}
.localized-picker-input__native::-webkit-datetime-edit,
.localized-picker-input__native::-webkit-datetime-edit-fields-wrapper,
.localized-picker-input__native::-webkit-datetime-edit-text,
.localized-picker-input__native::-webkit-datetime-edit-month-field,
.localized-picker-input__native::-webkit-datetime-edit-day-field,
.localized-picker-input__native::-webkit-datetime-edit-year-field,
.localized-picker-input__native::-webkit-datetime-edit-hour-field,
.localized-picker-input__native::-webkit-datetime-edit-minute-field,
.localized-picker-input__native::-webkit-datetime-edit-second-field,
.localized-picker-input__native::-webkit-datetime-edit-ampm-field {
color: transparent;
}
.localized-picker-input__native::-webkit-calendar-picker-indicator {
cursor: pointer;
opacity: 1;
}
.time-picker {
position: relative;
}
.time-picker__trigger {
display: flex;
align-items: center;
width: 100%;
text-align: left;
cursor: pointer;
padding-right: 40px;
background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='12' cy='12' r='9' stroke='%23a09e94' stroke-width='1.5'/%3E%3Cpath d='M12 7v5l3 2' stroke='%23a09e94' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 14px center;
}
.time-picker__placeholder {
color: var(--color-text-muted);
}
.time-picker__dropdown {
position: absolute;
top: calc(100% + 4px);
left: 0;
z-index: 100;
display: flex;
min-width: 160px;
max-width: 100%;
background: #ffffff;
border-radius: var(--radius-md);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
overflow: hidden;
}
.time-picker__column {
flex: 1;
max-height: 220px;
overflow-y: auto;
scroll-snap-type: y mandatory;
overscroll-behavior: contain;
}
.time-picker__column:first-child {
border-right: 1px solid #e5e5e5;
}
.time-picker__option {
display: block;
width: 100%;
padding: 8px 12px;
text-align: center;
font-size: 0.9375rem;
color: #1a1a1a;
scroll-snap-align: center;
transition: background-color var(--transition-base);
}
.time-picker__option:hover {
background: #f0f0f0;
}
.time-picker__option.is-selected {
background: #0b74de;
color: #ffffff;
}
.time-picker__option.is-selected:hover {
background: #0b74de;
}
/* ─── Card ───────────────────────────────── */ /* ─── Card ───────────────────────────────── */
.card { .card {
background: var(--color-bg-card); background: var(--color-bg-card);
+18
View File
@@ -0,0 +1,18 @@
declare module 'html2pdf.js' {
interface Html2PdfOptions {
margin?: number | number[];
filename?: string;
image?: { type?: string; quality?: number };
html2canvas?: Record<string, unknown>;
jsPDF?: Record<string, unknown>;
}
interface Html2PdfWorker {
set(options: Html2PdfOptions): Html2PdfWorker;
from(element: HTMLElement): Html2PdfWorker;
save(): Promise<void>;
}
function html2pdf(): Html2PdfWorker;
export default html2pdf;
}
+25
View File
@@ -0,0 +1,25 @@
export function isoDateToDisplay(isoDate: string): string {
if (!isoDate) return '';
const [year, month, day] = isoDate.split('-');
if (!year || !month || !day) return '';
return `${day.padStart(2, '0')}/${month.padStart(2, '0')}/${year}`;
}
export function formatTime24(time: string): string {
if (!time) return '';
const [hours, minutes] = time.split(':');
if (!hours || minutes === undefined) return '';
return `${hours.padStart(2, '0')}:${minutes.padStart(2, '0')}`;
}
export function formatTimeInput(raw: string): string {
const digits = raw.replace(/\D/g, '').slice(0, 4);
if (digits.length <= 2) return digits;
return `${digits.slice(0, 2)}:${digits.slice(2)}`;
}
export function isValidTime24(time: string): boolean {
if (!/^\d{2}:\d{2}$/.test(time)) return false;
const [hours, minutes] = time.split(':').map(Number);
return hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59;
}
+188
View File
@@ -0,0 +1,188 @@
import html2pdf from 'html2pdf.js';
export interface ReceiptDish {
title: string;
quantity: number;
price: number;
}
export interface ReceiptData {
reservationId: number;
paymentId?: string;
pubName: string;
pubAddress: string;
date: string;
time: string;
tableNumber: number;
numPeople: number;
customerName: string;
dishes: ReceiptDish[];
totalCost: number;
prepaid: number;
}
function formatDate(dateStr: string): string {
const [year, month, day] = dateStr.split('-').map(Number);
return new Intl.DateTimeFormat('ru-RU', {
day: 'numeric',
month: 'long',
year: 'numeric',
}).format(new Date(year, month - 1, day));
}
function formatPrice(amount: number): string {
return `${amount.toLocaleString('ru-RU')}`;
}
function buildReceiptHtml(data: ReceiptData): string {
const dishRows = data.dishes
.map(
(d) => `
<tr>
<td style="padding:8px 12px;border-bottom:1px solid #e5e5e5;">${d.title}</td>
<td style="padding:8px 12px;border-bottom:1px solid #e5e5e5;text-align:center;">${d.quantity}</td>
<td style="padding:8px 12px;border-bottom:1px solid #e5e5e5;text-align:right;">${formatPrice(d.price)}</td>
<td style="padding:8px 12px;border-bottom:1px solid #e5e5e5;text-align:right;">${formatPrice(d.price * d.quantity)}</td>
</tr>`
)
.join('');
const issuedAt = new Intl.DateTimeFormat('ru-RU', {
day: 'numeric',
month: 'long',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
}).format(new Date());
return `
<div style="font-family:Arial,sans-serif;color:#1a1a1a;line-height:1.5;">
<div style="text-align:center;margin-bottom:24px;">
<div style="font-size:24px;font-weight:bold;color:#b8941f;">Harat's Irish Pub</div>
<div style="font-size:16px;color:#2eaa66;margin-top:4px;">Чек об оплате предзаказа</div>
</div>
<div style="margin-bottom:20px;padding:16px;background:#f9f9f9;border-radius:8px;">
<div style="font-size:13px;color:#666;margin-bottom:8px;">Номер брони: <strong style="color:#1a1a1a;">#${data.reservationId}</strong></div>
${data.paymentId ? `<div style="font-size:13px;color:#666;margin-bottom:8px;">ID платежа: <strong style="color:#1a1a1a;">${data.paymentId}</strong></div>` : ''}
<div style="font-size:13px;color:#666;">Дата выдачи чека: <strong style="color:#1a1a1a;">${issuedAt}</strong></div>
</div>
<div style="margin-bottom:20px;">
<div style="font-size:14px;font-weight:bold;color:#b8941f;margin-bottom:8px;">Информация о бронировании</div>
<table style="width:100%;font-size:13px;border-collapse:collapse;">
<tr><td style="padding:4px 0;color:#000000;width:140px;">Паб:</td><td style="padding:4px 0;"><strong>${data.pubName}</strong></td></tr>
<tr><td style="padding:4px 0;color:#000000;">Адрес:</td><td style="padding:4px 0;">${data.pubAddress}</td></tr>
<tr><td style="padding:4px 0;color:#000000;">Дата:</td><td style="padding:4px 0;">${formatDate(data.date)}</td></tr>
<tr><td style="padding:4px 0;color:#000000;">Время:</td><td style="padding:4px 0;">${data.time}</td></tr>
<tr><td style="padding:4px 0;color:#000000;">Стол:</td><td style="padding:4px 0;">№${data.tableNumber}</td></tr>
<tr><td style="padding:4px 0;color:#000000;">Гостей:</td><td style="padding:4px 0;">${data.numPeople}</td></tr>
<tr><td style="padding:4px 0;color:#000000;">Гость:</td><td style="padding:4px 0;">${data.customerName}</td></tr>
</table>
</div>
${
data.dishes.length > 0
? `
<div style="margin-bottom:20px;">
<div style="font-size:14px;font-weight:bold;color:#b8941f;margin-bottom:8px;">Предзаказ блюд</div>
<table style="width:100%;font-size:13px;border-collapse:collapse;">
<thead>
<tr style="background:#f0f0f0;">
<th style="padding:8px 12px;text-align:left;">Блюдо</th>
<th style="padding:8px 12px;text-align:center;width:60px;">Кол-во</th>
<th style="padding:8px 12px;text-align:right;width:90px;">Цена</th>
<th style="padding:8px 12px;text-align:right;width:90px;">Сумма</th>
</tr>
</thead>
<tbody>${dishRows}</tbody>
</table>
</div>
<div style="margin-top:16px;padding-top:12px;border-top:2px solid #b8941f;">
<table style="width:100%;font-size:14px;">
<tr>
<td style="padding:4px 0;">Стоимость предзаказа:</td>
<td style="padding:4px 0;text-align:right;font-weight:bold;">${formatPrice(data.totalCost)}</td>
</tr>
<tr>
<td style="padding:4px 0;color:#2eaa66;font-weight:bold;">Оплачено (предоплата 50%):</td>
<td style="padding:4px 0;text-align:right;font-weight:bold;color:#2eaa66;">${formatPrice(data.prepaid)}</td>
</tr>
<tr>
<td style="padding:4px 0;color:#666;font-size:12px;">К оплате в пабе:</td>
<td style="padding:4px 0;text-align:right;color:#666;font-size:12px;">${formatPrice(data.totalCost - data.prepaid)}</td>
</tr>
</table>
</div>`
: ''
}
<div style="margin-top:32px;text-align:center;font-size:11px;color:#999;">
Спасибо за бронирование! По вопросам: 8 (800) 775-03-39
</div>
</div>
`;
}
export async function downloadReceiptPdf(data: ReceiptData): Promise<void> {
const container = document.createElement('div');
container.style.cssText =
'position:fixed;left:-9999px;top:0;width:595px;padding:40px;background:#ffffff;';
container.innerHTML = buildReceiptHtml(data);
document.body.appendChild(container);
try {
await html2pdf()
.set({
margin: [10, 10, 10, 10],
filename: `receipt-${data.reservationId}.pdf`,
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, useCORS: true },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' },
})
.from(container)
.save();
} finally {
document.body.removeChild(container);
}
}
export function reservationToReceiptData(
reservation: {
id: number;
pub: { name: string; address: string };
table: { number: number };
date: string;
time: string;
numPeople: number;
totalCost: number | null;
prepaid: number | null;
dishes: Array<{
dish?: { title: string };
quantity: number;
priceAtOrder: number;
}>;
},
customerName: string,
paymentId?: string
): ReceiptData {
return {
reservationId: reservation.id,
paymentId,
pubName: reservation.pub.name,
pubAddress: reservation.pub.address,
date: reservation.date,
time: reservation.time,
tableNumber: reservation.table.number,
numPeople: reservation.numPeople,
customerName,
dishes: reservation.dishes.map((d) => ({
title: d.dish?.title ?? 'Блюдо',
quantity: d.quantity,
price: d.priceAtOrder,
})),
totalCost: reservation.totalCost ?? 0,
prepaid: reservation.prepaid ?? 0,
};
}
+24 -14
View File
@@ -25,32 +25,32 @@ async function main() {
// ─── Pubs ──────────────────────────────────────────── // ─── Pubs ────────────────────────────────────────────
const pub1 = await prisma.pub.upsert({ const pub1 = await prisma.pub.upsert({
where: { address: 'ул. Карла Маркса, 26, Иркутск' }, where: { address: 'пр. Мира, 7А, Красноярск' },
update: {}, update: {},
create: { create: {
name: "Harat's Irish Pub Иркутск", name: "Harat's Irish Pub Красноярск",
address: 'ул. Карла Маркса, 26, Иркутск', address: 'пр. Мира, 7А, Красноярск',
phone: '+7 (3952) 48-68-76', phone: '+7 (391) 227-78-18',
}, },
}); });
const pub2 = await prisma.pub.upsert({ const pub2 = await prisma.pub.upsert({
where: { address: 'ул. Ленина, 15, Новосибирск' }, where: { address: 'ул. Красной Гвардии, 24, Красноярск' },
update: {}, update: {},
create: { create: {
name: "Harat's Irish Pub Новосибирск", name: "Harat's Irish Pub Красноярск",
address: 'ул. Ленина, 15, Новосибирск', address: 'ул. Красной Гвардии, 24, Красноярск',
phone: '+7 (383) 230-01-20', phone: '+7 (391) 214-68-17',
}, },
}); });
const pub3 = await prisma.pub.upsert({ const pub3 = await prisma.pub.upsert({
where: { address: 'Невский проспект, 88, Санкт-Петербург' }, where: { address: 'Влетная ул., 6А, Красноярск' },
update: {}, update: {},
create: { create: {
name: "Harat's Irish Pub Санкт-Петербург", name: "Harat's Irish Pub Санкт-Петербург",
address: 'Невский проспект, 88, Санкт-Петербург', address: 'Влетная ул., 6А, Красноярск',
phone: '+7 (812) 640-16-16', phone: '+7 (391) 271-77-54',
}, },
}); });
@@ -64,6 +64,16 @@ async function main() {
}, },
}); });
const pub5 = await prisma.pub.upsert({
where: { address: 'ул. Перенсона, 26, Красноярск' },
update: {},
create: {
name: "Harat's Irish Pub Красноярск",
address: 'ул. Перенсона, 26, Красноярск',
phone: '+7 (391) 214-07-99',
},
});
// ─── Tables for Pub 1 (main pub with full layout) ─── // ─── Tables for Pub 1 (main pub with full layout) ───
const tablesData = [ const tablesData = [
// Main hall // Main hall
@@ -95,7 +105,7 @@ async function main() {
} }
// Simple tables for pub2, pub3, and pub4 // Simple tables for pub2, pub3, and pub4
for (const pub of [pub2, pub3, pub4]) { for (const pub of [pub2, pub3, pub4, pub5]) {
for (let i = 1; i <= 8; i++) { for (let i = 1; i <= 8; i++) {
await prisma.table.upsert({ await prisma.table.upsert({
where: { pubId_number: { pubId: pub.id, number: i } }, where: { pubId_number: { pubId: pub.id, number: i } },
@@ -191,7 +201,7 @@ async function main() {
} }
// ─── Menu (link dishes to pubs, except lunch category) ── // ─── Menu (link dishes to pubs, except lunch category) ──
for (const pub of [pub1, pub2, pub3, pub4]) { for (const pub of [pub1, pub2, pub3, pub4, pub5]) {
for (const dish of createdDishes) { for (const dish of createdDishes) {
if (dish.categoryId === lunch.id) continue; // lunch is separate if (dish.categoryId === lunch.id) continue; // lunch is separate
await prisma.menu.upsert({ await prisma.menu.upsert({
@@ -211,7 +221,7 @@ async function main() {
sunday.setDate(monday.getDate() + 6); sunday.setDate(monday.getDate() + 6);
const lunchDishes = createdDishes.filter(d => d.categoryId === lunch.id); const lunchDishes = createdDishes.filter(d => d.categoryId === lunch.id);
for (const pub of [pub1, pub2, pub3, pub4]) { for (const pub of [pub1, pub2, pub3, pub4, pub5]) {
for (const dish of lunchDishes) { for (const dish of lunchDishes) {
await prisma.lunchMenu.upsert({ await prisma.lunchMenu.upsert({
where: { pubId_dishId_weekStart: { pubId: pub.id, dishId: dish.id, weekStart: monday } }, where: { pubId_dishId_weekStart: { pubId: pub.id, dishId: dish.id, weekStart: monday } },