From e3e438df6828574b210480c7fc72979e8646784c Mon Sep 17 00:00:00 2001 From: sova-bootstrap Date: Thu, 28 May 2026 12:29:31 +0300 Subject: [PATCH] feat: migrate to VitePress from monorepo docs, add test-contour section --- .DS_Store | Bin 0 -> 10244 bytes .gitea/workflows/build.yml | 12 +- .gitignore | 3 + .vitepress/config.mts | 113 + .vitepress/theme/index.ts | 44 + .vitepress/theme/style.css | 15 + Dockerfile | 11 +- api-routes.md | 209 + apps/admin-panel-content-crud.md | 416 ++ apps/admin-panel.md | 183 + apps/backend-architecture.md | 173 + apps/backend-content-crud.md | 673 +++ apps/backend-ddd.md | 263 ++ apps/backend-scenarios/anonymous-reserve.md | 108 + apps/backend-scenarios/auth-uid-pcode.md | 99 + apps/backend-scenarios/calltouch-lead.md | 67 + apps/backend-scenarios/change-region.md | 74 + apps/backend-scenarios/index.md | 40 + apps/backend-scenarios/kiosk-checkpass.md | 75 + apps/backend-scenarios/login-jwt.md | 85 + apps/backend-scenarios/schedule-cache.md | 113 + apps/backend-scenarios/schedule-messenger.md | 90 + apps/backend-scenarios/sms-record.md | 61 + .../specialist-card-locations.md | 86 + .../backend-scenarios/sync-doctors-reviews.md | 115 + apps/backend-scenarios/xml-yandex-feed.md | 68 + apps/backend.md | 136 + apps/cabinet-architecture.md | 163 + apps/cabinet.md | 93 + apps/doctor-schedule-sync.md | 451 ++ apps/online-consultation.md | 216 + architecture.md | 154 + data-model.md | 474 +++ docs-site.md | 90 + environment.md | 74 + flows.md | 146 + forms-screenshots.zip | Bin 0 -> 1883723 bytes forms-screenshots/01-login-landing.png | Bin 0 -> 49684 bytes forms-screenshots/02-login-modal.png | Bin 0 -> 66706 bytes forms-screenshots/03-password-recovery.png | Bin 0 -> 56115 bytes forms-screenshots/04-registration.png | Bin 0 -> 104225 bytes forms-screenshots/05-specialists-search.png | Bin 0 -> 114290 bytes forms-screenshots/06-price-search.png | Bin 0 -> 73068 bytes forms-screenshots/07-help-callback.png | Bin 0 -> 96472 bytes forms-screenshots/08-info-reference-entry.png | Bin 0 -> 251833 bytes forms-screenshots/09-widget-form-2.png | Bin 0 -> 25791 bytes forms-screenshots/10-widget-reference.png | Bin 0 -> 63690 bytes .../11-doctor-your-home-modal.png | Bin 0 -> 162929 bytes .../12-booking-modal-offline.png | Bin 0 -> 190393 bytes forms-screenshots/13-online-specialists.png | Bin 0 -> 108884 bytes forms-screenshots/14-case-history.png | Bin 0 -> 124586 bytes forms-screenshots/15-payment.png | Bin 0 -> 110901 bytes forms-screenshots/16-settings.png | Bin 0 -> 99895 bytes forms-screenshots/17-security-card.png | Bin 0 -> 113749 bytes forms-screenshots/18-refund-form.png | Bin 0 -> 57397 bytes forms-screenshots/19-booking-modal-online.png | Bin 0 -> 111056 bytes forms-screenshots/README.md | 25 + forms-screenshots/manifest.json | 120 + index.md | 34 + infrastructure/.DS_Store | Bin 0 -> 6148 bytes infrastructure/backend-external-services.md | 527 +++ infrastructure/docker.md | 73 + infrastructure/k8s-cicd-platform-plan.md | 1804 ++++++++ .../test-contour}/argocd-apps.md | 0 infrastructure/test-contour/index.md | 13 + .../test-contour}/tags.md | 0 .../test-contour}/test-contour-article.md | 2 +- nginx.conf | 6 +- operations/maintenance.md | 88 + package-lock.json | 3748 +++++++++++++++++ package.json | 14 + quick-start.md | 149 + site/README.md | 22 - site/_sidebar.md | 4 - site/index.html | 25 - testing.md | 211 + 76 files changed, 11998 insertions(+), 60 deletions(-) create mode 100644 .DS_Store create mode 100644 .gitignore create mode 100644 .vitepress/config.mts create mode 100644 .vitepress/theme/index.ts create mode 100644 .vitepress/theme/style.css create mode 100644 api-routes.md create mode 100644 apps/admin-panel-content-crud.md create mode 100644 apps/admin-panel.md create mode 100644 apps/backend-architecture.md create mode 100644 apps/backend-content-crud.md create mode 100644 apps/backend-ddd.md create mode 100644 apps/backend-scenarios/anonymous-reserve.md create mode 100644 apps/backend-scenarios/auth-uid-pcode.md create mode 100644 apps/backend-scenarios/calltouch-lead.md create mode 100644 apps/backend-scenarios/change-region.md create mode 100644 apps/backend-scenarios/index.md create mode 100644 apps/backend-scenarios/kiosk-checkpass.md create mode 100644 apps/backend-scenarios/login-jwt.md create mode 100644 apps/backend-scenarios/schedule-cache.md create mode 100644 apps/backend-scenarios/schedule-messenger.md create mode 100644 apps/backend-scenarios/sms-record.md create mode 100644 apps/backend-scenarios/specialist-card-locations.md create mode 100644 apps/backend-scenarios/sync-doctors-reviews.md create mode 100644 apps/backend-scenarios/xml-yandex-feed.md create mode 100644 apps/backend.md create mode 100644 apps/cabinet-architecture.md create mode 100644 apps/cabinet.md create mode 100644 apps/doctor-schedule-sync.md create mode 100644 apps/online-consultation.md create mode 100644 architecture.md create mode 100644 data-model.md create mode 100644 docs-site.md create mode 100644 environment.md create mode 100644 flows.md create mode 100644 forms-screenshots.zip create mode 100644 forms-screenshots/01-login-landing.png create mode 100644 forms-screenshots/02-login-modal.png create mode 100644 forms-screenshots/03-password-recovery.png create mode 100644 forms-screenshots/04-registration.png create mode 100644 forms-screenshots/05-specialists-search.png create mode 100644 forms-screenshots/06-price-search.png create mode 100644 forms-screenshots/07-help-callback.png create mode 100644 forms-screenshots/08-info-reference-entry.png create mode 100644 forms-screenshots/09-widget-form-2.png create mode 100644 forms-screenshots/10-widget-reference.png create mode 100644 forms-screenshots/11-doctor-your-home-modal.png create mode 100644 forms-screenshots/12-booking-modal-offline.png create mode 100644 forms-screenshots/13-online-specialists.png create mode 100644 forms-screenshots/14-case-history.png create mode 100644 forms-screenshots/15-payment.png create mode 100644 forms-screenshots/16-settings.png create mode 100644 forms-screenshots/17-security-card.png create mode 100644 forms-screenshots/18-refund-form.png create mode 100644 forms-screenshots/19-booking-modal-online.png create mode 100644 forms-screenshots/README.md create mode 100644 forms-screenshots/manifest.json create mode 100644 index.md create mode 100644 infrastructure/.DS_Store create mode 100644 infrastructure/backend-external-services.md create mode 100644 infrastructure/docker.md create mode 100644 infrastructure/k8s-cicd-platform-plan.md rename {site/docs => infrastructure/test-contour}/argocd-apps.md (100%) create mode 100644 infrastructure/test-contour/index.md rename {site/docs => infrastructure/test-contour}/tags.md (100%) rename {site/docs => infrastructure/test-contour}/test-contour-article.md (99%) create mode 100644 operations/maintenance.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 quick-start.md delete mode 100644 site/README.md delete mode 100644 site/_sidebar.md delete mode 100644 site/index.html create mode 100644 testing.md diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..174c38f3e44d969587355b88e09c87319aed2d78 GIT binary patch literal 10244 zcmeGhZEPG@ac1q*yIy~+9Xm;LPIB_KB57>bb&@7+^Km}MG|mThZ72Rr+`Zeiz4hMi zb$9O^Cj{JINDUy$56XuM5*3K@qm`h60)IdVDM-MNU?Ql5id2aoP>IhXAtcJY_x9|& z^Tk0_tqj~d?arI`X5QPKotZZ?2LPDJ8V>k!H0lgrBx(Qk-%n_fJ6_+?6;b(2C%>>vfnJE$d0pQ$)=UcWs5(I-c}1q2;B6zm2w^NA5+Hv^f0N3zP(@ayNdkE{Qe6EQp)^Ne8lHw_cphGa zSKtTmGk6o;hIg&g8Pq(PSpepmX+amH!5FaJlrS)MisGkR6$w-%@P94=<{zSTF&GcwvVh!m(2e;U zHqSx`-l$I=jE_Y_7!TpHfSi?hI4eQHieZX@f}O^rjGSORgv$a7c0j=n819T=hJt)| z;)`-}z_@^wdqn~j39OfZLPHzI{z^-yAk= zJ+N}-Nj)%q7EZH}do4B|OSyJ7?a)k9O1D#4A~t(%n+4LGx;S7jNDoS4`SD5L^#V!) zXt9D&yi7>1C@(i`-*eyI&Yr_Z*atimJA58_QX~m9)(X-A|d0LFvf(Mau~0 zCoRKt0&CVXJw<84j!{QXn=6#jG)LMZyYpOvc8yooV)A%4X+52_f~A#at*J2@X;-HW zdckw9!B#V5?S{f*jX z6gkf`5k)fu+Y$BA$XHG!Fp73Y3?5TkBjYJIou*gu6*1Nu>5E)q+n&|^U@&7k?2&GY zxewCZq(85F=HQ}Zdf^kDrcwS8X`C_j8&=@5NiL1{$dD{gumyV!!!%Z!@7=j$cqBp` zn&srIfE0mpBm!AqyCuu=YVH$@KOi)*Amt$$$Ku^$cwK5lhr=%Dh6J33OBB5>!FBiy zd`3XcF8GimZp*0k_2x z9cIcHX;XLa$(!byQ3@kHPX=UOmYFSUH6eBEKA8+i+Y+LC_YrzVyGNjHon0~+t0r+& zZR$KykoF%}cdHsGO1R9)ep=?H_MU0}kKq>lk~IG< z()@p770S4sw0$pW`w=|OHNFq~@k2O>LzKmi;zhiKGnC1uNbA$+U;B{49PBzkn~0_J0ju!mr~u@m0Km-wy5ICu9Y$_AzuU49J2J3EFqIG2+HO1*j~b0 z%PR-0xXwy-b7gs`a!|CN_!t}9n`y3=mdb^Ni43dv(SzMh?DZtT4Doaly-TmeiW+{o zM*?_cSH{!c=*~iMQ)8f_}K8wf}M8UXN80@Oirj^Q{1i-y87P#Ni~&1kMh;j^&wV4 z(DdJ0Q?1nhicR6YsQ>w7uw4{MrNJ&XL{>vA@*#C$4bkTwOVlsRR4uueEv!8@&<+lF zEX+kXv=hV2`a15_3q7<5#ytsi=DWq>AdY4>Ln&_EdO>sBI_VcOc>e8+y4voEK z(V5VC_Z3K9SIFgMO5{Z~UTiHm8)WXES&jB`Ifuv{UE#BD82=UT#Vf=AHt(izoCmwR z-=*W+P(FsAvXkWO>3dhsfBw%wk?j^ALnYW2fk#>WzyHqv|Cex8(24{q68K+|09KzI zIhmk0%7?F|FUQWxC|ys|#Tuhq7Lc>hjgjz19af6xf7Q}>Skd7;aYjI6%K~y%!UgO5 W`9}c#M^HKc->1Z>a{h15{QqA<7~-%1 literal 0 HcmV?d00001 diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 2380d81..088f270 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -17,11 +17,15 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Validate site files + - uses: actions/setup-node@v4 + with: + node-version: '24' + - name: Build VitePress site run: | - test -f site/index.html - test -f site/README.md - test -f site/docs/test-contour-article.md + npm ci + npm run build + test -f .vitepress/dist/index.html + test -f infrastructure/test-contour/test-contour-article.md parse-tag: if: startsWith(github.ref, 'refs/tags/docs-v') diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c76bc00 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.vitepress/cache/ +.vitepress/dist/ diff --git a/.vitepress/config.mts b/.vitepress/config.mts new file mode 100644 index 0000000..4180bec --- /dev/null +++ b/.vitepress/config.mts @@ -0,0 +1,113 @@ +import { defineConfig } from 'vitepress' + +const escapeHtml = (value: string) => + value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + +export default defineConfig({ + lang: 'ru-RU', + title: 'Sova: документация проекта', + description: 'Онбординг, запуск и эксплуатация инфраструктуры Sova', + cleanUrls: true, + ignoreDeadLinks: true, + markdown: { + config(md) { + const defaultFence = md.renderer.rules.fence + + md.renderer.rules.fence = (tokens, idx, options, env, self) => { + const token = tokens[idx] + const info = token.info.trim().split(/\s+/)[0] + + if (info === 'mermaid') { + return `
${escapeHtml(token.content)}
` + } + + return defaultFence + ? defaultFence(tokens, idx, options, env, self) + : self.renderToken(tokens, idx, options) + } + } + }, + themeConfig: { + nav: [ + { text: 'Старт', link: '/quick-start' }, + { text: 'Архитектура', link: '/architecture' }, + { text: 'Test-контур k3s', link: '/infrastructure/test-contour/' }, + { text: 'Инфраструктура', link: '/infrastructure/docker' } + ], + sidebar: [ + { + text: 'Онбординг', + items: [ + { text: 'Обзор', link: '/' }, + { text: 'Быстрый старт', link: '/quick-start' }, + { text: 'Проверка локального контура', link: '/testing' }, + { text: 'Окружение', link: '/environment' }, + { text: 'Архитектура', link: '/architecture' }, + { text: 'Потоки данных', link: '/flows' }, + { text: 'API и Swagger', link: '/api-routes' }, + { text: 'Модели данных', link: '/data-model' }, + { text: 'Документация VitePress', link: '/docs-site' } + ] + }, + { + text: 'Приложения', + items: [ + { text: 'Backend API', link: '/apps/backend' }, + { text: 'Backend: DDD / бизнес-сущности', link: '/apps/backend-ddd' }, + { + text: 'Backend: бизнес-сценарии (use cases)', + collapsed: true, + items: [ + { text: 'Оглавление', link: '/apps/backend-scenarios/' }, + { text: '1.1 Логин и JWT', link: '/apps/backend-scenarios/login-jwt' }, + { text: '1.2 UID / pcode', link: '/apps/backend-scenarios/auth-uid-pcode' }, + { text: '1.3 Смена региона', link: '/apps/backend-scenarios/change-region' }, + { text: '2.1 Карточка врача и локации', link: '/apps/backend-scenarios/specialist-card-locations' }, + { text: '2.2 Расписание и кеш', link: '/apps/backend-scenarios/schedule-cache' }, + { text: '2.3 GetScheduleMessage', link: '/apps/backend-scenarios/schedule-messenger' }, + { text: '2.4 Расписание: полный мануал (Backend + Cabinet)', link: '/apps/doctor-schedule-sync' }, + { text: '3.1 Анонимная запись', link: '/apps/backend-scenarios/anonymous-reserve' }, + { text: '3.2 SMS и Record', link: '/apps/backend-scenarios/sms-record' }, + { text: '3.3 Киоск checkpass', link: '/apps/backend-scenarios/kiosk-checkpass' }, + { text: '4.1 Синхронизация врачей/отзывов', link: '/apps/backend-scenarios/sync-doctors-reviews' }, + { text: '4.2 Calltouch', link: '/apps/backend-scenarios/calltouch-lead' }, + { text: '4.3 XML-фид Яндекса', link: '/apps/backend-scenarios/xml-yandex-feed' } + ] + }, + { text: 'Backend: CRUD контента', link: '/apps/backend-content-crud' }, + { text: 'adminPanel: обзор', link: '/apps/admin-panel' }, + { text: 'adminPanel: CRUD контента', link: '/apps/admin-panel-content-crud' }, + { text: 'Backend: архитектура модулей', link: '/apps/backend-architecture' }, + { text: 'Cabinet', link: '/apps/cabinet' }, + { text: 'Cabinet: онлайн-консультация', link: '/apps/online-consultation' }, + { text: 'Cabinet: архитектура модулей', link: '/apps/cabinet-architecture' } + ] + }, + { + text: 'Инфраструктура', + items: [ + { text: 'Docker Compose', link: '/infrastructure/docker' }, + { text: 'K8s + Terraform + ArgoCD + Gitea', link: '/infrastructure/k8s-cicd-platform-plan' }, + { text: 'Backend: внешние сервисы (test/stage/prod)', link: '/infrastructure/backend-external-services' }, + { + text: 'Test-контур k3s (песочница)', + collapsed: false, + items: [ + { text: 'Обзор', link: '/infrastructure/test-contour/' }, + { text: 'Что сделано + перенос на сервер', link: '/infrastructure/test-contour/test-contour-article' }, + { text: 'Система тегов CI/CD', link: '/infrastructure/test-contour/tags' }, + { text: 'ArgoCD: sova-root и data-test', link: '/infrastructure/test-contour/argocd-apps' } + ] + }, + { text: 'Эксплуатация', link: '/operations/maintenance' } + ] + } + ], + socialLinks: [] + } +}) diff --git a/.vitepress/theme/index.ts b/.vitepress/theme/index.ts new file mode 100644 index 0000000..503c921 --- /dev/null +++ b/.vitepress/theme/index.ts @@ -0,0 +1,44 @@ +import DefaultTheme from 'vitepress/theme' +import mermaid from 'mermaid' +import { nextTick, watch } from 'vue' +import { useRoute } from 'vitepress' +import './style.css' + +const renderMermaid = async () => { + await nextTick() + + const diagrams = Array.from( + document.querySelectorAll('.mermaid') + ) + + diagrams.forEach((diagram) => { + diagram.removeAttribute('data-processed') + }) + + await mermaid.run({ nodes: diagrams }) +} + +export default { + extends: DefaultTheme, + setup() { + if (typeof window === 'undefined') { + return + } + + const route = useRoute() + + mermaid.initialize({ + startOnLoad: false, + securityLevel: 'loose', + theme: 'default' + }) + + watch( + () => route.path, + () => { + renderMermaid() + }, + { immediate: true } + ) + } +} diff --git a/.vitepress/theme/style.css b/.vitepress/theme/style.css new file mode 100644 index 0000000..be53921 --- /dev/null +++ b/.vitepress/theme/style.css @@ -0,0 +1,15 @@ +.mermaid { + margin: 24px 0; + padding: 16px; + overflow-x: auto; + border: 1px solid var(--vp-c-divider); + border-radius: 12px; + background: var(--vp-c-bg-soft); + text-align: center; + white-space: pre; +} + +.mermaid svg { + max-width: 100%; + height: auto; +} diff --git a/Dockerfile b/Dockerfile index 7b96fa3..53abf67 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,13 @@ +# syntax=docker/dockerfile:1 + +FROM node:24-alpine AS builder +WORKDIR /docs +COPY package.json package-lock.json ./ +RUN npm ci +COPY . . +RUN npm run build + FROM nginx:1.27-alpine COPY nginx.conf /etc/nginx/conf.d/default.conf -COPY site/ /usr/share/nginx/html/ +COPY --from=builder /docs/.vitepress/dist /usr/share/nginx/html EXPOSE 8080 diff --git a/api-routes.md b/api-routes.md new file mode 100644 index 0000000..53d5e1d --- /dev/null +++ b/api-routes.md @@ -0,0 +1,209 @@ +# API, HTTP-методы и Swagger + +## Swagger / OpenAPI + +Backend API: + +- UI: `http://localhost:8081/docs` +- JSON: `http://localhost:8081/api/doc.json` +- Конфиг: `apps/backend/config/routes/nelmio_api_doc.yaml` +- В документацию backend сейчас попадают только path patterns из `apps/backend/config/packages/nelmio_api_doc.yaml`. + +Cabinet: + +- UI: `http://localhost:8082/api/swagger` +- JSON: `http://localhost:8082/api/swagger.json` +- Реализовано вручную в `apps/cabinet/src/Controller/InternalAPIController.php`. + +## Backend API + +### Пользователь + +| Метод | Путь | Контроллер | Назначение | +| --- | --- | --- | --- | +| `GET` | `/user/logout` | `UserController::logout` | Logout-заглушка для клиента | +| `POST` | `/user/login` | `UserController::login` | Логин по email/password, выдача JWT | +| `GET` | `/user/` | `UserController::index` | Текущий пользователь по JWT | +| `PUT` | `/user/change-region` | `UserController::changeRegion` | Смена региона пользователя | +| `POST` | `/user/auth` | `UserController::auth` | Регистрация/авторизация | +| `POST` | `/user/auth-by-pcode` | `UserController::authByPcode` | Авторизация по pcode и дате рождения | + +### Справочники и контент + +| Метод | Путь | Контроллер | Назначение | +| --- | --- | --- | --- | +| `GET` | `/filial/list` | `FilialController` | Список филиалов | +| `GET` | `/filial/by-region/{regionId}` | `FilialController` | Филиалы по региону | +| `PUT` | `/filial/{fid}` | `FilialController` | Обновление филиала | +| `POST` | `/filial/create` | `FilialController` | Создание филиала | +| `GET` | `/filial/picture/{id}` | `FilialController` | Картинка филиала | +| `POST` | `/filial/picture/{id}` | `FilialController` | Загрузка картинки филиала | +| `GET` | `/department/list` | `DepartmentController` | Список отделений | +| `PUT` | `/department/{did}` | `DepartmentController` | Обновление отделения | +| `POST` | `/department/create` | `DepartmentController` | Создание отделения | +| `GET` | `/pricelist/list` | `PriceListController` | Прайс | +| `GET` | `/pricelist/department` | `PriceDepartmentController` | Группы прайса | + +### Врачи и расписание + +| Метод | Путь | Контроллер | Назначение | +| --- | --- | --- | --- | +| `POST` | `/specialist/create` | `SpecialistController` | Создание врача | +| `PUT` | `/specialist/{id}` | `SpecialistController` | Обновление врача | +| `DELETE` | `/specialist/{id}` | `SpecialistController` | Удаление врача | +| `GET` | `/specialist/list` | `SpecialistController` | Список врачей | +| `GET` | `/specialist/post` | `SpecialistController` | Список должностей | +| `GET` | `/specialist/picture/{id}` | `SpecialistController` | Фото врача | +| `POST` | `/specialist/picture/{id}` | `SpecialistController` | Загрузка фото врача | +| `GET` | `/specialist/schedule` | `SpecialistController` | Расписание врача | +| `GET` | `/specialist/{id}` | `SpecialistController` | Детальная карточка по id | +| `GET` | `/specialist/by/{identifier}` | `SpecialistController` | Детальная карточка по alias/id | +| `POST` | `/specialist/{id}/location/create` | `LocationController` | Создание локации врача | +| `PUT` | `/specialist/{specialistId}/location/{id}` | `LocationController` | Обновление локации врача | +| `GET` | `/specialist-dcode-description/list` | `SpecialistDcodeDescriptionController` | Описания dcode | +| `GET` | `/specialist-dcode-description/{id}` | `SpecialistDcodeDescriptionController` | Описание dcode | +| `POST` | `/specialist/{specialistId}/specialist-dcode-description/create` | `SpecialistDcodeDescriptionController` | Создание описания dcode | +| `PUT` | `/specialist/{specialistId}/specialist-dcode-description/{id}` | `SpecialistDcodeDescriptionController` | Обновление описания dcode | +| `DELETE` | `/specialist-dcode-description/{id}` | `SpecialistDcodeDescriptionController` | Удаление описания dcode | +| `GET` | `/specialist-docs/list` | `SpecialistDocsController` | Документы врачей | +| `GET` | `/specialist-docs/{id}` | `SpecialistDocsController` | Документ врача | +| `GET` | `/specialist-docs/picture/{id}` | `SpecialistDocsController` | Картинка документа | +| `POST` | `/specialist/{id}/specialist-docs/create` | `SpecialistDocsController` | Создание документа | +| `PUT` | `/specialist/{specialistId}/specialist-docs/{id}` | `SpecialistDocsController` | Обновление документа | +| `DELETE` | `/specialist-docs/{id}` | `SpecialistDocsController` | Удаление документа | +| `POST` | `/specialist-docs/picture/{id}` | `SpecialistDocsController` | Загрузка картинки документа | +| `DELETE/PUT` | `/stock/{id}/specialist/{specialistId}` | `StockSpecialistController` | Связь акции и врача | + +### CRUD-контент + +Эти контроллеры имеют одинаковый набор методов: + +| Контроллер | Базовый путь | Методы | +| --- | --- | --- | +| `ArticleController` | `/article` | `GET /list`, `GET /alias/{alias}`, `GET /{id}`, `POST /create`, `PUT /{id}`, `DELETE /{id}` | +| `DiseaseController` | `/disease` | `GET /list`, `GET /{id}`, `POST /create`, `PUT /{id}`, `DELETE /{id}` | +| `MedicalCenterController` | `/medical-center` | `GET /list`, `GET /{id}`, `POST /create`, `PUT /{id}`, `DELETE /{id}` | +| `NewsController` | `/news` | `GET /list`, `GET /{id}`, `POST /create`, `PUT /{id}`, `DELETE /{id}` | +| `PromoController` | `/promo` | `GET /list`, `GET /{id}`, `POST /create`, `PUT /{id}`, `DELETE /{id}` | +| `ReviewController` | `/review` | `GET /list`, `GET /{id}`, `POST /create`, `PUT /{id}`, `DELETE /{id}` | +| `SiteServiceController` | `/site-services` | `GET /list`, `GET /{id}`, `POST /create`, `PUT /{id}`, `DELETE /{id}` | + +Для контентных сущностей `POST`, `PUT` и `DELETE` доступны только с JWT пользователя с ролью `ROLE_ADMIN`. + +Списки `news`, `promo`, `medical-center`, `disease`, `site-services` возвращают единый формат пагинации: + +```json +{ + "data": [], + "pagination": { + "total": 1, + "count": 1, + "per_page": 50, + "current_page": 1, + "total_pages": 1, + "has_previous_page": false, + "has_next_page": false + } +} +``` + +Основные query-параметры списков: + +- `page` - номер страницы; +- `perPage` - размер страницы; +- `regionId` - фильтр по региону; +- `active` - фильтр активности. + +`/article/list` сохраняет старый контракт фронтенда: размер страницы задаётся параметром `limit`, а метаданные возвращаются в ключе `meta`: + +```json +{ + "data": [], + "meta": { + "total": 1, + "page": 1, + "limit": 20, + "totalPages": 1 + } +} +``` + +Пример создания новости: + +```bash +curl -X POST http://localhost:8081/news/create \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + --data '{ + "id": 900001, + "name": "Локальная новость", + "active": true, + "regionId": 91, + "alias": "local-admin-news", + "anons": "Краткий анонс", + "content": "Полный текст новости" + }' +``` + +### Интеграции и служебные методы + +| Метод | Путь | Контроллер | Назначение | +| --- | --- | --- | --- | +| `GET` | `/infoclinica/clvisitsovacheckpass/{filial}` | `InfoclinicaController` | Проверка прохода | +| `GET` | `/idoctor/list` | `InfoclinicaController` | Список врачей Infoclinica | +| `POST` | `/reservation/anonymous-reserve` | `InfoclinicaController` | Анонимная запись | +| `POST` | `/calltouch/create-lead` | `CalltouchController` | Создание лида | +| `GET` | `/service/sendmail` | `ServiceController` | Отправка письма | +| `POST` | `/smart-captcha` | `ServiceController` | Проверка captcha | +| `GET` | `/xml/feed` | `XmlFeedController` | XML-фид | +| `GET` | `/xml/feed/v1` | `XmlFeedController` | XML-фид v1 | + +## Cabinet API и страницы + +### Swagger и внутренний API + +| Метод | Путь | Контроллер | Назначение | +| --- | --- | --- | --- | +| `ANY` | `/api/swagger.json` | `InternalAPIController::swaggerJson` | OpenAPI JSON | +| `ANY` | `/api/swagger` | `InternalAPIController::swaggerUI` | Swagger UI | +| `POST` | `/api/smart-captcha` | `InternalAPIController::smartCaptcha` | Проверка captcha | +| `GET` | `/api/banner/{regionId}` | `InternalAPIController::show` | Баннер региона | +| `POST` | `/api/log` | `InternalAPIController::log` | Логирование | +| `POST` | `/api/count-record` | `InternalAPIController::countRecord` | Количество записей | +| `POST` | `/api/add-record` | `InternalAPIController` | Создание записи | +| `POST` | `/api/msg` | `InternalAPIController` | Сообщение/SMS | +| `POST` | `/api/veretify` | `InternalAPIController` | Проверка SMS-кода | +| `POST` | `/api/search` | `InternalAPIController` | Поиск | +| `GET` | `/api/departments` | `InternalAPIController` | Отделения | +| `POST` | `/api/add-calltouch` | `CalltouchAPIController` | Заявка Calltouch | + +### Публичный API cabinet + +| Метод | Путь | Контроллер | Назначение | +| --- | --- | --- | --- | +| `POST` | `/api/anonymous-reserve` | `PublicAPIController` | Анонимная запись | +| `GET` | `/api/interval` | `PublicAPIController` | Интервалы расписания | +| `GET` | `/api/userInfo` | `PublicAPIController` | Информация о пользователе | +| `GET` | `/api/pricelist/departments` | `PublicAPIController` | Группы прайса | +| `GET` | `/api/pricelist` | `PublicAPIController` | Прайс | +| `GET` | `/api/doctor` | `PublicAPIController` | Врач | +| `GET` | `/api/doctors/{region}` | `PublicAPIController` | Врачи региона | + +### Страницы cabinet + +| Метод | Путь | Контроллер | Назначение | +| --- | --- | --- | --- | +| `ANY` | `/login` | `SecurityController` | Страница входа | +| `ANY` | `/logout` | `SecurityController` | Выход | +| `POST` | `/api/usrlog/logout` | `SecurityController` | API logout | +| `GET/POST` | `/registration` | `SecurityController` | Регистрация | +| `POST` | `/forget` | `SecurityController` | Восстановление | +| `POST` | `/api/authenticated` | `SecurityController` | Авторизация API | +| `GET` | `/` | `DefaultController` | Главная кабинета, требует авторизации | +| `ANY` | `/stoimost-uslug` | `DefaultController` | Стоимость услуг | +| `POST` | `/update/price-list` | `DefaultController` | Обновление прайса | +| `ANY` | `/price-list` | `DefaultController` | Админский прайс | +| `GET` | `/specialists/{alias?}` | `SpecialistController` | Каталог врачей | +| `GET` | `/online-specialists` | `SpecialistController` | Онлайн-врачи | +| `GET` | `/specialist/{alias}` | `SpecialistController` | Карточка врача | +| `ANY` | `/favorites` | `SpecialistController` | Избранное | diff --git a/apps/admin-panel-content-crud.md b/apps/admin-panel-content-crud.md new file mode 100644 index 0000000..88fe7ef --- /dev/null +++ b/apps/admin-panel-content-crud.md @@ -0,0 +1,416 @@ +# adminPanel: CRUD контентных сущностей + +Подробное описание UI для [backend CRUD контента](/apps/backend-content-crud). Общие соглашения админки (layout, RTK, переиспользуемые компоненты): [adminPanel: обзор](/apps/admin-panel). + +## Задача (#27) + +Дать контент-менеджерам экраны в `apps/adminPanel` для шести сущностей backend API: + +| Сущность | Раздел меню | Backend API | +| --- | --- | --- | +| Новости | Новости | `/news` | +| Промо (контент) | Промо (контент) | `/promo` | +| Заболевания | Заболевания | `/disease` | +| Медцентры | Медцентры | `/medical-center` | +| Статьи | Статьи | `/article` | +| Услуги сайта | Услуги сайта | `/site-services` | + +**Не путать** с разделом **«Акции»** (`/promotions`) — это сущность `stock` (отдельные `apiStock.js`, `StoksListPage`, `EditStockPage`). + +Требования к UI: + +- единый CRUD-подход для всех шести ресурсов (один код списка и формы, конфиг полей); +- список с поиском и серверной пагинацией; +- создание / редактирование / удаление; +- ошибки валидации **на форме** (красная подсветка полей), **без** `window.alert` при сохранении; +- успешное сохранение — `Modal`, как у акций. + +## Ветки Git + +| Ветка | Подход | Назначение | +| --- | --- | --- | +| **`issues/27-future`** | Generic: `ContentListPage`, `ContentEditPage`, `apiContent` | Рекомендуемая реализация с виджетами полей и `parseSaveError` | +| `issues/27` | Копия `/promotions`: отдельные `*ListPage` / `Edit*Page` / `api*` на ресурс | Интеграционная / альтернативная ветка | +| `issues/27-refactor` | То же, что `issues/27` (от `dev`) | Исторический MR «как акции» | +| `dev` | Без контентного CRUD | База | + +Backend: [ветки и MR](/apps/backend-content-crud#ветки-git-и-mr-файлы) — `issues/27` на backend **не трогать**. + +Дальше в документе описан код ветки **`issues/27-future`**. + +## Ресурсы: UI-маршруты и API + +У каждого ресурса **три маршрута** в React Router и один `basePath` в RTK Query. + +| Ключ конфига | `slug` (UI) | Список | Создание | Редактирование | `basePath` (API) | +| --- | --- | --- | --- | --- | --- | +| `news` | `/news` | `/news` | `/news/create` | `/news/edit/:id` | `/news` | +| `promo` | `/site-promo` | `/site-promo` | `/site-promo/create` | `/site-promo/edit/:id` | `/promo` | +| `disease` | `/disease` | `/disease` | `/disease/create` | `/disease/edit/:id` | `/disease` | +| `medical-center` | `/medical-center` | `/medical-center` | `/medical-center/create` | `/medical-center/edit/:id` | `/medical-center` | +| `article` | `/article` | `/article` | `/article/create` | `/article/edit/:id` | `/article` | +| `site-services` | `/site-services` | `/site-services` | `/site-services/create` | `/site-services/edit/:id` | `/site-services` | + +`slug` — сегмент в URL админки. `basePath` — префикс HTTP к backend (может отличаться, напр. промо: UI `site-promo`, API `promo`). + +### Соответствие HTTP backend + +| Действие в UI | RTK hook | HTTP | +| --- | --- | --- | +| Список | `useListQuery({ search, page, perPage })` | `GET {basePath}/list?...` | +| Карточка | `useItemQuery(id)` | `GET {basePath}/{id}` | +| Создание | `useCreateMutation(data)` | `POST {basePath}/create` | +| Обновление | `useUpdateMutation({ id, data })` | `PUT {basePath}/{id}` | +| Удаление | `useDeleteMutation(id)` | `DELETE {basePath}/{id}` | + +Write-запросы отправляют `Authorization: Bearer` через `authHeader()` в `apiContent.js`. + +Пагинация списка: + +- по умолчанию: `?page=1&perPage=20&search=...`; +- **статьи**: `listUsesLimit: true` → `limit` вместо `perPage`; +- ответ: `{ data, pagination }` или для статей `{ data, meta }` — UI нормализует в `normalizePagination()`. + +## Общая схема + +```mermaid +flowchart TD + subgraph routes [React Router] + List["/{slug}"] + Create["/{slug}/create"] + Edit["/{slug}/edit/:id"] + end + + subgraph pages [pages/content] + Index["index.jsx → NewsListPage, …"] + CLP[ContentListPage] + CEP[ContentEditPage] + end + + subgraph config [Конфигурация] + CR[contentResources.js] + Hooks[contentHooks в apiContent.js] + end + + List --> Index --> CLP + Create --> Index --> CEP + Edit --> Index --> CEP + + CLP --> Hooks + CEP --> Hooks + CLP --> CR + CEP --> CR + + Hooks --> Slice[apiSlice + baseUrl] + Slice --> API[Backend Symfony CRUD] + + CEP --> Parse[parseSaveError] + CEP --> Widgets[ContentField / FieldHint] + CEP --> Shell[EditElementForm + Modal] +``` + +## Ключевой принцип + +Страница **не знает**, сколько полей у «новости» или «услуги». Она получает `config` и `hooks` и рисует форму по `config.fields` и таблицу по `config.listColumns`. + +Добавление седьмого ресурса = запись в `CONTENT_RESOURCES` + автоматически те же `ContentListPage` / `ContentEditPage` (через `pages/content/index.jsx`). + +Контраст с `/promotions`: там отдельный файл страницы и `apiStock.js` на домен. Здесь — **один** список и **одна** форма на все ресурсы. + +## Файлы проекта + +| Файл | Назначение | +| --- | --- | +| `src/config/contentResources.js` | Описание 6 ресурсов: `slug`, `basePath`, колонки списка, поля формы | +| `src/api/apiContent.js` | RTK Query: `injectEndpoints` × 6, экспорт `contentHooks` | +| `src/pages/content/ContentListPage.jsx` | Универсальная страница списка | +| `src/pages/content/ContentEditPage.jsx` | Универсальная форма create/edit + виджеты полей | +| `src/pages/content/index.jsx` | Связывает ресурс → `NewsListPage`, `NewsEditPage`, … | +| `src/utils/parseSaveError.js` | Разбор ошибок API → `fieldErrors` / `globalMessage` | +| `src/styles/theme-override.scss` | Классы `.content-field--has-error`, `.content-field-error-msg` | +| `src/App.jsx` | 18 маршрутов (6 × list/create/edit) | +| `src/components/Sidebar/Sidebar.jsx` | Пункты меню | +| `src/components/Navbar/Navbar.jsx` | Мобильное меню | +| `src/store/store.js` | `import '../api/apiContent'` | +| `src/config/api.js` | `VITE_API_BASE_URL` (локальная разработка) | + +## Конфигурация ресурса (`contentResources.js`) + +Каждый ресурс — объект в `CONTENT_RESOURCES`: + +| Поле | Тип | Назначение | +| --- | --- | --- | +| `slug` | string | URL в админке (`news`, `site-promo`, …) | +| `basePath` | string | Префикс API (`/news`, `/promo`, …) | +| `title` | string | Заголовок списка | +| `titleSingle` | string | Подпись в форме («новость», «услугу») | +| `icon` | string | Font Awesome для меню (не используется в `ContentListPage`, только в Sidebar) | +| `listColumns` | array | Колонки таблицы списка | +| `fields` | array | Поля формы create/edit | +| `listUsesLimit` | boolean? | Только `article`: `limit` в query | +| `listUsesMeta` | boolean? | Только `article`: ответ с `meta` | + +### Колонки списка (`listColumns`) + +```javascript +{ key: 'id', label: 'ID' } +{ key: 'name', label: 'Название' } +{ key: 'active', label: 'Активно', format: 'bool' } // → «Да» / «Нет» +{ key: 'regionId', label: 'Регион' } // → имя из regionSlice +``` + +### Поля формы (`fields`) + +Общий набор для большинства ресурсов (`baseContentFields`): + +| `key` | `type` | Виджет | +| --- | --- | --- | +| `name` | `text` | input text | +| `active` | `checkbox` | checkbox | +| `regionId` | `region` | select регионов | +| `alias` | `text` | input text | +| `anons` | `html` | `TextEditor` | +| `content` | `html` | `TextEditor` | + +Дополнительные поля задаются хелперами `text(key, label)` и `json(key, label)` — см. таблицу ресурсов ниже. + +## Виджеты полей (форма) + +Виджеты объявлены **внутри** `ContentEditPage.jsx` (не отдельные файлы в `components/`). Это осознанный «мини-движок формы» для контента. + +### Вспомогательные компоненты + +| Виджет | Назначение | +| --- | --- | +| `FieldHint` | Текст ошибки под полем: `` | +| `fieldWrapperClass(hasError, extra)` | Собирает классы: `form-group`, `content-field--has-error`, `form-check` | +| `ContentField` | Рендер одного поля по `field.type` | + +Атрибут `data-field-key={field.key}` на обёртке — для скролла к первому полю с ошибкой (`querySelector`). + +### Типы полей (`field.type`) + +| type | UI | Значение в state | Отправка в API | +| --- | --- | --- | --- | +| `text` | `` | string | string или `null` если пусто | +| `number` | `` | string в форме | `Number` или `null` | +| `checkbox` | `` | boolean | boolean | +| `region` | `