fix(booking): use ophthalmologists for pediatric eye consultation
This commit is contained in:
parent
0bca60ed0b
commit
d791ea4d12
62
web/scripts/test-service-specialty.mjs
Normal file
62
web/scripts/test-service-specialty.mjs
Normal file
@ -0,0 +1,62 @@
|
||||
// Fictional doctors only; no external network or appointment submissions.
|
||||
import assert from 'node:assert/strict';
|
||||
import { parseHTML } from 'linkedom';
|
||||
import { createServer } from 'vite';
|
||||
const { window } = parseHTML('<html><body><div id="root"></div></body></html>');
|
||||
Object.assign(globalThis, { window, document: window.document, HTMLElement: window.HTMLElement, IS_REACT_ACT_ENVIRONMENT: true });
|
||||
window.location = new URL('https://test.invalid/services/detskaya-klinika/priem-detskogo-oftalmologa/');
|
||||
document.oninput = null;
|
||||
const { default: React, act } = await import('react');
|
||||
const { createRoot } = await import('react-dom/client');
|
||||
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 { default: ServicePage } = await vite.ssrLoadModule('/src/pages/ServicePage.jsx');
|
||||
const { LeadModalProvider } = await vite.ssrLoadModule('/src/context/LeadModalContext.jsx');
|
||||
const { serviceSpecialty } = await vite.ssrLoadModule('/src/lib/media.js');
|
||||
const { renderService, renderPage } = await vite.ssrLoadModule('/src/entry-server.jsx');
|
||||
const path = 'detskaya-klinika/priem-detskogo-oftalmologa';
|
||||
const service = { id: 17, path, slug: 'priem-detskogo-oftalmologa', name: 'Тестовый детский офтальмолог', doctors: [], blocks: [], seo: {} };
|
||||
const parent = { id: 1, path: 'detskaya-klinika', name: 'Детская клиника', children: [service], blocks: [], seo: {} };
|
||||
const doctors = [
|
||||
{ id: 1, slug: 'eye-test', full_name: 'Тестовый офтальмолог', last_name: 'Тестовый', first_name: 'Офтальмолог', position: 'Офтальмолог', specialties: [{ slug: 'oftalmologi', name: 'Офтальмологи', name_plural: 'Офтальмологи' }], booking: { provider: 'none' } },
|
||||
{ id: 2, slug: 'uzi-test', full_name: 'Посторонний врач УЗИ', last_name: 'Посторонний', first_name: 'Врач', position: 'Врач УЗИ', specialties: [{ slug: 'detskie-vrachi', name: 'Детские врачи' }], booking: { provider: 'none' } },
|
||||
];
|
||||
const requested = [];
|
||||
api.service = async (requestedPath) => ({ data: requestedPath === path ? service : parent });
|
||||
api.prices = async () => ({ data: [] });
|
||||
api.specialties = async () => ({ data: [{ slug: 'oftalmologi', name: 'Офтальмологи' }] });
|
||||
api.doctors = async (params = {}) => {
|
||||
requested.push(params.specialty);
|
||||
return { data: doctors.filter((doctor) => !params.specialty || doctor.specialties.some((s) => s.slug === params.specialty)) };
|
||||
};
|
||||
api.bookingAvailability = async () => ({ data: { enabled: true, days: [] } });
|
||||
api.createLead = api.createAppointment = () => { throw new Error('Submissions forbidden'); };
|
||||
const container = document.getElementById('root');
|
||||
const root = createRoot(container);
|
||||
const click = async (node) => act(async () => node.dispatchEvent(new window.Event('click', { bubbles: true })));
|
||||
try {
|
||||
assert.equal(serviceSpecialty(path), 'oftalmologi');
|
||||
assert.equal(serviceSpecialty('detskaya-klinika'), 'detskie-vrachi');
|
||||
assert.equal(serviceSpecialty('oko'), 'oftalmologi');
|
||||
assert.equal(serviceSpecialty('ginekologiya/test'), 'ginekologi');
|
||||
assert.equal(serviceSpecialty('unknown/test'), undefined);
|
||||
await act(async () => root.render(React.createElement(MemoryRouter, { initialEntries: [`/services/${path}/`] },
|
||||
React.createElement(LeadModalProvider, {}, React.createElement(Routes, {}, React.createElement(Route, { path: '/services/*', element: React.createElement(ServicePage) }))))));
|
||||
assert.match(container.querySelector('.service-doctors').textContent, /Тестовый офтальмолог/);
|
||||
assert.doesNotMatch(container.querySelector('.service-doctors').textContent, /Посторонний/);
|
||||
await click(container.querySelector('button.service-hero__btn'));
|
||||
await click(container.querySelector('.doctor-picker__trigger'));
|
||||
assert.deepEqual([...container.querySelectorAll('[data-doctor-id]')].map((node) => node.dataset.doctorId), ['1']);
|
||||
assert.ok(requested.length >= 2);
|
||||
assert.ok(requested.every((specialty) => specialty === 'oftalmologi'), 'Both service cards and booking must request ophthalmologists');
|
||||
const common = { doctors, specialties: [], prices: [], site: { branches: [], addresses: [], phone: '', hours: '' } };
|
||||
const server = renderService(service, parent, common, 'https://test.invalid');
|
||||
assert.deepEqual(JSON.parse(server.initial)['doctors:oftalmologi'].data.map((d) => d.id), [1]);
|
||||
const runtime = renderPage(`/services/${path}/`, { [`service:${path}`]: { data: service }, doctors: { data: doctors } }, 'https://test.invalid');
|
||||
assert.deepEqual(JSON.parse(runtime.initial)['doctors:oftalmologi'].data.map((d) => d.id), [1]);
|
||||
console.log('PASS: service doctor cards, booking picker, runtime SSR and prerender use the specific ophthalmology specialty');
|
||||
} finally {
|
||||
await act(async () => root.unmount());
|
||||
await vite.close();
|
||||
}
|
||||
@ -4,7 +4,7 @@ import App from './App.jsx';
|
||||
import { ApiDataProvider } from './context/ApiData.jsx';
|
||||
import { parseBlocks } from './lib/serviceContent.js';
|
||||
import { serviceMetadata, doctorMetadata, priceMetadata, pageMetadata, jsonForHTML } from './lib/seo.js';
|
||||
import { DIRECTION_MEDIA, directionOf } from './lib/media.js';
|
||||
import { serviceSpecialty } from './lib/media.js';
|
||||
export { parseBlocks };
|
||||
|
||||
export function siteFromCommon(common) {
|
||||
@ -27,7 +27,7 @@ export function renderPage(url, data, siteURL) {
|
||||
const service = initial[`service:${servicePath}`]?.data;
|
||||
if (service) {
|
||||
service.content_sections = parseBlocks(service.blocks);
|
||||
const specialty = DIRECTION_MEDIA[directionOf(service.path)]?.specialty;
|
||||
const specialty = serviceSpecialty(service.path);
|
||||
initial[`doctors:${specialty}`] = { data: (initial.doctors?.data || []).filter((d) => d.specialties?.some((s) => s.slug === specialty)), meta: {} };
|
||||
}
|
||||
const parts = pathname.split('/').filter(Boolean);
|
||||
@ -82,7 +82,7 @@ export function renderPage(url, data, siteURL) {
|
||||
|
||||
export function renderService(service, parent, common, siteURL) {
|
||||
service.content_sections = parseBlocks(service.blocks);
|
||||
const specialty = DIRECTION_MEDIA[directionOf(service.path)]?.specialty;
|
||||
const specialty = serviceSpecialty(service.path);
|
||||
const initial = {
|
||||
prerendered: true, siteURL, site: common.site,
|
||||
[`service:${service.path}`]: { data: service, meta: {} },
|
||||
|
||||
@ -91,6 +91,15 @@ export function directionOf(path) {
|
||||
return String(path || '').split('/')[0];
|
||||
}
|
||||
|
||||
// Узкий профиль услуги важнее широкого раздела «Детская клиника».
|
||||
// Только явные соответствия каталогу: не угадываем специальность по тексту.
|
||||
const SERVICE_SPECIALTIES = {
|
||||
'detskaya-klinika/priem-detskogo-oftalmologa': 'oftalmologi',
|
||||
};
|
||||
export function serviceSpecialty(path) {
|
||||
return SERVICE_SPECIALTIES[path] || DIRECTION_MEDIA[directionOf(path)]?.specialty;
|
||||
}
|
||||
|
||||
// Фото, загруженное через админку (/media/uploads/…), главнее картинки макета — как у чек-апов.
|
||||
// Старые картинки, перенесённые из Битрикса (/media/iblock/…), макет не перебивают.
|
||||
const isUploaded = (src) => typeof src === 'string' && src.includes('/media/uploads/');
|
||||
|
||||
@ -11,7 +11,7 @@ import { useSiteData } from '../context/SiteData.jsx';
|
||||
import { priceLabel, telHref } from '../lib/format.js';
|
||||
import { servicePath, specialtyPath, priceSectionPath } from '../lib/links.js';
|
||||
import { servicePriceSection } from '../lib/serviceNavigation.js';
|
||||
import { DIRECTION_MEDIA, SERVICE_IMAGES, directionOf } from '../lib/media.js';
|
||||
import { DIRECTION_MEDIA, SERVICE_IMAGES, directionOf, serviceSpecialty } from '../lib/media.js';
|
||||
import { URGENT_TITLE, parseBlocks } from '../lib/serviceContent.js';
|
||||
import { cleanText } from '../lib/text.js';
|
||||
import { useApi } from '../lib/useApi.js';
|
||||
@ -76,7 +76,7 @@ function DirectionView({ direction }) {
|
||||
className="btn btn--coral direction-card__btn"
|
||||
onClick={() => open({
|
||||
form: 'appointment', service: c, message: `Услуга: ${c.name}`,
|
||||
specialty: DIRECTION_MEDIA[directionOf(direction.path)]?.specialty,
|
||||
specialty: serviceSpecialty(c.path),
|
||||
})}
|
||||
>
|
||||
Записаться
|
||||
@ -106,7 +106,7 @@ function ServiceView({ service, parent }) {
|
||||
const media = DIRECTION_MEDIA[directionOf(service.path)];
|
||||
const image = SERVICE_IMAGES[service.path] || service.image || (!parent ? media?.image : '');
|
||||
const lead = cleanText(service.lead);
|
||||
const book = () => open({ form: 'appointment', service, specialty: media?.specialty, message: `Услуга: ${service.name}` });
|
||||
const book = () => open({ form: 'appointment', service, specialty: serviceSpecialty(service.path), message: `Услуга: ${service.name}` });
|
||||
|
||||
const crumbs = [
|
||||
{ to: '/', label: 'Главная' },
|
||||
@ -165,7 +165,7 @@ function ServiceView({ service, parent }) {
|
||||
|
||||
{(service.prices?.length > 0 || service.path.startsWith('estetic/') || priceSection) && <PricesSection prices={service.prices || []} service={service} priceSection={priceSection} />}
|
||||
|
||||
<ServiceDoctors service={service} specialtySlug={media?.specialty} />
|
||||
<ServiceDoctors service={service} specialtySlug={serviceSpecialty(service.path)} />
|
||||
<BookingSection service={service} />
|
||||
<script id="service-schema" type="application/ld+json" dangerouslySetInnerHTML={{ __html: jsonForHTML(serviceSchema(service, parent, site, siteURL,
|
||||
(service.doctors || []).filter((d) => !d.absent).sort((a, b) => Number(Boolean(b.photo)) - Number(Boolean(a.photo))).slice(0, 4))) }} />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user