Initial release of Moniki paid services site and CMS (v1.0.0).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alexey Novikov
2026-09-16 13:10:25 +03:00
co-authored by Cursor
commit 8abd8d06dd
263 changed files with 24973 additions and 0 deletions
+376
View File
@@ -0,0 +1,376 @@
import 'dotenv/config'
import { getPayload } from 'payload'
import { paidDoctors } from './paid-doctors'
import { seedPromotions } from './import-promotions'
import config from '../payload.config'
const translations = {
ru: {
branchMain: 'КДЦ МОНИКИ (основной)',
branchReutov: 'Филиал КДЦ МОНИКИ в г. Реутов',
addrMain:
'г. Москва, ул. Щепкина, 61/2. Станция метро «Проспект Мира», выход к спорткомплексу «Олимпийский».',
addrReutov: 'Московская область, г. Реутов, проспект Мира, д. 53',
howMain: 'От метро «Проспект Мира» пешком по улице Щепкина до корпуса института.',
howReutov:
'Московская область, г. Реутов, проспект Мира, д. 53. Удобно доехать наземным транспортом от метро «Новокосино» и от станции Реутов.',
},
en: {
branchMain: 'MONIKI CDC (main)',
branchReutov: 'MONIKI CDC branch in Reutov',
addrMain: 'Moscow, Shchepkina St., 61/2. Prospect Mira metro station.',
addrReutov: 'Moscow Region, Reutov, Prospekt Mira, 53',
howMain: 'From Prospect Mira metro walk along Shchepkina Street to the institute campus.',
howReutov:
'Moscow Region, Reutov, Prospekt Mira, 53. Convenient by surface transport from Novokosino metro and Reutov station.',
},
}
async function findOne(payload: Awaited<ReturnType<typeof getPayload>>, collection: 'branches' | 'specialties' | 'services' | 'doctors' | 'users' | 'posts', slugOrEmail: string) {
const field = collection === 'users' ? 'email' : 'slug'
const res = await payload.find({
collection,
where: { [field]: { equals: slugOrEmail } },
limit: 1,
})
return res.docs[0]
}
async function ensureBranchesSchema(payload: Awaited<ReturnType<typeof getPayload>>) {
const adapter = payload.db as {
drizzle: { execute: (query: unknown) => Promise<unknown> }
}
const { sql } = await import('@payloadcms/db-postgres')
await adapter.drizzle.execute(sql`
ALTER TABLE branches ADD COLUMN IF NOT EXISTS sort_order numeric DEFAULT 100
`)
await adapter.drizzle.execute(sql`
UPDATE branches
SET sort_order = 10, is_main = true, lat = 55.786637, lng = 37.627265
WHERE slug = 'kdc-moniki'
`)
await adapter.drizzle.execute(sql`
UPDATE branches
SET sort_order = 20, is_main = false, lat = 55.771049, lng = 37.862507
WHERE slug = 'kdc-reutov'
`)
await adapter.drizzle.execute(sql`
UPDATE branches_locales
SET
name = 'КДЦ МОНИКИ (основной)',
address = 'г. Москва, ул. Щепкина, 61/2. Станция метро «Проспект Мира», выход к спорткомплексу «Олимпийский».',
city = 'Москва',
metro = 'Проспект Мира',
how_to_get = 'От метро «Проспект Мира» пешком по улице Щепкина до корпуса института.'
WHERE _locale = 'ru'
AND _parent_id IN (SELECT id FROM branches WHERE slug = 'kdc-moniki')
`)
await adapter.drizzle.execute(sql`
UPDATE branches_locales
SET
name = 'Филиал КДЦ МОНИКИ в г. Реутов',
address = 'Московская область, г. Реутов, проспект Мира, д. 53',
city = 'Реутов',
how_to_get = 'Московская область, г. Реутов, проспект Мира, д. 53. Удобно доехать наземным транспортом от метро «Новокосино» и от станции Реутов.'
WHERE _locale = 'ru'
AND _parent_id IN (SELECT id FROM branches WHERE slug = 'kdc-reutov')
`)
await adapter.drizzle.execute(sql`
UPDATE branches_locales
SET
name = 'MONIKI CDC (main)',
address = 'Moscow, Shchepkina St., 61/2. Prospect Mira metro station.',
city = 'Moscow',
metro = 'Prospect Mira',
how_to_get = 'From Prospect Mira metro walk along Shchepkina Street to the institute campus.'
WHERE _locale = 'en'
AND _parent_id IN (SELECT id FROM branches WHERE slug = 'kdc-moniki')
`)
await adapter.drizzle.execute(sql`
UPDATE branches_locales
SET
name = 'MONIKI CDC branch in Reutov',
address = 'Moscow Region, Reutov, Prospekt Mira, 53',
city = 'Reutov',
how_to_get = 'Moscow Region, Reutov, Prospekt Mira, 53. Convenient by surface transport from Novokosino metro and Reutov station.'
WHERE _locale = 'en'
AND _parent_id IN (SELECT id FROM branches WHERE slug = 'kdc-reutov')
`)
}
async function seed() {
const payload = await getPayload({ config })
await ensureBranchesSchema(payload)
console.log('Seeding MONIKI demo data...')
const adminEmail = 'admin@moniki.local'
const existingAdmin = await payload.find({
collection: 'users',
where: { email: { equals: adminEmail } },
limit: 1,
})
if (!existingAdmin.docs.length) {
await payload.create({
collection: 'users',
data: {
email: adminEmail,
password: 'Admin123!',
fullName: 'Администратор МОНИКИ',
role: 'admin',
phone: '+7 (499) 674-07-09',
},
})
}
const patientEmail = 'patient@demo.local'
const existingPatient = await payload.find({
collection: 'users',
where: { email: { equals: patientEmail } },
limit: 1,
})
if (!existingPatient.docs.length) {
await payload.create({
collection: 'users',
data: {
email: patientEmail,
password: 'Patient123!',
fullName: 'Иванова Анна Сергеевна',
role: 'patient',
phone: '+7 (900) 123-45-67',
smsOptIn: true,
},
})
}
let branchMain = await findOne(payload, 'branches', 'kdc-moniki')
if (!branchMain) {
branchMain = await payload.create({
collection: 'branches',
locale: 'ru',
data: {
name: translations.ru.branchMain,
slug: 'kdc-moniki',
address: translations.ru.addrMain,
city: 'Москва',
phone: '+7 (499) 674-07-09',
email: 'mail@monikiweb.ru',
metro: 'Проспект Мира',
howToGet: translations.ru.howMain,
isMain: true,
sortOrder: 10,
lat: 55.786637,
lng: 37.627265,
workHours: [
{ day: 'Пн–Сб', hours: '08:00–20:00' },
{ day: 'Вс', hours: '09:00–16:00' },
],
},
})
}
let branchReutov = await findOne(payload, 'branches', 'kdc-reutov')
if (!branchReutov) {
branchReutov = await payload.create({
collection: 'branches',
locale: 'ru',
data: {
name: translations.ru.branchReutov,
slug: 'kdc-reutov',
address: translations.ru.addrReutov,
city: 'Реутов',
phone: '+7 (499) 674-07-09',
howToGet: translations.ru.howReutov,
isMain: false,
sortOrder: 20,
lat: 55.771049,
lng: 37.862507,
workHours: [{ day: 'Пн–Сб', hours: '08:00–20:00' }],
},
})
}
const specialtyDefs = [
{ title: 'Терапия', slug: 'therapy', icon: 'heart' },
{ title: 'Кардиология', slug: 'cardiology', icon: 'activity' },
{ title: 'Неврология', slug: 'neurology', icon: 'cpu-chip' },
{ title: 'Гастроэнтерология', slug: 'gastro', icon: 'beaker' },
{ title: 'Эндокринология', slug: 'endo', icon: 'flash' },
{ title: 'Диагностика', slug: 'diagnostics', icon: 'scan' },
]
const specialties: Record<string, string | number> = {}
for (const [i, s] of specialtyDefs.entries()) {
let doc = await findOne(payload, 'specialties', s.slug)
if (!doc) {
doc = await payload.create({
collection: 'specialties',
locale: 'ru',
data: {
title: s.title,
slug: s.slug,
description: `Платные услуги направления «${s.title}» в МОНИКИ`,
icon: s.icon,
sortOrder: i,
},
})
}
specialties[s.slug] = doc.id
await payload.update({
collection: 'specialties',
id: doc.id,
locale: 'en',
data: {
title: s.slug.charAt(0).toUpperCase() + s.slug.slice(1),
description: `Paid ${s.slug} services at MONIKI`,
},
})
}
const serviceDefs = [
{
title: 'Консультация терапевта',
slug: 'consult-therapy',
price: 3500,
durationMinutes: 30,
specialty: 'therapy',
category: 'consultation' as const,
isPopular: true,
},
{
title: 'Консультация кардиолога',
slug: 'consult-cardio',
price: 4500,
durationMinutes: 40,
specialty: 'cardiology',
category: 'consultation' as const,
isPopular: true,
},
{
title: 'Консультация невролога',
slug: 'consult-neuro',
price: 4200,
durationMinutes: 40,
specialty: 'neurology',
category: 'consultation' as const,
isPopular: false,
},
{
title: 'УЗИ органов брюшной полости',
slug: 'uzi-abdomen',
price: 3200,
durationMinutes: 30,
specialty: 'diagnostics',
category: 'diagnostics' as const,
isPopular: true,
},
{
title: 'ЭКГ с расшифровкой',
slug: 'ecg',
price: 1800,
durationMinutes: 20,
specialty: 'cardiology',
category: 'diagnostics' as const,
isPopular: true,
},
{
title: 'Комплексное обследование «Чек-ап»',
slug: 'checkup',
price: 18900,
durationMinutes: 120,
specialty: 'diagnostics',
category: 'package' as const,
isPopular: true,
},
]
const services: Record<string, string | number> = {}
for (const s of serviceDefs) {
let doc = await findOne(payload, 'services', s.slug)
if (!doc) {
doc = await payload.create({
collection: 'services',
locale: 'ru',
data: {
title: s.title,
slug: s.slug,
description: `${s.title} в платных отделениях МОНИКИ. Полная оплата через СБП.`,
price: s.price,
durationMinutes: s.durationMinutes,
specialty: specialties[s.specialty],
category: s.category,
branches: [branchMain.id, branchReutov.id],
isPopular: s.isPopular,
isPublished: true,
},
})
}
services[s.slug] = doc.id
}
const doctors: Record<string, string | number> = {}
for (const d of paidDoctors) {
let doc = await findOne(payload, 'doctors', d.slug)
if (!doc) {
doc = await payload.create({
collection: 'doctors',
locale: 'ru',
data: {
fullName: d.fullName,
slug: d.slug,
position: [d.position, d.specialty]
.filter((value, index, arr) => value && arr.indexOf(value) === index)
.join(' · '),
degree: d.degree || undefined,
experienceYears: d.experienceYears,
bio: d.bio,
photoUrl: d.photo,
photoPending: d.photoPending,
specialties: d.specialtySlugs.map((s) => specialties[s]).filter(Boolean),
branches: [branchMain.id],
education: d.education.map((item) => ({ item })),
defaultSlotMinutes: d.defaultSlotMinutes,
isPublished: true,
rating: 0,
reviewsCount: 0,
},
})
await payload.create({
collection: 'schedules',
data: {
title: `Расписание: ${d.fullName}`,
doctor: doc.id,
branch: branchMain.id,
slotMinutes: d.defaultSlotMinutes,
isActive: true,
weekdays: [
{ day: '1', startTime: '09:00', endTime: '14:00' },
{ day: '2', startTime: '09:00', endTime: '14:00' },
{ day: '3', startTime: '14:00', endTime: '19:00' },
{ day: '4', startTime: '09:00', endTime: '14:00' },
{ day: '5', startTime: '09:00', endTime: '13:00' },
],
},
})
}
doctors[d.slug] = doc.id
}
const existingReviews = await payload.find({ collection: 'reviews', limit: 1 })
if (!existingReviews.docs.length) {
console.log('Doctor reviews skipped')
}
await seedPromotions(payload)
console.log('Seed complete.')
console.log('Admin: admin@moniki.local / Admin123!')
console.log('Patient: patient@demo.local / Patient123!')
process.exit(0)
}
seed().catch((err) => {
console.error(err)
process.exit(1)
})