From f9f45e5eaec59979cc028b3222f397f9aa559b0c Mon Sep 17 00:00:00 2001
From: Dana Medical Developer
Date: Thu, 22 Oct 2026 14:34:30 +0000
Subject: [PATCH] fix(checkup): request programs without selecting a doctor
---
web/scripts/test-checkup-request.mjs | 90 ++++++++++++++++++++++++++++
web/src/components/LeadModal.jsx | 19 +++++-
web/src/pages/CheckupPage.jsx | 2 +-
web/src/pages/PromosPage.jsx | 7 ++-
4 files changed, 113 insertions(+), 5 deletions(-)
create mode 100644 web/scripts/test-checkup-request.mjs
diff --git a/web/scripts/test-checkup-request.mjs b/web/scripts/test-checkup-request.mjs
new file mode 100644
index 0000000..10193fb
--- /dev/null
+++ b/web/scripts/test-checkup-request.mjs
@@ -0,0 +1,90 @@
+// Fictional contact data and mocked submissions only. No external requests.
+import assert from 'node:assert/strict';
+import { parseHTML } from 'linkedom';
+import { createServer } from 'vite';
+const { window } = parseHTML('');
+Object.assign(globalThis, { window, document: window.document, HTMLElement: window.HTMLElement, IS_REACT_ACT_ENVIRONMENT: true });
+window.location = new URL('https://test.invalid/sales/');
+document.oninput = null;
+const { default: React, act } = await import('react');
+const { createRoot } = await import('react-dom/client');
+const { Simulate } = await import('react-dom/test-utils');
+const { MemoryRouter, Routes, Route } = await import('react-router-dom');
+const vite = await createServer({ server: { middlewareMode: true, hmr: false }, appType: 'custom' });
+const { api } = await vite.ssrLoadModule('/src/api.js');
+const { LeadModalProvider } = await vite.ssrLoadModule('/src/context/LeadModalContext.jsx');
+const { default: PromosPage } = await vite.ssrLoadModule('/src/pages/PromosPage.jsx');
+const { default: CheckupPage } = await vite.ssrLoadModule('/src/pages/CheckupPage.jsx');
+const program = { id: 1, slug: 'chek-ap-ya-v-sad', name: 'Я в сад', audience: 'kids', blocks: [], seo: {} };
+api.promos = async () => ({ data: [
+ { id: 1, slug: 'ya-v-sad-sale', title: 'Я в сад' },
+ { id: 2, slug: 'unrelated-promo', title: 'Другая акция', checkup_slug: program.slug },
+] });
+api.checkups = async () => ({ data: [program] });
+api.checkup = async () => ({ data: program });
+let bookingCalls = 0;
+api.doctors = api.bookingAvailability = api.doctorSlots = async () => {
+ bookingCalls++;
+ throw new Error('Checkup request must not load doctors or schedules');
+};
+const submissions = [];
+api.createLead = async (payload) => { submissions.push(payload); return { data: { message: 'Тестовая заявка принята' } }; };
+api.createAppointment = () => { throw new Error('Checkup must not create an appointment in 1C'); };
+const container = document.getElementById('root');
+const root = createRoot(container);
+const click = async (node) => {
+ assert.ok(node, 'Expected actionable element');
+ await act(async () => node.dispatchEvent(new window.Event('click', { bubbles: true })));
+};
+const mount = async (url, Page, route) => act(async () => root.render(
+ React.createElement(MemoryRouter, { key: url, initialEntries: [url] },
+ React.createElement(LeadModalProvider, {}, React.createElement(Routes, {},
+ React.createElement(Route, { path: route, element: React.createElement(Page) }))))));
+function checkSimpleForm() {
+ const modal = container.querySelector('[role="dialog"]');
+ assert.ok(modal);
+ assert.match(modal.textContent, /Записаться на чек-ап/);
+ assert.match(modal.textContent, /Программа: Я в сад/);
+ assert.equal(modal.querySelectorAll('input').length, 3, 'Only name, phone and consent');
+ assert.equal(modal.querySelector('select, textarea, input[type="date"], .doctor-picker, .native-booking'), null);
+ assert.equal(bookingCalls, 0);
+ return modal;
+}
+try {
+ await mount('/sales/', PromosPage, '/sales/');
+ for (const card of container.querySelectorAll('.promo-card')) {
+ await click(card.querySelector('button'));
+ checkSimpleForm();
+ await click(container.querySelector('[aria-label="Закрыть"]'));
+ }
+ await click(container.querySelector('.promo-card button'));
+ let modal = checkSimpleForm();
+ await act(async () => Simulate.submit(modal.querySelector('form')));
+ assert.equal(submissions.length, 0, 'Empty form rejected');
+ await act(async () => Simulate.change(modal.querySelector('input[type="text"]'), { target: { value: 'Тестовая заявка' } }));
+ await act(async () => Simulate.change(modal.querySelector('input[type="tel"]'), { target: { value: '79990000000' } }));
+ await act(async () => Simulate.submit(modal.querySelector('form')));
+ assert.equal(submissions.length, 0, 'Consent required');
+ await act(async () => Simulate.change(modal.querySelector('input[type="checkbox"]'), { target: { type: 'checkbox', checked: true } }));
+ await act(async () => Simulate.submit(modal.querySelector('form')));
+ assert.equal(submissions.length, 1);
+ assert.equal(submissions[0].form, 'appointment', 'Use existing lead endpoint form');
+ assert.deepEqual(submissions[0].extra, { checkup_slug: program.slug, checkup_name: program.name });
+ assert.match(submissions[0].message, /Чек-ап: Я в сад/);
+ assert.match(submissions[0].message, /Акция: Я в сад/);
+ assert.equal(submissions[0].doctor_id, undefined);
+ assert.equal(submissions[0].slot, undefined);
+ assert.equal(submissions[0].consent, true);
+ assert.match(container.querySelector('[role="dialog"]').textContent, /Тестовая заявка принята/);
+ await mount(`/checkup/${program.slug}/`, CheckupPage, '/checkup/:slug/');
+ await click(container.querySelector('.checkup-hero__btn'));
+ checkSimpleForm();
+ await click(container.querySelector('[aria-label="Закрыть"]'));
+ const bottomCta = [...container.querySelectorAll('button')].find((node) => node.textContent === 'Оставить заявку');
+ await click(bottomCta);
+ checkSimpleForm();
+ console.log('PASS: promotion links (slug fallback and explicit), checkup hero/bottom CTAs, contact validation and program metadata; no doctors, schedules or 1C appointment calls');
+} finally {
+ await act(async () => root.unmount());
+ await vite.close();
+}
diff --git a/web/src/components/LeadModal.jsx b/web/src/components/LeadModal.jsx
index bf86cfc..420c0c7 100644
--- a/web/src/components/LeadModal.jsx
+++ b/web/src/components/LeadModal.jsx
@@ -14,6 +14,10 @@ import { todayKey } from '../lib/slots.js';
import './LeadModal.css';
const COPY = {
+ checkup: {
+ title: 'Записаться на чек-ап',
+ subtitle: 'Оставьте контакты — администратор согласует дату и расскажет о подготовке',
+ },
appointment: {
title: 'Записаться на приём',
subtitle: 'Выберите дату — покажем врачей со свободными окнами на этот день',
@@ -32,7 +36,7 @@ const COPY = {
},
};
-export default function LeadModal({ form = 'appointment', doctor, slot, service, specialty, message = '', onClose }) {
+export default function LeadModal({ form = 'appointment', doctor, slot, service, specialty, program, message = '', onClose }) {
const copy = COPY[form] || COPY.appointment;
const { hours } = useSiteData();
const uid = useId();
@@ -202,11 +206,17 @@ export default function LeadModal({ form = 'appointment', doctor, slot, service,
const extra = {};
if (form === 'review') extra.rating = values.rating;
+ if (form === 'checkup' && program) {
+ extra.checkup_slug = program.slug;
+ extra.checkup_name = program.name;
+ }
setStatus({ state: 'sending', message: '' });
try {
const { data } = await api.createLead({
- form,
+ // Заявка на программу использует существующую форму записи,
+ // а не создание приёма конкретного врача в 1С.
+ form: form === 'checkup' ? 'appointment' : form,
name: values.name.trim(),
phone: values.phone,
doctor_id: values.doctorId ? Number(values.doctorId) : undefined,
@@ -298,6 +308,9 @@ export default function LeadModal({ form = 'appointment', doctor, slot, service,
Свободные окна этого врача сейчас недоступны для онлайн-записи. Оставьте контакты — администратор уточнит расписание.
)}
+ {form === 'checkup' && program?.name && (
+ Программа: {program.name}
+ )}
)}
- {form !== 'callback' && (
+ {form !== 'callback' && form !== 'checkup' && (