diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f1c57a..e380769 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.1.0/), версии — [SemVer](https://semver.org/lang/ru/). +## [1.0.10] — 2026-09-17 + +### Сайт + +- Личный кабинет больше не показывает чужие демо-записи: без входа разделы пустые, у каждого пациента только свои заявки, документы и счета +- После заявки на запись создаётся кабинет (если пациента ещё не было) и на почту уходит письмо со сводкой услуги, стоимостью и полезной информацией +- В галерее четыре фото КДЦ МОНИКИ как загруженные файлы, не ссылки + ## [1.0.9] — 2026-09-17 ### Сайт @@ -86,6 +94,7 @@ - Docker Compose для локальной БД - Секреты и логины в git не входят +[1.0.10]: https://git.myunion.pro/Alex/Moniki_uslugi/src/tag/v1.0.10 [1.0.9]: https://git.myunion.pro/Alex/Moniki_uslugi/src/tag/v1.0.9 [1.0.8]: https://git.myunion.pro/Alex/Moniki_uslugi/src/tag/v1.0.8 [1.0.7]: https://git.myunion.pro/Alex/Moniki_uslugi/src/tag/v1.0.7 diff --git a/README.md b/README.md index ca8d796..c52bf93 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Репозиторий: [Moniki_uslugi](https://git.myunion.pro/Alex/Moniki_uslugi) на [git.myunion.pro](https://git.myunion.pro/). -Текущая версия: **v1.0.9** (см. [CHANGELOG.md](CHANGELOG.md)). +Текущая версия: **v1.0.10** (см. [CHANGELOG.md](CHANGELOG.md)). ## Что внутри diff --git a/apps/cms/package.json b/apps/cms/package.json index a116470..5dce538 100644 --- a/apps/cms/package.json +++ b/apps/cms/package.json @@ -1,6 +1,6 @@ { "name": "@moniki/cms", - "version": "1.0.9", + "version": "1.0.10", "private": true, "type": "module", "scripts": { @@ -19,6 +19,7 @@ "seed:news": "tsx src/seed/import-news.ts", "seed:pages": "tsx src/seed/site-pages.ts", "seed:documents": "tsx src/seed/public-documents.ts", + "seed:gallery": "tsx src/seed/gallery-photos.ts", "seed:i18n": "python3 src/seed/translate_locales.py", "lint": "next lint" }, diff --git a/apps/cms/src/app/(payload)/api/custom/inquiries/route.ts b/apps/cms/src/app/(payload)/api/custom/inquiries/route.ts index b7c0c70..3992a36 100644 --- a/apps/cms/src/app/(payload)/api/custom/inquiries/route.ts +++ b/apps/cms/src/app/(payload)/api/custom/inquiries/route.ts @@ -2,6 +2,7 @@ import configPromise from '@payload-config' import { getPayload } from 'payload' import { NextResponse } from 'next/server' import { isValidEmailAddress, isValidFullName, isValidRuPhone, normalizeRuPhone } from '@/lib/contact-fields' +import { findOrCreatePatient, notifyInquiry } from '@/lib/inquiry-account' function asText(value: unknown) { return typeof value === "string" ? value.trim() : "" @@ -31,6 +32,18 @@ export async function POST(req: Request) { try { const payload = await getPayload({ config: configPromise }) + let patientId: string | number | undefined + let cabinetCreated = false + let password: string | undefined + try { + const account = await findOrCreatePatient(payload, { email, name, phone }) + patientId = account.id + cabinetCreated = account.created + password = account.password + } catch (error) { + payload.logger.error({ err: error }, 'Failed to create patient cabinet from inquiry') + } + const doc = await payload.create({ collection: "inquiries", overrideAccess: true, @@ -41,10 +54,23 @@ export async function POST(req: Request) { branch, ...(service != null ? { service } : {}), ...(doctor != null ? { doctor } : {}), + ...(patientId != null ? { patient: patientId } : {}), status: "new", }, }) - return NextResponse.json({ id: String(doc.id) }) + + notifyInquiry(payload, { + inquiryId: doc.id, + patientName: name, + email, + phone, + cabinetCreated, + password, + }).catch((error) => { + payload.logger.error({ err: error }, 'Failed to send inquiry email') + }) + + return NextResponse.json({ id: String(doc.id), cabinetCreated }) } catch (error) { const message = error instanceof Error ? error.message : "failed" return NextResponse.json({ error: message }, { status: 400 }) diff --git a/apps/cms/src/collections/Inquiries.ts b/apps/cms/src/collections/Inquiries.ts index d99c660..f3e5e0f 100644 --- a/apps/cms/src/collections/Inquiries.ts +++ b/apps/cms/src/collections/Inquiries.ts @@ -13,7 +13,11 @@ export const Inquiries: CollectionConfig = { }, access: { create: () => true, - read: ({ req: { user } }) => user?.role === 'admin', + read: ({ req: { user } }) => { + if (!user) return false + if (user.role === 'admin') return true + return { patient: { equals: user.id } } + }, update: ({ req: { user } }) => user?.role === 'admin', delete: ({ req: { user } }) => user?.role === 'admin', }, @@ -59,6 +63,7 @@ export const Inquiries: CollectionConfig = { { name: 'service', type: 'relationship', relationTo: 'services', label: 'Услуга' }, { name: 'doctor', type: 'relationship', relationTo: 'doctors', label: 'Врач' }, { name: 'branch', type: 'relationship', relationTo: 'branches', required: true, label: 'Филиал' }, + { name: 'patient', type: 'relationship', relationTo: 'users', label: 'Пациент' }, { name: 'status', type: 'select', diff --git a/apps/cms/src/lib/ensure-inquiries-schema.ts b/apps/cms/src/lib/ensure-inquiries-schema.ts index 5d9ac1d..9786744 100644 --- a/apps/cms/src/lib/ensure-inquiries-schema.ts +++ b/apps/cms/src/lib/ensure-inquiries-schema.ts @@ -40,6 +40,20 @@ export async function ensureInquiriesSchema(payload: Payload) { `, ) + await exec( + payload, + sql` + ALTER TABLE inquiries + ADD COLUMN IF NOT EXISTS patient_id integer + `, + ) + await exec( + payload, + sql` + CREATE INDEX IF NOT EXISTS inquiries_patient_idx ON inquiries (patient_id) + `, + ) + await exec( payload, sql` diff --git a/apps/cms/src/lib/inquiry-account.ts b/apps/cms/src/lib/inquiry-account.ts new file mode 100644 index 0000000..13ed958 --- /dev/null +++ b/apps/cms/src/lib/inquiry-account.ts @@ -0,0 +1,105 @@ +import { randomBytes } from 'crypto' +import type { Payload } from 'payload' +import { buildInquiryEmail } from './inquiry-email' +import { sendNotification } from './notifications' + +function siteUrl() { + return (process.env.CORS_ORIGINS || 'http://localhost:3000').split(',')[0]?.trim() || 'http://localhost:3000' +} + +function generateCabinetPassword() { + const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789' + const bytes = randomBytes(10) + let out = 'Mk-' + for (const byte of bytes) out += alphabet[byte % alphabet.length] + return out +} + +function relRecord(value: unknown): Record | null { + return value && typeof value === 'object' ? (value as Record) : null +} + +function relText(value: unknown, key: string) { + const rec = relRecord(value) + const raw = rec?.[key] + return typeof raw === 'string' ? raw.trim() : '' +} + +export async function findOrCreatePatient( + payload: Payload, + input: { email: string; name: string; phone: string }, +): Promise<{ id: string | number; created: boolean; password?: string }> { + const email = input.email.trim().toLowerCase() + const existing = await payload.find({ + collection: 'users', + where: { email: { equals: email } }, + limit: 1, + overrideAccess: true, + }) + if (existing.docs[0]) { + const user = existing.docs[0] + if (!user.phone && input.phone) { + await payload.update({ + collection: 'users', + id: user.id, + overrideAccess: true, + data: { phone: input.phone }, + }) + } + return { id: user.id, created: false } + } + + const password = generateCabinetPassword() + const created = await payload.create({ + collection: 'users', + overrideAccess: true, + data: { + email, + password, + fullName: input.name, + phone: input.phone, + role: 'patient', + emailOptIn: true, + }, + }) + return { id: created.id, created: true, password } +} + +export async function notifyInquiry( + payload: Payload, + input: { + inquiryId: string | number + patientName: string + email: string + phone: string + cabinetCreated: boolean + password?: string + }, +) { + const inquiry = await payload.findByID({ + collection: 'inquiries', + id: input.inquiryId, + depth: 2, + overrideAccess: true, + }) + const message = buildInquiryEmail({ + patientName: input.patientName, + email: input.email, + phone: input.phone, + serviceTitle: relText(inquiry.service, 'title').replace(/^\d+(?:\.\d+)*\.?\s+/, '').trim(), + servicePrice: Number(relRecord(inquiry.service)?.price) || undefined, + doctorName: relText(inquiry.doctor, 'fullName'), + branchName: relText(inquiry.branch, 'name'), + branchAddress: relText(inquiry.branch, 'address'), + cabinetCreated: input.cabinetCreated, + password: input.password, + loginUrl: `${siteUrl()}/ru/login`, + }) + await sendNotification(payload, { + channel: 'email', + to: input.email, + subject: message.subject, + body: message.text, + html: message.html, + }) +} diff --git a/apps/cms/src/lib/inquiry-email.ts b/apps/cms/src/lib/inquiry-email.ts new file mode 100644 index 0000000..1d28168 --- /dev/null +++ b/apps/cms/src/lib/inquiry-email.ts @@ -0,0 +1,149 @@ +function escapeHtml(text: string) { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') +} + +function row(label: string, value: string) { + if (!value.trim()) return '' + return ` + ${escapeHtml(label)} + ${escapeHtml(value)} + ` +} + +export type InquiryEmailInput = { + patientName: string + email: string + phone: string + serviceTitle?: string + servicePrice?: number + doctorName?: string + branchName?: string + branchAddress?: string + cabinetCreated: boolean + password?: string + loginUrl: string +} + +function formatPrice(amount?: number) { + if (amount == null || !Number.isFinite(amount) || amount <= 0) return '' + return new Intl.NumberFormat('ru-RU', { + style: 'currency', + currency: 'RUB', + maximumFractionDigits: 0, + }).format(amount) +} + +export function buildInquiryEmail(input: InquiryEmailInput) { + const firstName = input.patientName.trim().split(/\s+/)[1] || input.patientName.trim() + const price = formatPrice(input.servicePrice) + const subject = input.cabinetCreated + ? 'Личный кабинет и заявка на запись — МОНИКИ' + : 'Ваша заявка на запись принята — МОНИКИ' + + const cabinetBlock = input.cabinetCreated + ? ` +
+

Для вас создан личный кабинет

+

+ В кабинете будут ваши заявки, документы и счета. Войти можно по почте, которую вы указали в заявке. +

+ + ${row('Логин', input.email)} + ${input.password ? row('Пароль', input.password) : ''} +
+

+ + Войти в личный кабинет + +

+
` + : ` +

+ Заявка сохранена в вашем личном кабинете. Войти можно по адресу + ${escapeHtml(input.loginUrl)}. +

` + + const html = ` + + + + + + +
+ + + + + + + +
+

КДЦ МОНИКИ

+

Заявка принята

+
+

Здравствуйте, ${escapeHtml(firstName)}!

+

+ Спасибо, что выбрали платные медицинские услуги КДЦ МОНИКИ им. М. Ф. Владимирского. + Мы получили вашу заявку и свяжемся с вами, чтобы уточнить удобную дату и время. +

+
+

Сводка заявки

+ + ${row('Пациент', input.patientName)} + ${row('Услуга', input.serviceTitle || '')} + ${row('Стоимость', price)} + ${row('Врач', input.doctorName || '')} + ${row('Филиал', input.branchName || '')} + ${row('Адрес', input.branchAddress || '')} + ${row('Телефон для связи', input.phone)} + ${row('E-mail', input.email)} +
+
+ ${cabinetBlock} +
+

Полезная информация

+
    +
  • Возьмите паспорт. Если есть полис ОМС или результаты обследований — их тоже полезно иметь при себе.
  • +
  • Приходите за 10–15 минут до приёма, чтобы оформить документы на стойке регистрации.
  • +
  • Точное время визита подтвердит администратор: +7 (499) 674-07-09 или 122.
  • +
  • Основной филиал: ул. Щепкина, 61/2, метро «Проспект Мира».
  • +
  • Если планы изменятся, напишите нам или позвоните — заявку можно уточнить или перенести.
  • +
+
+

+ С заботой о вашем здоровье,
+ КДЦ МОНИКИ
+ Платные медицинские услуги +

+
+
+ +` + + const textLines = [ + `Здравствуйте, ${firstName}!`, + '', + 'Мы получили вашу заявку на запись в КДЦ МОНИКИ и свяжемся с вами, чтобы уточнить дату и время.', + '', + input.serviceTitle ? `Услуга: ${input.serviceTitle}` : '', + price ? `Стоимость: ${price}` : '', + input.doctorName ? `Врач: ${input.doctorName}` : '', + input.branchName ? `Филиал: ${input.branchName}` : '', + input.branchAddress ? `Адрес: ${input.branchAddress}` : '', + `Телефон: ${input.phone}`, + `E-mail: ${input.email}`, + '', + input.cabinetCreated + ? `Для вас создан личный кабинет.\nЛогин: ${input.email}\nПароль: ${input.password || ''}\nВход: ${input.loginUrl}` + : `Личный кабинет: ${input.loginUrl}`, + '', + 'Возьмите паспорт, приходите за 10–15 минут. Телефон: +7 (499) 674-07-09, 122.', + ].filter(Boolean) + + return { subject, html, text: textLines.join('\n') } +} diff --git a/apps/cms/src/lib/notifications.ts b/apps/cms/src/lib/notifications.ts index bc43cd3..a07c613 100644 --- a/apps/cms/src/lib/notifications.ts +++ b/apps/cms/src/lib/notifications.ts @@ -9,6 +9,7 @@ export type NotifyInput = { to: string subject: string body: string + html?: string } function escapeHtml(text: string) { @@ -36,7 +37,7 @@ export async function sendNotification( const result = await payload.sendEmail({ to: input.to, subject: input.subject, - html: `

${escapeHtml(input.body)}

`, + html: input.html || `

${escapeHtml(input.body)}

`, text: input.body, }) if (result && typeof result === 'object' && 'skipped' in result && result.skipped) { diff --git a/apps/cms/src/payload.config.ts b/apps/cms/src/payload.config.ts index a3a2272..13120da 100644 --- a/apps/cms/src/payload.config.ts +++ b/apps/cms/src/payload.config.ts @@ -96,6 +96,12 @@ export default buildConfig({ } catch (err) { payload.logger.error({ err }, 'Failed to seed public documents') } + try { + const { seedGalleryPhotos } = await import('./seed/gallery-photos') + await seedGalleryPhotos(payload) + } catch (err) { + payload.logger.error({ err }, 'Failed to seed gallery photos') + } }, editor: lexicalEditor(), i18n: { diff --git a/apps/cms/src/seed/files/gallery/photo-6.jpg b/apps/cms/src/seed/files/gallery/photo-6.jpg new file mode 100644 index 0000000..e3b4f16 Binary files /dev/null and b/apps/cms/src/seed/files/gallery/photo-6.jpg differ diff --git a/apps/cms/src/seed/files/gallery/photo-9.jpg b/apps/cms/src/seed/files/gallery/photo-9.jpg new file mode 100644 index 0000000..1ed9784 Binary files /dev/null and b/apps/cms/src/seed/files/gallery/photo-9.jpg differ diff --git a/apps/cms/src/seed/files/gallery/photo68.jpg b/apps/cms/src/seed/files/gallery/photo68.jpg new file mode 100644 index 0000000..039d27b Binary files /dev/null and b/apps/cms/src/seed/files/gallery/photo68.jpg differ diff --git a/apps/cms/src/seed/files/gallery/photo69.jpeg b/apps/cms/src/seed/files/gallery/photo69.jpeg new file mode 100644 index 0000000..911afb5 Binary files /dev/null and b/apps/cms/src/seed/files/gallery/photo69.jpeg differ diff --git a/apps/cms/src/seed/gallery-photos.ts b/apps/cms/src/seed/gallery-photos.ts new file mode 100644 index 0000000..f91db8f --- /dev/null +++ b/apps/cms/src/seed/gallery-photos.ts @@ -0,0 +1,110 @@ +import 'dotenv/config' +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from 'url' +import type { Payload } from 'payload' + +export const GALLERY_PHOTOS = [ + { + filename: 'photo-6.jpg', + mime: 'image/jpeg', + title: 'Ресепшен КДЦ МОНИКИ', + alt: 'Стойка регистрации КДЦ МОНИКИ', + sortOrder: 10, + }, + { + filename: 'photo69.jpeg', + mime: 'image/jpeg', + title: 'Администратор на ресепшене', + alt: 'Администратор КДЦ МОНИКИ за стойкой регистрации', + sortOrder: 20, + }, + { + filename: 'photo68.jpg', + mime: 'image/jpeg', + title: 'Зона ожидания', + alt: 'Зал ожидания отделения платных медицинских услуг КДЦ МОНИКИ', + sortOrder: 30, + }, + { + filename: 'photo-9.jpg', + mime: 'image/jpeg', + title: 'Стойка регистрации', + alt: 'Интерьер стойки регистрации КДЦ МОНИКИ', + sortOrder: 40, + }, +] + +function filePath(filename: string) { + const here = path.dirname(fileURLToPath(import.meta.url)) + const bundled = path.join(here, 'files', 'gallery', filename) + return fs.existsSync(bundled) ? bundled : null +} + +export async function seedGalleryPhotos(payload: Payload) { + for (const item of GALLERY_PHOTOS) { + const source = filePath(item.filename) + if (!source) { + payload.logger.warn(`Gallery photo not found: ${item.filename}`) + continue + } + + const already = await payload.find({ + collection: 'gallery', + where: { title: { equals: item.title } }, + locale: 'ru', + limit: 1, + }) + if (already.docs.length) continue + + const existingMedia = await payload.find({ + collection: 'media', + where: { filename: { equals: item.filename } }, + limit: 1, + }) + + let mediaId = existingMedia.docs[0]?.id + if (!mediaId) { + const buf = fs.readFileSync(source) + const media = await payload.create({ + collection: 'media', + locale: 'ru', + data: { alt: item.alt }, + file: { + data: buf, + mimetype: item.mime, + name: item.filename, + size: buf.length, + }, + }) + mediaId = media.id + } + + await payload.create({ + collection: 'gallery', + locale: 'ru', + data: { + title: item.title, + image: mediaId, + category: 'clinic', + sortOrder: item.sortOrder, + }, + }) + payload.logger.info(`Seeded gallery photo ${item.filename}`) + } +} + +async function main() { + const { getPayload } = await import('payload') + const config = (await import('../payload.config')).default + const payload = await getPayload({ config }) + await seedGalleryPhotos(payload) + process.exit(0) +} + +if (process.argv[1]?.includes('gallery-photos')) { + main().catch((err) => { + console.error(err) + process.exit(1) + }) +} diff --git a/apps/web/package.json b/apps/web/package.json index db516e2..7633fa6 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@moniki/web", - "version": "1.0.9", + "version": "1.0.10", "private": true, "scripts": { "dev": "next dev -p 3000", diff --git a/apps/web/src/app/[locale]/cabinet/documents/page.tsx b/apps/web/src/app/[locale]/cabinet/documents/page.tsx index 2b65c3a..b3a427b 100644 --- a/apps/web/src/app/[locale]/cabinet/documents/page.tsx +++ b/apps/web/src/app/[locale]/cabinet/documents/page.tsx @@ -1,3 +1,5 @@ +import { getMyDocuments } from "@/lib/api"; +import { formatDate } from "@/lib/utils"; import { getTranslations, setRequestLocale } from "next-intl/server"; export default async function CabinetDocumentsPage({ @@ -8,11 +10,27 @@ export default async function CabinetDocumentsPage({ const { locale } = await params; setRequestLocale(locale); const t = await getTranslations("cabinet"); + const documents = await getMyDocuments(); return (

{t("documents")}

-

{t("emptyDocuments")}

+ {documents.length === 0 ? ( +

{t("emptyDocuments")}

+ ) : ( +
    + {documents.map((doc) => ( +
  • + + {doc.title} + + {doc.issuedAt ? ( +

    {formatDate(doc.issuedAt, locale)}

    + ) : null} +
  • + ))} +
+ )}
); } diff --git a/apps/web/src/app/[locale]/cabinet/invoices/page.tsx b/apps/web/src/app/[locale]/cabinet/invoices/page.tsx index 76cf19b..67a863f 100644 --- a/apps/web/src/app/[locale]/cabinet/invoices/page.tsx +++ b/apps/web/src/app/[locale]/cabinet/invoices/page.tsx @@ -1,6 +1,6 @@ import { Card } from "@/components/ui/Card"; -import { getMyBookings } from "@/lib/api"; -import { formatPrice } from "@/lib/utils"; +import { getMyInvoices } from "@/lib/api"; +import { formatDate, formatPrice } from "@/lib/utils"; import { getTranslations, setRequestLocale } from "next-intl/server"; export default async function CabinetInvoicesPage({ @@ -11,24 +11,27 @@ export default async function CabinetInvoicesPage({ const { locale } = await params; setRequestLocale(locale); const t = await getTranslations("cabinet"); - const bookings = await getMyBookings(); + const invoices = await getMyInvoices(); return (

{t("invoices")}

- {bookings.length === 0 ? ( + {invoices.length === 0 ? (

{t("emptyInvoices")}

) : (
- {bookings.map((b) => ( - + {invoices.map((invoice) => ( +
-

{b.serviceTitle}

-

{b.datetime}

+

{invoice.title}

+

+ {invoice.number} + {invoice.datetime ? ` · ${formatDate(invoice.datetime, locale)}` : ""} +

-

{formatPrice(b.price, locale)}

-

{b.paid ? t("paid") : t("unpaid")}

+

{formatPrice(invoice.amount, locale)}

+

{invoice.paid ? t("paid") : t("unpaid")}

))} diff --git a/apps/web/src/app/[locale]/cabinet/layout.tsx b/apps/web/src/app/[locale]/cabinet/layout.tsx index c1d8398..4e4e3b4 100644 --- a/apps/web/src/app/[locale]/cabinet/layout.tsx +++ b/apps/web/src/app/[locale]/cabinet/layout.tsx @@ -1,5 +1,7 @@ import { CabinetNav } from "@/components/cabinet/CabinetNav"; import { namedPageMetadata } from "@/lib/page-metadata"; +import { me } from "@/lib/api"; +import { Link } from "@/i18n/routing"; import { getTranslations, setRequestLocale } from "next-intl/server"; import type { ReactNode } from "react"; @@ -22,6 +24,20 @@ export default async function CabinetLayout({ const { locale } = await params; setRequestLocale(locale); const t = await getTranslations("cabinet"); + const auth = await getTranslations("auth"); + const user = await me(); + + if (!user) { + return ( +
+

{t("title")}

+

{t("loginHint")}

+ + {auth("login")} + +
+ ); + } return (
diff --git a/apps/web/src/app/[locale]/gallery/page.tsx b/apps/web/src/app/[locale]/gallery/page.tsx index 7087c4d..e1bb8e2 100644 --- a/apps/web/src/app/[locale]/gallery/page.tsx +++ b/apps/web/src/app/[locale]/gallery/page.tsx @@ -35,7 +35,7 @@ export default async function GalleryPage({ {/* eslint-disable-next-line @next/next/no-img-element */} {item.title}
- {item.title} · {item.category} + {item.title}
))} diff --git a/apps/web/src/app/api/auth/login/route.ts b/apps/web/src/app/api/auth/login/route.ts index f079d40..dafcf9e 100644 --- a/apps/web/src/app/api/auth/login/route.ts +++ b/apps/web/src/app/api/auth/login/route.ts @@ -24,19 +24,6 @@ export async function POST(request: Request) { }); return response; } catch { - // Demo login fallback when CMS is down - if (body.email && body.password) { - const response = NextResponse.json({ - user: { id: "demo", email: body.email, name: "Пациент" }, - }); - response.cookies.set(AUTH_COOKIE, "demo-token", { - httpOnly: true, - sameSite: "lax", - path: "/", - maxAge: 60 * 60 * 24 * 7, - }); - return response; - } return NextResponse.json({ message: "Login failed" }, { status: 500 }); } } diff --git a/apps/web/src/app/api/auth/me/route.ts b/apps/web/src/app/api/auth/me/route.ts index 3da08b6..1dcd3f8 100644 --- a/apps/web/src/app/api/auth/me/route.ts +++ b/apps/web/src/app/api/auth/me/route.ts @@ -5,13 +5,7 @@ import { NextResponse } from "next/server"; export async function GET() { const store = await cookies(); const token = store.get(AUTH_COOKIE)?.value; - if (!token) return NextResponse.json({ user: null }, { status: 401 }); - - if (token === "demo-token") { - return NextResponse.json({ - user: { id: "demo", email: "patient@example.com", name: "Пациент", phone: "+7" }, - }); - } + if (!token || token === "demo-token") return NextResponse.json({ user: null }, { status: 401 }); try { const res = await fetch(`${CMS_URL}/api/users/me`, { @@ -21,21 +15,21 @@ export async function GET() { const data = await res.json(); return NextResponse.json(data, { status: res.status }); } catch { - return NextResponse.json({ - user: { id: "demo", email: "patient@example.com", name: "Пациент" }, - }); + return NextResponse.json({ user: null }, { status: 401 }); } } export async function PATCH(request: Request) { const store = await cookies(); const token = store.get(AUTH_COOKIE)?.value; - if (!token) return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); + if (!token || token === "demo-token") return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); const body = await request.json(); - if (token === "demo-token") { - return NextResponse.json({ user: { id: "demo", ...body } }); - } + const payload = { + ...body, + fullName: body.fullName || body.name, + }; + delete payload.name; try { const res = await fetch(`${CMS_URL}/api/users/me`, { @@ -44,11 +38,11 @@ export async function PATCH(request: Request) { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, - body: JSON.stringify(body), + body: JSON.stringify(payload), }); const data = await res.json(); return NextResponse.json(data, { status: res.status }); } catch { - return NextResponse.json({ user: { id: "demo", ...body } }); + return NextResponse.json({ message: "Update failed" }, { status: 500 }); } } diff --git a/apps/web/src/app/api/auth/register/route.ts b/apps/web/src/app/api/auth/register/route.ts index 6c552fa..13f6d36 100644 --- a/apps/web/src/app/api/auth/register/route.ts +++ b/apps/web/src/app/api/auth/register/route.ts @@ -4,24 +4,33 @@ import { NextResponse } from "next/server"; export async function POST(request: Request) { const body = await request.json(); try { + const payload = { + email: body.email, + password: body.password, + fullName: body.fullName || body.name, + phone: body.phone, + role: "patient", + }; const res = await fetch(`${CMS_URL}/api/users`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), + body: JSON.stringify(payload), }); const data = await res.json(); if (!res.ok) { return NextResponse.json(data, { status: res.status }); } - // Auto-login after register const loginRes = await fetch(`${CMS_URL}/api/users/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: body.email, password: body.password }), }); const loginData = await loginRes.json().catch(() => ({})); - const token = loginData.token || data.token || "demo-token"; + const token = loginData.token || data.token; + if (!token) { + return NextResponse.json({ message: "Register succeeded but login failed" }, { status: 500 }); + } const response = NextResponse.json({ user: data.doc || data.user || { email: body.email } }); response.cookies.set(AUTH_COOKIE, token, { @@ -33,15 +42,6 @@ export async function POST(request: Request) { }); return response; } catch { - const response = NextResponse.json({ - user: { id: "demo", email: body.email, name: body.name }, - }); - response.cookies.set(AUTH_COOKIE, "demo-token", { - httpOnly: true, - sameSite: "lax", - path: "/", - maxAge: 60 * 60 * 24 * 7, - }); - return response; + return NextResponse.json({ message: "Register failed" }, { status: 500 }); } } diff --git a/apps/web/src/components/cabinet/BookingActions.tsx b/apps/web/src/components/cabinet/BookingActions.tsx index 372f252..a077aba 100644 --- a/apps/web/src/components/cabinet/BookingActions.tsx +++ b/apps/web/src/components/cabinet/BookingActions.tsx @@ -95,15 +95,20 @@ export function BookingActions({ booking }: { booking: Booking }) {

{booking.doctorName}

{booking.branchName}

{formatDate(booking.datetime, locale)}

+ {booking.source === "inquiry" ? ( +

{t("requestPending")}

+ ) : null}

{statusLabel}

-

{formatPrice(booking.price, locale)}

-

{paid ? t("paid") : t("unpaid")}

+ {booking.price ?

{formatPrice(booking.price, locale)}

: null} + {booking.source === "inquiry" ? null : ( +

{paid ? t("paid") : t("unpaid")}

+ )}
- {status === "upcoming" ? ( + {status === "upcoming" && booking.source !== "inquiry" ? (
{!paid ? (