7 Commits

Author SHA1 Message Date
sova-ci e4641ad83a chore(#27): sync issues/27-future from monorepo (6300722 issues/27: url on component) 2026-06-09 15:42:13 +03:00
Valery Petrov e696e6779a issues/27: sync branch from k3s-test 2026-06-03 18:38:00 +03:00
sova-ci c7d4b1f0c5 chore(#27): sync issues/27-future from monorepo (6300722 issues/27: url on component) 2026-06-03 18:38:00 +03:00
Valery Petrov 75be0504ab issues/27: url on component 2026-06-03 18:38:00 +03:00
Valery Petrov 40fcfc303e issues/27: validate only fields present in resource config 2026-06-03 18:38:00 +03:00
Valery Petrov 67388d9628 issues/27: generic content CRUD with field error widgets 2026-06-03 18:38:00 +03:00
Valery Petrov 90779fdc08 issues/27: content CRUD examples by /promotions 2026-06-03 18:38:00 +03:00
45 changed files with 1209 additions and 2919 deletions
+28 -11
View File
@@ -1,8 +1,17 @@
name: adminpanel-ci-cd
# CI/CD: только push git-тега (ручное тегирование на ветке prod|test|stage).
on:
workflow_dispatch:
inputs:
branch:
description: 'Ветка для прогона тестов'
required: true
default: test
type: choice
options:
- prod
- test
- stage
push:
tags:
- 'adminpanel-v*'
@@ -14,10 +23,12 @@ env:
jobs:
test:
if: false # test contour: setup-node downloads hang on slow egress; build runs without blocking
if: github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/adminpanel-v')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.branch || github.ref }}
- uses: actions/setup-node@v4
with:
node-version: '24'
@@ -27,6 +38,7 @@ jobs:
- run: npm run build
parse-tag:
if: startsWith(github.ref, 'refs/tags/adminpanel-v')
runs-on: ubuntu-latest
outputs:
full_tag: ${{ steps.meta.outputs.full_tag }}
@@ -42,7 +54,8 @@ jobs:
echo "version=$(echo "$TAG" | sed -E 's/adminpanel-v([0-9.]+).*/\1/')" >> "$GITHUB_OUTPUT"
build-and-push:
needs: [parse-tag]
needs: [test, parse-tag]
if: startsWith(github.ref, 'refs/tags/adminpanel-v')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -61,6 +74,7 @@ jobs:
deploy-gitops:
needs: [build-and-push, parse-tag]
if: startsWith(github.ref, 'refs/tags/adminpanel-v')
runs-on: ubuntu-latest
steps:
- name: Bump image tag in sova-deploy
@@ -81,15 +95,18 @@ jobs:
git config user.email "ci-bot@sova.local"
git config user.name "sova-ci"
MAX_RETRIES=5
bump_values() {
local file="apps/adminpanel/values-${ENV}.yaml"
sed -i "s|^ repository:.*| repository: ${IMAGE_DEPLOY}|" "$file"
sed -i "s|^ tag:.*| tag: ${TAG}|" "$file"
sed -i "s|^ pullPolicy:.*| pullPolicy: IfNotPresent|" "$file"
}
case "$(uname -m)" in
x86_64|amd64) YQ_ARCH=amd64 ;;
aarch64|arm64) YQ_ARCH=arm64 ;;
*) echo "Unsupported arch: $(uname -m)"; exit 1 ;;
esac
curl -sSL -o /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/v4.44.3/yq_linux_${YQ_ARCH}"
chmod +x /usr/local/bin/yq
for attempt in $(seq 1 $MAX_RETRIES); do
git pull --rebase origin "${ENV}"
bump_values
yq -i ".image.repository = \"${IMAGE_DEPLOY}\"" "apps/adminpanel/values-${ENV}.yaml"
yq -i ".image.tag = \"${TAG}\"" "apps/adminpanel/values-${ENV}.yaml"
yq -i ".image.pullPolicy = \"IfNotPresent\"" "apps/adminpanel/values-${ENV}.yaml"
git add "apps/adminpanel/values-${ENV}.yaml"
git diff --cached --quiet && { echo "No changes"; exit 0; }
git commit -m "chore(adminpanel): bump ${ENV} to ${TAG}"
+1 -1
View File
@@ -10,7 +10,7 @@ RUN npm run build
FROM nginx:1.27-alpine AS runtime
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
RUN echo 'window.__ENV__ = window.__ENV__ || {};' > /usr/share/nginx/html/env.js
COPY public/env.js /usr/share/nginx/html/env.js
COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 80
+32 -30
View File
@@ -21,24 +21,26 @@ import { EditStockPage } from './pages/EditStockPage';
import { AddStockPage } from './pages/AddStockPage';
import { InfoclinicListPage } from './pages/InfoclinicListPage';
import { LostDoctorsPage } from './pages/LostDoctorsPage';
import { NewsListPage } from './pages/NewsListPage';
import { EditNewsPage } from './pages/EditNewsPage';
import { AddNewsPage } from './pages/AddNewsPage';
import { SitePromoListPage } from './pages/SitePromoListPage';
import { EditSitePromoPage } from './pages/EditSitePromoPage';
import { AddSitePromoPage } from './pages/AddSitePromoPage';
import { DiseaseListPage } from './pages/DiseaseListPage';
import { EditDiseasePage } from './pages/EditDiseasePage';
import { AddDiseasePage } from './pages/AddDiseasePage';
import { MedicalCenterListPage } from './pages/MedicalCenterListPage';
import { EditMedicalCenterPage } from './pages/EditMedicalCenterPage';
import { AddMedicalCenterPage } from './pages/AddMedicalCenterPage';
import { ArticleListPage } from './pages/ArticleListPage';
import { EditArticlePage } from './pages/EditArticlePage';
import { AddArticlePage } from './pages/AddArticlePage';
import { SiteServicesListPage } from './pages/SiteServicesListPage';
import { EditSiteServicesPage } from './pages/EditSiteServicesPage';
import { AddSiteServicesPage } from './pages/AddSiteServicesPage';
import {
NewsListPage,
NewsEditPage,
NewsCreatePage,
SitePromoListPage,
SitePromoEditPage,
SitePromoCreatePage,
DiseaseListPage,
DiseaseEditPage,
DiseaseCreatePage,
MedicalCenterListPage,
MedicalCenterEditPage,
MedicalCenterCreatePage,
ArticleListPage,
ArticleEditPage,
ArticleCreatePage,
SiteServicesListPage,
SiteServicesEditPage,
SiteServicesCreatePage,
} from './pages/content';
function App() {
return (
@@ -70,23 +72,23 @@ function App() {
<Route path="promotions/edit/:id" element={<EditStockPage/>} />
<Route path="promotions/create" element={<AddStockPage/>} />
<Route path="news" element={<NewsListPage />} />
<Route path="news/edit/:id" element={<EditNewsPage />} />
<Route path="news/create" element={<AddNewsPage />} />
<Route path="news/edit/:id" element={<NewsEditPage />} />
<Route path="news/create" element={<NewsCreatePage />} />
<Route path="site-promo" element={<SitePromoListPage />} />
<Route path="site-promo/edit/:id" element={<EditSitePromoPage />} />
<Route path="site-promo/create" element={<AddSitePromoPage />} />
<Route path="site-promo/edit/:id" element={<SitePromoEditPage />} />
<Route path="site-promo/create" element={<SitePromoCreatePage />} />
<Route path="disease" element={<DiseaseListPage />} />
<Route path="disease/edit/:id" element={<EditDiseasePage />} />
<Route path="disease/create" element={<AddDiseasePage />} />
<Route path="disease/edit/:id" element={<DiseaseEditPage />} />
<Route path="disease/create" element={<DiseaseCreatePage />} />
<Route path="medical-center" element={<MedicalCenterListPage />} />
<Route path="medical-center/edit/:id" element={<EditMedicalCenterPage />} />
<Route path="medical-center/create" element={<AddMedicalCenterPage />} />
<Route path="medical-center/edit/:id" element={<MedicalCenterEditPage />} />
<Route path="medical-center/create" element={<MedicalCenterCreatePage />} />
<Route path="article" element={<ArticleListPage />} />
<Route path="article/edit/:id" element={<EditArticlePage />} />
<Route path="article/create" element={<AddArticlePage />} />
<Route path="article/edit/:id" element={<ArticleEditPage />} />
<Route path="article/create" element={<ArticleCreatePage />} />
<Route path="site-services" element={<SiteServicesListPage />} />
<Route path="site-services/edit/:id" element={<EditSiteServicesPage />} />
<Route path="site-services/create" element={<AddSiteServicesPage />} />
<Route path="site-services/edit/:id" element={<SiteServicesEditPage />} />
<Route path="site-services/create" element={<SiteServicesCreatePage />} />
<Route path="*" element={<NotFoundPage />} />
</Route>
<Route path="*" element={<Navigate to="/" replace />} />
-55
View File
@@ -1,55 +0,0 @@
import { API, authHeader } from './apiSlice';
export const articleApi = API.injectEndpoints({
endpoints: (build) => ({
getArticleList: build.query({
query: ({ search = '', page = '' }) => {
let queryString = '?';
if (page) queryString += `page=${page}&limit=20`;
else queryString += `limit=20`;
if (search) queryString += `&search=${encodeURIComponent(search)}`;
return {
url: `/article/list${queryString}`,
};
},
refetchOnMountOrArgChange: true,
keepUnusedDataFor: 0,
}),
getArticle: build.query({
query: ({ articleId }) => ({
url: `/article/${articleId}`,
}),
}),
createArticle: build.mutation({
query: ({ data }) => ({
url: `/article/create`,
method: 'POST',
headers: authHeader(),
body: JSON.stringify(data),
}),
}),
updateArticle: build.mutation({
query: ({ articleId, data }) => ({
url: `/article/${articleId}`,
method: 'PUT',
headers: authHeader(),
body: JSON.stringify(data),
}),
}),
deleteArticle: build.mutation({
query: ({ articleId }) => ({
url: `/article/${articleId}`,
method: 'DELETE',
headers: authHeader(),
}),
}),
}),
});
export const {
useGetArticleQuery,
useGetArticleListQuery,
useCreateArticleMutation,
useUpdateArticleMutation,
useDeleteArticleMutation,
} = articleApi;
+117
View File
@@ -0,0 +1,117 @@
import { API, authHeader } from './apiSlice'
import { CONTENT_RESOURCES } from '../config/contentResources'
const buildListQuery = (basePath, { usesLimit = false } = {}) =>
({ search = '', page = 1, perPage = 20 } = {}) => {
let queryString = `?page=${page}`
if (usesLimit) {
queryString += `&limit=${perPage}`
} else {
queryString += `&perPage=${perPage}`
}
if (search) {
queryString += `&search=${encodeURIComponent(search)}`
}
return {
url: `${basePath}/list${queryString}`,
}
}
const injectResource = (resourceKey) => {
const { basePath, listUsesLimit } = CONTENT_RESOURCES[resourceKey]
const cap = resourceKey
.split('-')
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join('')
return API.injectEndpoints({
endpoints: (build) => ({
[`get${cap}List`]: build.query({
query: buildListQuery(basePath, { usesLimit: listUsesLimit }),
refetchOnMountOrArgChange: true,
keepUnusedDataFor: 0,
}),
[`get${cap}Item`]: build.query({
query: (id) => ({
url: `${basePath}/${id}`,
}),
}),
[`create${cap}`]: build.mutation({
query: (data) => ({
url: `${basePath}/create`,
method: 'POST',
headers: authHeader(),
body: JSON.stringify(data),
}),
}),
[`update${cap}`]: build.mutation({
query: ({ id, data }) => ({
url: `${basePath}/${id}`,
method: 'PUT',
headers: authHeader(),
body: JSON.stringify(data),
}),
}),
[`delete${cap}`]: build.mutation({
query: (id) => ({
url: `${basePath}/${id}`,
method: 'DELETE',
headers: authHeader(),
}),
}),
}),
overrideExisting: false,
})
}
const newsApi = injectResource('news')
const promoApi = injectResource('promo')
const diseaseApi = injectResource('disease')
const medicalCenterApi = injectResource('medical-center')
const articleApi = injectResource('article')
const siteServicesApi = injectResource('site-services')
export const contentHooks = {
news: {
useListQuery: newsApi.useGetNewsListQuery,
useItemQuery: newsApi.useGetNewsItemQuery,
useCreateMutation: newsApi.useCreateNewsMutation,
useUpdateMutation: newsApi.useUpdateNewsMutation,
useDeleteMutation: newsApi.useDeleteNewsMutation,
},
promo: {
useListQuery: promoApi.useGetPromoListQuery,
useItemQuery: promoApi.useGetPromoItemQuery,
useCreateMutation: promoApi.useCreatePromoMutation,
useUpdateMutation: promoApi.useUpdatePromoMutation,
useDeleteMutation: promoApi.useDeletePromoMutation,
},
disease: {
useListQuery: diseaseApi.useGetDiseaseListQuery,
useItemQuery: diseaseApi.useGetDiseaseItemQuery,
useCreateMutation: diseaseApi.useCreateDiseaseMutation,
useUpdateMutation: diseaseApi.useUpdateDiseaseMutation,
useDeleteMutation: diseaseApi.useDeleteDiseaseMutation,
},
'medical-center': {
useListQuery: medicalCenterApi.useGetMedicalCenterListQuery,
useItemQuery: medicalCenterApi.useGetMedicalCenterItemQuery,
useCreateMutation: medicalCenterApi.useCreateMedicalCenterMutation,
useUpdateMutation: medicalCenterApi.useUpdateMedicalCenterMutation,
useDeleteMutation: medicalCenterApi.useDeleteMedicalCenterMutation,
},
article: {
useListQuery: articleApi.useGetArticleListQuery,
useItemQuery: articleApi.useGetArticleItemQuery,
useCreateMutation: articleApi.useCreateArticleMutation,
useUpdateMutation: articleApi.useUpdateArticleMutation,
useDeleteMutation: articleApi.useDeleteArticleMutation,
},
'site-services': {
useListQuery: siteServicesApi.useGetSiteServicesListQuery,
useItemQuery: siteServicesApi.useGetSiteServicesItemQuery,
useCreateMutation: siteServicesApi.useCreateSiteServicesMutation,
useUpdateMutation: siteServicesApi.useUpdateSiteServicesMutation,
useDeleteMutation: siteServicesApi.useDeleteSiteServicesMutation,
},
}
-55
View File
@@ -1,55 +0,0 @@
import { API, authHeader } from './apiSlice';
export const diseaseApi = API.injectEndpoints({
endpoints: (build) => ({
getDiseaseList: build.query({
query: ({ search = '', page = '' }) => {
let queryString = '?';
if (page) queryString += `page=${page}&perPage=20`;
else queryString += `perPage=20`;
if (search) queryString += `&search=${encodeURIComponent(search)}`;
return {
url: `/disease/list${queryString}`,
};
},
refetchOnMountOrArgChange: true,
keepUnusedDataFor: 0,
}),
getDisease: build.query({
query: ({ diseaseId }) => ({
url: `/disease/${diseaseId}`,
}),
}),
createDisease: build.mutation({
query: ({ data }) => ({
url: `/disease/create`,
method: 'POST',
headers: authHeader(),
body: JSON.stringify(data),
}),
}),
updateDisease: build.mutation({
query: ({ diseaseId, data }) => ({
url: `/disease/${diseaseId}`,
method: 'PUT',
headers: authHeader(),
body: JSON.stringify(data),
}),
}),
deleteDisease: build.mutation({
query: ({ diseaseId }) => ({
url: `/disease/${diseaseId}`,
method: 'DELETE',
headers: authHeader(),
}),
}),
}),
});
export const {
useGetDiseaseQuery,
useGetDiseaseListQuery,
useCreateDiseaseMutation,
useUpdateDiseaseMutation,
useDeleteDiseaseMutation,
} = diseaseApi;
-55
View File
@@ -1,55 +0,0 @@
import { API, authHeader } from './apiSlice';
export const medicalCenterApi = API.injectEndpoints({
endpoints: (build) => ({
getMedicalCenterList: build.query({
query: ({ search = '', page = '' }) => {
let queryString = '?';
if (page) queryString += `page=${page}&perPage=20`;
else queryString += `perPage=20`;
if (search) queryString += `&search=${encodeURIComponent(search)}`;
return {
url: `/medical-center/list${queryString}`,
};
},
refetchOnMountOrArgChange: true,
keepUnusedDataFor: 0,
}),
getMedicalCenter: build.query({
query: ({ medicalCenterId }) => ({
url: `/medical-center/${medicalCenterId}`,
}),
}),
createMedicalCenter: build.mutation({
query: ({ data }) => ({
url: `/medical-center/create`,
method: 'POST',
headers: authHeader(),
body: JSON.stringify(data),
}),
}),
updateMedicalCenter: build.mutation({
query: ({ medicalCenterId, data }) => ({
url: `/medical-center/${medicalCenterId}`,
method: 'PUT',
headers: authHeader(),
body: JSON.stringify(data),
}),
}),
deleteMedicalCenter: build.mutation({
query: ({ medicalCenterId }) => ({
url: `/medical-center/${medicalCenterId}`,
method: 'DELETE',
headers: authHeader(),
}),
}),
}),
});
export const {
useGetMedicalCenterQuery,
useGetMedicalCenterListQuery,
useCreateMedicalCenterMutation,
useUpdateMedicalCenterMutation,
useDeleteMedicalCenterMutation,
} = medicalCenterApi;
-55
View File
@@ -1,55 +0,0 @@
import { API, authHeader } from './apiSlice';
export const newsApi = API.injectEndpoints({
endpoints: (build) => ({
getNewsList: build.query({
query: ({ search = '', page = '' }) => {
let queryString = '?';
if (page) queryString += `page=${page}&perPage=20`;
else queryString += `perPage=20`;
if (search) queryString += `&search=${encodeURIComponent(search)}`;
return {
url: `/news/list${queryString}`,
};
},
refetchOnMountOrArgChange: true,
keepUnusedDataFor: 0,
}),
getNews: build.query({
query: ({ newsId }) => ({
url: `/news/${newsId}`,
}),
}),
createNews: build.mutation({
query: ({ data }) => ({
url: `/news/create`,
method: 'POST',
headers: authHeader(),
body: JSON.stringify(data),
}),
}),
updateNews: build.mutation({
query: ({ newsId, data }) => ({
url: `/news/${newsId}`,
method: 'PUT',
headers: authHeader(),
body: JSON.stringify(data),
}),
}),
deleteNews: build.mutation({
query: ({ newsId }) => ({
url: `/news/${newsId}`,
method: 'DELETE',
headers: authHeader(),
}),
}),
}),
});
export const {
useGetNewsQuery,
useGetNewsListQuery,
useCreateNewsMutation,
useUpdateNewsMutation,
useDeleteNewsMutation,
} = newsApi;
-55
View File
@@ -1,55 +0,0 @@
import { API, authHeader } from './apiSlice';
export const promoApi = API.injectEndpoints({
endpoints: (build) => ({
getSitePromoList: build.query({
query: ({ search = '', page = '' }) => {
let queryString = '?';
if (page) queryString += `page=${page}&perPage=20`;
else queryString += `perPage=20`;
if (search) queryString += `&search=${encodeURIComponent(search)}`;
return {
url: `/promo/list${queryString}`,
};
},
refetchOnMountOrArgChange: true,
keepUnusedDataFor: 0,
}),
getSitePromo: build.query({
query: ({ promoId }) => ({
url: `/promo/${promoId}`,
}),
}),
createSitePromo: build.mutation({
query: ({ data }) => ({
url: `/promo/create`,
method: 'POST',
headers: authHeader(),
body: JSON.stringify(data),
}),
}),
updateSitePromo: build.mutation({
query: ({ promoId, data }) => ({
url: `/promo/${promoId}`,
method: 'PUT',
headers: authHeader(),
body: JSON.stringify(data),
}),
}),
deleteSitePromo: build.mutation({
query: ({ promoId }) => ({
url: `/promo/${promoId}`,
method: 'DELETE',
headers: authHeader(),
}),
}),
}),
});
export const {
useGetSitePromoQuery,
useGetSitePromoListQuery,
useCreateSitePromoMutation,
useUpdateSitePromoMutation,
useDeleteSitePromoMutation,
} = promoApi;
-55
View File
@@ -1,55 +0,0 @@
import { API, authHeader } from './apiSlice';
export const siteServicesApi = API.injectEndpoints({
endpoints: (build) => ({
getSiteServicesList: build.query({
query: ({ search = '', page = '' }) => {
let queryString = '?';
if (page) queryString += `page=${page}&perPage=20`;
else queryString += `perPage=20`;
if (search) queryString += `&search=${encodeURIComponent(search)}`;
return {
url: `/site-services/list${queryString}`,
};
},
refetchOnMountOrArgChange: true,
keepUnusedDataFor: 0,
}),
getSiteServices: build.query({
query: ({ siteServicesId }) => ({
url: `/site-services/${siteServicesId}`,
}),
}),
createSiteServices: build.mutation({
query: ({ data }) => ({
url: `/site-services/create`,
method: 'POST',
headers: authHeader(),
body: JSON.stringify(data),
}),
}),
updateSiteServices: build.mutation({
query: ({ siteServicesId, data }) => ({
url: `/site-services/${siteServicesId}`,
method: 'PUT',
headers: authHeader(),
body: JSON.stringify(data),
}),
}),
deleteSiteServices: build.mutation({
query: ({ siteServicesId }) => ({
url: `/site-services/${siteServicesId}`,
method: 'DELETE',
headers: authHeader(),
}),
}),
}),
});
export const {
useGetSiteServicesQuery,
useGetSiteServicesListQuery,
useCreateSiteServicesMutation,
useUpdateSiteServicesMutation,
useDeleteSiteServicesMutation,
} = siteServicesApi;
+2 -1
View File
@@ -1,4 +1,5 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import { API_BASE_URL } from '@/config/api'
export const authHeader = () => {
const token = localStorage.getItem('token')
@@ -8,7 +9,7 @@ export const authHeader = () => {
export const API = createApi({
reducerPath: 'API',
baseQuery: fetchBaseQuery({
baseUrl: 'https://api.sovamed.ru',
baseUrl: API_BASE_URL,
credentials: 'include',
}),
endpoints: (builder) => ({
+2 -1
View File
@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react';
import { apiUrl } from '@/config/api';
import { CertEditor } from '../Editors/CertEditor'
const isValidImage = (file) => {
@@ -15,7 +16,7 @@ export function CertificatesForm({ initCertificates, onChange }) {
const [certificates, setCertificates] = useState([]);
useEffect(() => {
const certificatesWithPictureUrl = initCertificates.map((init) => ({ ...init, picture: `https://api.sovamed.ru/uploads/${init.picture}`}))
const certificatesWithPictureUrl = initCertificates.map((init) => ({ ...init, picture: apiUrl(`/uploads/${init.picture}`)}))
setInitialCertificates([...certificatesWithPictureUrl])
setCertificates([...certificatesWithPictureUrl])
}, [initCertificates])
+2 -1
View File
@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react';
import { apiUrl } from '@/config/api';
import { CertEditor } from '../Editors/CertEditor'
const isValidImage = (file) => {
@@ -17,7 +18,7 @@ export function PortfolioForm({ initPortfolios, onChange }) {
useEffect(() => {
// console.log(initPortfolios)
const portfolioWithPictureUrl = initPortfolios.map((init) => {
if ( init.picture ) return ({ ...init, picture: `https://api.sovamed.ru/uploads/${init.picture}`})
if ( init.picture ) return ({ ...init, picture: apiUrl(`/uploads/${init.picture}`)})
return { ...init }
})
setInitialPorfolios([...portfolioWithPictureUrl])
+2 -1
View File
@@ -1,5 +1,6 @@
import React, { useState, useEffect, useRef } from 'react';
import { apiUrl } from '@/config/api';
import { CertEditor } from '../Editors/CertEditor'
const isValidImage = (file) => {
@@ -22,7 +23,7 @@ export function StocksForm({ initStocks, onChange }) {
// console.log(initStocks[0]?.startDate)
// console.log(typeof initStocks[0]?.startDate)
const portfolioWithPictureUrl = initStocks.map((init) => {
if ( init.picture ) return ({ ...init, picture: `https://api.sovamed.ru/uploads/${init.picture}`})
if ( init.picture ) return ({ ...init, picture: apiUrl(`/uploads/${init.picture}`)})
return { ...init }
}).map(init => {
const dateStart = new Date(init.startDate);
+7
View File
@@ -0,0 +1,7 @@
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'https://api.sovamed.ru'
export const apiUrl = (path) => {
const base = API_BASE_URL.replace(/\/$/, '')
const suffix = path.startsWith('/') ? path : `/${path}`
return `${base}${suffix}`
}
+223
View File
@@ -0,0 +1,223 @@
const baseContentFields = [
{ key: 'name', label: 'Название', type: 'text' },
{ key: 'active', label: 'Активно', type: 'checkbox' },
{ key: 'regionId', label: 'Регион', type: 'region' },
{ key: 'alias', label: 'Alias', type: 'text' },
{ key: 'anons', label: 'Анонс', type: 'html' },
{ key: 'content', label: 'Контент', type: 'html' },
]
const json = (key, label) => ({ key, label: `${label} (JSON)`, type: 'json' })
const text = (key, label) => ({ key, label, type: 'text' })
export const CONTENT_RESOURCES = {
news: {
slug: 'news',
basePath: '/news',
title: 'Новости',
titleSingle: 'новость',
icon: 'fas fa-newspaper',
listColumns: [
{ key: 'id', label: 'ID' },
{ key: 'name', label: 'Название' },
{ key: 'alias', label: 'Alias' },
{ key: 'active', label: 'Активно', format: 'bool' },
{ key: 'regionId', label: 'Регион' },
],
fields: [
...baseContentFields,
text('shortName', 'Короткое название'),
text('linkElPrice', 'Ссылка на прайс'),
text('timer', 'Таймер'),
text('timerBg', 'Фон таймера'),
json('formOrder', 'formOrder'),
json('linkServices', 'linkServices'),
json('linkStaff', 'linkStaff'),
json('photos', 'photos'),
],
},
promo: {
slug: 'site-promo',
basePath: '/promo',
title: 'Промо (контент)',
titleSingle: 'промо',
icon: 'fas fa-bullhorn',
listColumns: [
{ key: 'id', label: 'ID' },
{ key: 'name', label: 'Название' },
{ key: 'alias', label: 'Alias' },
{ key: 'active', label: 'Активно', format: 'bool' },
{ key: 'regionId', label: 'Регион' },
],
fields: [
...baseContentFields,
text('shortName', 'Короткое название'),
text('period', 'Период'),
text('timer', 'Таймер'),
text('timerBg', 'Фон таймера'),
json('clinics', 'clinics'),
json('linkServices', 'linkServices'),
json('linkStaff', 'linkStaff'),
json('photos', 'photos'),
],
},
disease: {
slug: 'disease',
basePath: '/disease',
title: 'Заболевания',
titleSingle: 'заболевание',
icon: 'fas fa-heartbeat',
listColumns: [
{ key: 'id', label: 'ID' },
{ key: 'name', label: 'Название' },
{ key: 'alias', label: 'Alias' },
{ key: 'active', label: 'Активно', format: 'bool' },
],
fields: [
...baseContentFields,
text('previewPicture', 'previewPicture'),
{ key: 'hidePicture', label: 'hidePicture', type: 'checkbox' },
text('readTime', 'readTime'),
text('diseasesName', 'diseasesName'),
text('diseasesOtherName', 'diseasesOtherName'),
text('symptom', 'symptom'),
text('staff', 'staff'),
text('bibliography', 'bibliography'),
json('tagsImportant', 'tagsImportant'),
json('tags', 'tags'),
json('linkServices', 'linkServices'),
json('staffList', 'staffList'),
json('staffPost', 'staffPost'),
json('staffPostExclude', 'staffPostExclude'),
json('linkFaq', 'linkFaq'),
json('staffCheck', 'staffCheck'),
],
},
'medical-center': {
slug: 'medical-center',
basePath: '/medical-center',
title: 'Медцентры',
titleSingle: 'медцентр',
icon: 'fas fa-hospital',
listColumns: [
{ key: 'id', label: 'ID' },
{ key: 'name', label: 'Название' },
{ key: 'alias', label: 'Alias' },
{ key: 'active', label: 'Активно', format: 'bool' },
],
fields: [
...baseContentFields,
text('mainLinkStaff', 'mainLinkStaff'),
text('plusText', 'plusText'),
text('plusTitle', 'plusTitle'),
text('processText', 'processText'),
text('processTitle', 'processTitle'),
text('servicesTitle', 'servicesTitle'),
text('trainingText', 'trainingText'),
text('trainingTextTitle', 'trainingTextTitle'),
text('whyText', 'whyText'),
text('whyTitle', 'whyTitle'),
{ key: 'hidePicture', label: 'hidePicture', type: 'number' },
json('kodUslug', 'kodUslug'),
json('doctors', 'doctors'),
json('services', 'services'),
json('articles', 'articles'),
json('txtUp', 'txtUp'),
json('contraindications', 'contraindications'),
json('indications', 'indications'),
json('linkSale', 'linkSale'),
json('plusList', 'plusList'),
json('servicesList', 'servicesList'),
json('servicesPhotos', 'servicesPhotos'),
json('sortStaff', 'sortStaff'),
],
},
article: {
slug: 'article',
basePath: '/article',
title: 'Статьи',
titleSingle: 'статью',
icon: 'fas fa-file-alt',
listUsesMeta: true,
listUsesLimit: true,
listColumns: [
{ key: 'id', label: 'ID' },
{ key: 'name', label: 'Название' },
{ key: 'alias', label: 'Alias' },
{ key: 'active', label: 'Активно', format: 'bool' },
],
fields: [
...baseContentFields,
text('previewPicture', 'previewPicture'),
json('doctors', 'doctors'),
json('services', 'services'),
],
},
'site-services': {
slug: 'site-services',
basePath: '/site-services',
title: 'Услуги сайта',
titleSingle: 'услугу',
icon: 'fas fa-concierge-bell',
listColumns: [
{ key: 'id', label: 'ID' },
{ key: 'name', label: 'Название' },
{ key: 'alias', label: 'Alias' },
{ key: 'active', label: 'Активно', format: 'bool' },
],
fields: [
...baseContentFields,
text('previewImg', 'previewImg'),
text('partPrice', 'partPrice'),
text('pokazaniya', 'pokazaniya'),
text('preparation', 'preparation'),
text('protivopokazaniya', 'protivopokazaniya'),
text('bannerImg', 'bannerImg'),
text('bannerImgM', 'bannerImgM'),
text('bannerImgUrl', 'bannerImgUrl'),
text('downloadFile', 'downloadFile'),
text('fullWidthBanner', 'fullWidthBanner'),
text('kodUslug', 'kodUslug'),
text('linkPrice', 'linkPrice'),
text('photosTitle', 'photosTitle'),
text('contraindicationsList', 'contraindicationsList'),
text('customBlockText', 'customBlockText'),
text('customBlockText2', 'customBlockText2'),
text('customBlockTitle', 'customBlockTitle'),
text('customBlockTitle2', 'customBlockTitle2'),
text('indicationsList', 'indicationsList'),
text('plusList', 'plusList'),
text('plusText', 'plusText'),
text('plusTitle', 'plusTitle'),
text('prepareTitle', 'prepareTitle'),
text('processText', 'processText'),
text('processTitle', 'processTitle'),
text('servicesList', 'servicesList'),
text('servicesTitle', 'servicesTitle'),
text('textUp', 'textUp'),
text('trainingText', 'trainingText'),
text('whyText', 'whyText'),
text('whyTitle', 'whyTitle'),
{ key: 'hidePicture', label: 'hidePicture', type: 'number' },
json('linkVideoreviews', 'linkVideoreviews'),
json('faq', 'faq'),
json('hideSignBtn', 'hideSignBtn'),
json('quiz', 'quiz'),
json('tags', 'tags'),
json('tagsImportant', 'tagsImportant'),
json('clinics', 'clinics'),
json('staffUp', 'staffUp'),
json('advantages', 'advantages'),
json('saleId', 'saleId'),
json('sortStaff', 'sortStaff'),
json('linkArticlesServices', 'linkArticlesServices'),
json('servicesPhotos', 'servicesPhotos'),
json('linkFaq', 'linkFaq'),
json('linkServices', 'linkServices'),
json('linkStaff', 'linkStaff'),
json('photos', 'photos'),
],
},
}
export const CONTENT_RESOURCE_KEYS = Object.keys(CONTENT_RESOURCES)
+3 -1
View File
@@ -2,6 +2,8 @@ import { useState, useEffect } from 'react';
import axios from 'axios';
import { useSelector } from 'react-redux';
import { apiUrl } from '@/config/api';
import { useGetFilialsQuery } from '../api/apiFilial';
import { useGetDepartmentsQuery } from '../api/apiDepartment';
import { selectRegions } from '../store/slice/regionSlice';
@@ -31,7 +33,7 @@ export function useLostDoctors() {
Promise.all(
data.data.map(item => {
const fetchString = fetchParams.filter((param => Boolean(item[param]))).map(param => `${param}=${item[param]}`).join('&');
return axios.get(`https://api.sovamed.ru/idoctor/list?${fetchString}`, {
return axios.get(apiUrl(`/idoctor/list?${fetchString}`), {
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
})
.then(res => {
+3 -1
View File
@@ -2,6 +2,8 @@ import { useState, useEffect } from 'react';
import axios from 'axios';
import { useSelector } from 'react-redux';
import { apiUrl } from '@/config/api';
import { useGetSpecialistsQuery, useGetSpecialistQuery } from '../api/apiSpecialist';
import { useGetKodopersQuery } from '../api/apiKodoper';
import { useGetFilialsQuery } from '../api/apiFilial';
@@ -27,7 +29,7 @@ export function useSpecialist(id) {
Promise.all(
specialist.kodoper.map(code =>
axios.get(`https://api.sovamed.ru/pricelist/list?search=${code}`, {
axios.get(apiUrl(`/pricelist/list?search=${code}`), {
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
})
.then(res => {
-88
View File
@@ -1,88 +0,0 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { useCreateArticleMutation } from '/src/api/apiArticle';
import { selectRegions } from '../store/slice/regionSlice';
import { TextEditor } from '../components/Editors/TextEditor';
import { LoadingComponent } from '../components/Placeholders/LoadingComponent';
import { ErrorComponent } from '../components/Placeholders/ErrorComponent';
import { NotFindElement } from '../components/Placeholders/NotFindElement';
import { EditElementForm } from '../components/Forms/EditElementForm';
import Modal from '../components/Modals/Modal';
export function AddArticlePage() {
const navigate = useNavigate();
const navigateBack = () => navigate(`/article`);
const regions = useSelector(selectRegions);
const [createArticle] = useCreateArticleMutation();
const [isModalSuccess, setModalSuccess] = useState(false);
const [errors, setErrors] = useState({ name: '', alias: '', regionId: '' });
const [form, setForm] = useState({
name: '',
active: false,
regionId: '',
alias: '',
previewPicture: '',
doctors: '',
services: '',
});
const [anons, setAnons] = useState('');
const [content, setContent] = useState('');
const handleChange = (key) => (e) => {
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
setForm((f) => ({ ...f, [key]: value }));
if (key === 'name' || key === 'alias' || key === 'regionId') setErrors((err) => ({ ...err, [key]: '' }));
};
const handleSave = async () => {
const newErrors = { name: '', alias: '', regionId: '' };
let hasError = false;
if (!String(form.name ?? '').trim()) { newErrors.name = 'Название не может быть пустым'; hasError = true; }
if (!String(form.alias ?? '').trim()) { newErrors.alias = 'Alias не может быть пустым'; hasError = true; }
if (form.regionId === '' || form.regionId == null) { newErrors.regionId = 'Укажите регион'; hasError = true; }
if (hasError) { setErrors(newErrors); window.alert('Пожалуйста исправьте ошибки в форме.'); return; }
const data = { anons, content };
try {
data.name = form.name === '' ? null : form.name;
data.active = Boolean(form.active);
data.regionId = form.regionId === '' ? null : Number(form.regionId);
data.alias = form.alias === '' ? null : form.alias;
data.previewPicture = form.previewPicture === '' ? null : form.previewPicture;
data.doctors = !form.doctors || !String(form.doctors).trim() ? null : JSON.parse(form.doctors);
data.services = !form.services || !String(form.services).trim() ? null : JSON.parse(form.services);
} catch (e) { window.alert('Пожалуйста исправьте ошибки в форме.'); return; }
try {
const response = await createArticle({ data }).unwrap();
setModalSuccess(true);
window.setTimeout(() => { navigate(`/article/edit/${response.id}`); window.location.reload(); }, 2000);
} catch (err) {
console.error('Ошибка сохранения:', err);
}
};
return (
<EditElementForm navigateBack={navigateBack} header={`Добавление: статью`} handleSave={handleSave} handleDelete={() => {}} isAddSpecialist={true}>
<div className="form-group"><label>Название</label><input type="text" className="form-control" value={form.name} onChange={handleChange('name')} />
{errors.name && <small className="text-danger">{errors.name}</small>}</div>
<div className="form-group form-check"><input type="checkbox" className="form-check-input" id="active" checked={Boolean(form.active)} onChange={handleChange('active')} /><label className="form-check-label" htmlFor="active">Активно</label></div>
<div className="form-group"><label>Регион</label><select className="form-control" value={form.regionId === '' || form.regionId == null ? '' : String(form.regionId)} onChange={handleChange('regionId')}><option value=""></option>{Object.entries(regions).map(([rid, name]) => (<option key={rid} value={rid}>{name}</option>))}</select>
{errors.regionId && <small className="text-danger">{errors.regionId}</small>}</div>
<div className="form-group"><label>Alias</label><input type="text" className="form-control" value={form.alias} onChange={handleChange('alias')} />
{errors.alias && <small className="text-danger">{errors.alias}</small>}</div>
<div className="form-group"><label>Анонс</label><TextEditor content={anons} setContent={setAnons} /></div>
<div className="form-group"><label>Контент</label><TextEditor content={content} setContent={setContent} /></div>
<div className="form-group"><label>previewPicture</label><input type="text" className="form-control" value={form.previewPicture} onChange={handleChange('previewPicture')} /></div>
<div className="form-group"><label>doctors (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.doctors} onChange={handleChange('doctors')} /></div>
<div className="form-group"><label>services (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.services} onChange={handleChange('services')} /></div>
<Modal isOpen={isModalSuccess} title="Изменения внесены" hasButtons={false}><p className="mb-1">Изменения успешно внесены.</p></Modal>
</EditElementForm>
);
}
-127
View File
@@ -1,127 +0,0 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { useCreateDiseaseMutation } from '/src/api/apiDisease';
import { selectRegions } from '../store/slice/regionSlice';
import { TextEditor } from '../components/Editors/TextEditor';
import { LoadingComponent } from '../components/Placeholders/LoadingComponent';
import { ErrorComponent } from '../components/Placeholders/ErrorComponent';
import { NotFindElement } from '../components/Placeholders/NotFindElement';
import { EditElementForm } from '../components/Forms/EditElementForm';
import Modal from '../components/Modals/Modal';
export function AddDiseasePage() {
const navigate = useNavigate();
const navigateBack = () => navigate(`/disease`);
const regions = useSelector(selectRegions);
const [createDisease] = useCreateDiseaseMutation();
const [isModalSuccess, setModalSuccess] = useState(false);
const [errors, setErrors] = useState({ name: '', alias: '', regionId: '' });
const [form, setForm] = useState({
name: '',
active: false,
regionId: '',
alias: '',
previewPicture: '',
hidePicture: false,
readTime: '',
diseasesName: '',
diseasesOtherName: '',
symptom: '',
staff: '',
bibliography: '',
tagsImportant: '',
tags: '',
linkServices: '',
staffList: '',
staffPost: '',
staffPostExclude: '',
linkFaq: '',
staffCheck: '',
});
const [anons, setAnons] = useState('');
const [content, setContent] = useState('');
const handleChange = (key) => (e) => {
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
setForm((f) => ({ ...f, [key]: value }));
if (key === 'name' || key === 'alias' || key === 'regionId') setErrors((err) => ({ ...err, [key]: '' }));
};
const handleSave = async () => {
const newErrors = { name: '', alias: '', regionId: '' };
let hasError = false;
if (!String(form.name ?? '').trim()) { newErrors.name = 'Название не может быть пустым'; hasError = true; }
if (!String(form.alias ?? '').trim()) { newErrors.alias = 'Alias не может быть пустым'; hasError = true; }
if (form.regionId === '' || form.regionId == null) { newErrors.regionId = 'Укажите регион'; hasError = true; }
if (hasError) { setErrors(newErrors); window.alert('Пожалуйста исправьте ошибки в форме.'); return; }
const data = { anons, content };
try {
data.name = form.name === '' ? null : form.name;
data.active = Boolean(form.active);
data.regionId = form.regionId === '' ? null : Number(form.regionId);
data.alias = form.alias === '' ? null : form.alias;
data.previewPicture = form.previewPicture === '' ? null : form.previewPicture;
data.hidePicture = Boolean(form.hidePicture);
data.readTime = form.readTime === '' ? null : form.readTime;
data.diseasesName = form.diseasesName === '' ? null : form.diseasesName;
data.diseasesOtherName = form.diseasesOtherName === '' ? null : form.diseasesOtherName;
data.symptom = form.symptom === '' ? null : form.symptom;
data.staff = form.staff === '' ? null : form.staff;
data.bibliography = form.bibliography === '' ? null : form.bibliography;
data.tagsImportant = !form.tagsImportant || !String(form.tagsImportant).trim() ? null : JSON.parse(form.tagsImportant);
data.tags = !form.tags || !String(form.tags).trim() ? null : JSON.parse(form.tags);
data.linkServices = !form.linkServices || !String(form.linkServices).trim() ? null : JSON.parse(form.linkServices);
data.staffList = !form.staffList || !String(form.staffList).trim() ? null : JSON.parse(form.staffList);
data.staffPost = !form.staffPost || !String(form.staffPost).trim() ? null : JSON.parse(form.staffPost);
data.staffPostExclude = !form.staffPostExclude || !String(form.staffPostExclude).trim() ? null : JSON.parse(form.staffPostExclude);
data.linkFaq = !form.linkFaq || !String(form.linkFaq).trim() ? null : JSON.parse(form.linkFaq);
data.staffCheck = !form.staffCheck || !String(form.staffCheck).trim() ? null : JSON.parse(form.staffCheck);
} catch (e) { window.alert('Пожалуйста исправьте ошибки в форме.'); return; }
try {
const response = await createDisease({ data }).unwrap();
setModalSuccess(true);
window.setTimeout(() => { navigate(`/disease/edit/${response.id}`); window.location.reload(); }, 2000);
} catch (err) {
console.error('Ошибка сохранения:', err);
}
};
return (
<EditElementForm navigateBack={navigateBack} header={`Добавление: заболевание`} handleSave={handleSave} handleDelete={() => {}} isAddSpecialist={true}>
<div className="form-group"><label>Название</label><input type="text" className="form-control" value={form.name} onChange={handleChange('name')} />
{errors.name && <small className="text-danger">{errors.name}</small>}</div>
<div className="form-group form-check"><input type="checkbox" className="form-check-input" id="active" checked={Boolean(form.active)} onChange={handleChange('active')} /><label className="form-check-label" htmlFor="active">Активно</label></div>
<div className="form-group"><label>Регион</label><select className="form-control" value={form.regionId === '' || form.regionId == null ? '' : String(form.regionId)} onChange={handleChange('regionId')}><option value=""></option>{Object.entries(regions).map(([rid, name]) => (<option key={rid} value={rid}>{name}</option>))}</select>
{errors.regionId && <small className="text-danger">{errors.regionId}</small>}</div>
<div className="form-group"><label>Alias</label><input type="text" className="form-control" value={form.alias} onChange={handleChange('alias')} />
{errors.alias && <small className="text-danger">{errors.alias}</small>}</div>
<div className="form-group"><label>Анонс</label><TextEditor content={anons} setContent={setAnons} /></div>
<div className="form-group"><label>Контент</label><TextEditor content={content} setContent={setContent} /></div>
<div className="form-group"><label>previewPicture</label><input type="text" className="form-control" value={form.previewPicture} onChange={handleChange('previewPicture')} /></div>
<div className="form-group form-check"><input type="checkbox" className="form-check-input" id="hidePicture" checked={Boolean(form.hidePicture)} onChange={handleChange('hidePicture')} /><label className="form-check-label" htmlFor="hidePicture">hidePicture</label></div>
<div className="form-group"><label>readTime</label><input type="text" className="form-control" value={form.readTime} onChange={handleChange('readTime')} /></div>
<div className="form-group"><label>diseasesName</label><input type="text" className="form-control" value={form.diseasesName} onChange={handleChange('diseasesName')} /></div>
<div className="form-group"><label>diseasesOtherName</label><input type="text" className="form-control" value={form.diseasesOtherName} onChange={handleChange('diseasesOtherName')} /></div>
<div className="form-group"><label>symptom</label><input type="text" className="form-control" value={form.symptom} onChange={handleChange('symptom')} /></div>
<div className="form-group"><label>staff</label><input type="text" className="form-control" value={form.staff} onChange={handleChange('staff')} /></div>
<div className="form-group"><label>bibliography</label><input type="text" className="form-control" value={form.bibliography} onChange={handleChange('bibliography')} /></div>
<div className="form-group"><label>tagsImportant (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.tagsImportant} onChange={handleChange('tagsImportant')} /></div>
<div className="form-group"><label>tags (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.tags} onChange={handleChange('tags')} /></div>
<div className="form-group"><label>linkServices (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.linkServices} onChange={handleChange('linkServices')} /></div>
<div className="form-group"><label>staffList (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.staffList} onChange={handleChange('staffList')} /></div>
<div className="form-group"><label>staffPost (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.staffPost} onChange={handleChange('staffPost')} /></div>
<div className="form-group"><label>staffPostExclude (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.staffPostExclude} onChange={handleChange('staffPostExclude')} /></div>
<div className="form-group"><label>linkFaq (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.linkFaq} onChange={handleChange('linkFaq')} /></div>
<div className="form-group"><label>staffCheck (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.staffCheck} onChange={handleChange('staffCheck')} /></div>
<Modal isOpen={isModalSuccess} title="Изменения внесены" hasButtons={false}><p className="mb-1">Изменения успешно внесены.</p></Modal>
</EditElementForm>
);
}
-148
View File
@@ -1,148 +0,0 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { useCreateMedicalCenterMutation } from '/src/api/apiMedicalCenter';
import { selectRegions } from '../store/slice/regionSlice';
import { TextEditor } from '../components/Editors/TextEditor';
import { LoadingComponent } from '../components/Placeholders/LoadingComponent';
import { ErrorComponent } from '../components/Placeholders/ErrorComponent';
import { NotFindElement } from '../components/Placeholders/NotFindElement';
import { EditElementForm } from '../components/Forms/EditElementForm';
import Modal from '../components/Modals/Modal';
export function AddMedicalCenterPage() {
const navigate = useNavigate();
const navigateBack = () => navigate(`/medical-center`);
const regions = useSelector(selectRegions);
const [createMedicalCenter] = useCreateMedicalCenterMutation();
const [isModalSuccess, setModalSuccess] = useState(false);
const [errors, setErrors] = useState({ name: '', alias: '', regionId: '' });
const [form, setForm] = useState({
name: '',
active: false,
regionId: '',
alias: '',
mainLinkStaff: '',
plusText: '',
plusTitle: '',
processText: '',
processTitle: '',
servicesTitle: '',
trainingText: '',
trainingTextTitle: '',
whyText: '',
whyTitle: '',
hidePicture: '',
kodUslug: '',
doctors: '',
services: '',
articles: '',
txtUp: '',
contraindications: '',
indications: '',
linkSale: '',
plusList: '',
servicesList: '',
servicesPhotos: '',
sortStaff: '',
});
const [anons, setAnons] = useState('');
const [content, setContent] = useState('');
const handleChange = (key) => (e) => {
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
setForm((f) => ({ ...f, [key]: value }));
if (key === 'name' || key === 'alias' || key === 'regionId') setErrors((err) => ({ ...err, [key]: '' }));
};
const handleSave = async () => {
const newErrors = { name: '', alias: '', regionId: '' };
let hasError = false;
if (!String(form.name ?? '').trim()) { newErrors.name = 'Название не может быть пустым'; hasError = true; }
if (!String(form.alias ?? '').trim()) { newErrors.alias = 'Alias не может быть пустым'; hasError = true; }
if (form.regionId === '' || form.regionId == null) { newErrors.regionId = 'Укажите регион'; hasError = true; }
if (hasError) { setErrors(newErrors); window.alert('Пожалуйста исправьте ошибки в форме.'); return; }
const data = { anons, content };
try {
data.name = form.name === '' ? null : form.name;
data.active = Boolean(form.active);
data.regionId = form.regionId === '' ? null : Number(form.regionId);
data.alias = form.alias === '' ? null : form.alias;
data.mainLinkStaff = form.mainLinkStaff === '' ? null : form.mainLinkStaff;
data.plusText = form.plusText === '' ? null : form.plusText;
data.plusTitle = form.plusTitle === '' ? null : form.plusTitle;
data.processText = form.processText === '' ? null : form.processText;
data.processTitle = form.processTitle === '' ? null : form.processTitle;
data.servicesTitle = form.servicesTitle === '' ? null : form.servicesTitle;
data.trainingText = form.trainingText === '' ? null : form.trainingText;
data.trainingTextTitle = form.trainingTextTitle === '' ? null : form.trainingTextTitle;
data.whyText = form.whyText === '' ? null : form.whyText;
data.whyTitle = form.whyTitle === '' ? null : form.whyTitle;
data.hidePicture = form.hidePicture === '' ? null : Number(form.hidePicture);
data.kodUslug = !form.kodUslug || !String(form.kodUslug).trim() ? null : JSON.parse(form.kodUslug);
data.doctors = !form.doctors || !String(form.doctors).trim() ? null : JSON.parse(form.doctors);
data.services = !form.services || !String(form.services).trim() ? null : JSON.parse(form.services);
data.articles = !form.articles || !String(form.articles).trim() ? null : JSON.parse(form.articles);
data.txtUp = !form.txtUp || !String(form.txtUp).trim() ? null : JSON.parse(form.txtUp);
data.contraindications = !form.contraindications || !String(form.contraindications).trim() ? null : JSON.parse(form.contraindications);
data.indications = !form.indications || !String(form.indications).trim() ? null : JSON.parse(form.indications);
data.linkSale = !form.linkSale || !String(form.linkSale).trim() ? null : JSON.parse(form.linkSale);
data.plusList = !form.plusList || !String(form.plusList).trim() ? null : JSON.parse(form.plusList);
data.servicesList = !form.servicesList || !String(form.servicesList).trim() ? null : JSON.parse(form.servicesList);
data.servicesPhotos = !form.servicesPhotos || !String(form.servicesPhotos).trim() ? null : JSON.parse(form.servicesPhotos);
data.sortStaff = !form.sortStaff || !String(form.sortStaff).trim() ? null : JSON.parse(form.sortStaff);
} catch (e) { window.alert('Пожалуйста исправьте ошибки в форме.'); return; }
try {
const response = await createMedicalCenter({ data }).unwrap();
setModalSuccess(true);
window.setTimeout(() => { navigate(`/medical-center/edit/${response.id}`); window.location.reload(); }, 2000);
} catch (err) {
console.error('Ошибка сохранения:', err);
}
};
return (
<EditElementForm navigateBack={navigateBack} header={`Добавление: медцентр`} handleSave={handleSave} handleDelete={() => {}} isAddSpecialist={true}>
<div className="form-group"><label>Название</label><input type="text" className="form-control" value={form.name} onChange={handleChange('name')} />
{errors.name && <small className="text-danger">{errors.name}</small>}</div>
<div className="form-group form-check"><input type="checkbox" className="form-check-input" id="active" checked={Boolean(form.active)} onChange={handleChange('active')} /><label className="form-check-label" htmlFor="active">Активно</label></div>
<div className="form-group"><label>Регион</label><select className="form-control" value={form.regionId === '' || form.regionId == null ? '' : String(form.regionId)} onChange={handleChange('regionId')}><option value=""></option>{Object.entries(regions).map(([rid, name]) => (<option key={rid} value={rid}>{name}</option>))}</select>
{errors.regionId && <small className="text-danger">{errors.regionId}</small>}</div>
<div className="form-group"><label>Alias</label><input type="text" className="form-control" value={form.alias} onChange={handleChange('alias')} />
{errors.alias && <small className="text-danger">{errors.alias}</small>}</div>
<div className="form-group"><label>Анонс</label><TextEditor content={anons} setContent={setAnons} /></div>
<div className="form-group"><label>Контент</label><TextEditor content={content} setContent={setContent} /></div>
<div className="form-group"><label>mainLinkStaff</label><input type="text" className="form-control" value={form.mainLinkStaff} onChange={handleChange('mainLinkStaff')} /></div>
<div className="form-group"><label>plusText</label><input type="text" className="form-control" value={form.plusText} onChange={handleChange('plusText')} /></div>
<div className="form-group"><label>plusTitle</label><input type="text" className="form-control" value={form.plusTitle} onChange={handleChange('plusTitle')} /></div>
<div className="form-group"><label>processText</label><input type="text" className="form-control" value={form.processText} onChange={handleChange('processText')} /></div>
<div className="form-group"><label>processTitle</label><input type="text" className="form-control" value={form.processTitle} onChange={handleChange('processTitle')} /></div>
<div className="form-group"><label>servicesTitle</label><input type="text" className="form-control" value={form.servicesTitle} onChange={handleChange('servicesTitle')} /></div>
<div className="form-group"><label>trainingText</label><input type="text" className="form-control" value={form.trainingText} onChange={handleChange('trainingText')} /></div>
<div className="form-group"><label>trainingTextTitle</label><input type="text" className="form-control" value={form.trainingTextTitle} onChange={handleChange('trainingTextTitle')} /></div>
<div className="form-group"><label>whyText</label><input type="text" className="form-control" value={form.whyText} onChange={handleChange('whyText')} /></div>
<div className="form-group"><label>whyTitle</label><input type="text" className="form-control" value={form.whyTitle} onChange={handleChange('whyTitle')} /></div>
<div className="form-group"><label>hidePicture</label><input type="number" className="form-control" value={form.hidePicture} onChange={handleChange('hidePicture')} /></div>
<div className="form-group"><label>kodUslug (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.kodUslug} onChange={handleChange('kodUslug')} /></div>
<div className="form-group"><label>doctors (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.doctors} onChange={handleChange('doctors')} /></div>
<div className="form-group"><label>services (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.services} onChange={handleChange('services')} /></div>
<div className="form-group"><label>articles (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.articles} onChange={handleChange('articles')} /></div>
<div className="form-group"><label>txtUp (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.txtUp} onChange={handleChange('txtUp')} /></div>
<div className="form-group"><label>contraindications (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.contraindications} onChange={handleChange('contraindications')} /></div>
<div className="form-group"><label>indications (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.indications} onChange={handleChange('indications')} /></div>
<div className="form-group"><label>linkSale (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.linkSale} onChange={handleChange('linkSale')} /></div>
<div className="form-group"><label>plusList (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.plusList} onChange={handleChange('plusList')} /></div>
<div className="form-group"><label>servicesList (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.servicesList} onChange={handleChange('servicesList')} /></div>
<div className="form-group"><label>servicesPhotos (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.servicesPhotos} onChange={handleChange('servicesPhotos')} /></div>
<div className="form-group"><label>sortStaff (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.sortStaff} onChange={handleChange('sortStaff')} /></div>
<Modal isOpen={isModalSuccess} title="Изменения внесены" hasButtons={false}><p className="mb-1">Изменения успешно внесены.</p></Modal>
</EditElementForm>
);
}
-103
View File
@@ -1,103 +0,0 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { useCreateNewsMutation } from '/src/api/apiNews';
import { selectRegions } from '../store/slice/regionSlice';
import { TextEditor } from '../components/Editors/TextEditor';
import { LoadingComponent } from '../components/Placeholders/LoadingComponent';
import { ErrorComponent } from '../components/Placeholders/ErrorComponent';
import { NotFindElement } from '../components/Placeholders/NotFindElement';
import { EditElementForm } from '../components/Forms/EditElementForm';
import Modal from '../components/Modals/Modal';
export function AddNewsPage() {
const navigate = useNavigate();
const navigateBack = () => navigate(`/news`);
const regions = useSelector(selectRegions);
const [createNews] = useCreateNewsMutation();
const [isModalSuccess, setModalSuccess] = useState(false);
const [errors, setErrors] = useState({ name: '', alias: '', regionId: '' });
const [form, setForm] = useState({
name: '',
active: false,
regionId: '',
alias: '',
shortName: '',
linkElPrice: '',
timer: '',
timerBg: '',
formOrder: '',
linkServices: '',
linkStaff: '',
photos: '',
});
const [anons, setAnons] = useState('');
const [content, setContent] = useState('');
const handleChange = (key) => (e) => {
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
setForm((f) => ({ ...f, [key]: value }));
if (key === 'name' || key === 'alias' || key === 'regionId') setErrors((err) => ({ ...err, [key]: '' }));
};
const handleSave = async () => {
const newErrors = { name: '', alias: '', regionId: '' };
let hasError = false;
if (!String(form.name ?? '').trim()) { newErrors.name = 'Название не может быть пустым'; hasError = true; }
if (!String(form.alias ?? '').trim()) { newErrors.alias = 'Alias не может быть пустым'; hasError = true; }
if (form.regionId === '' || form.regionId == null) { newErrors.regionId = 'Укажите регион'; hasError = true; }
if (hasError) { setErrors(newErrors); window.alert('Пожалуйста исправьте ошибки в форме.'); return; }
const data = { anons, content };
try {
data.name = form.name === '' ? null : form.name;
data.active = Boolean(form.active);
data.regionId = form.regionId === '' ? null : Number(form.regionId);
data.alias = form.alias === '' ? null : form.alias;
data.shortName = form.shortName === '' ? null : form.shortName;
data.linkElPrice = form.linkElPrice === '' ? null : form.linkElPrice;
data.timer = form.timer === '' ? null : form.timer;
data.timerBg = form.timerBg === '' ? null : form.timerBg;
data.formOrder = !form.formOrder || !String(form.formOrder).trim() ? null : JSON.parse(form.formOrder);
data.linkServices = !form.linkServices || !String(form.linkServices).trim() ? null : JSON.parse(form.linkServices);
data.linkStaff = !form.linkStaff || !String(form.linkStaff).trim() ? null : JSON.parse(form.linkStaff);
data.photos = !form.photos || !String(form.photos).trim() ? null : JSON.parse(form.photos);
} catch (e) { window.alert('Пожалуйста исправьте ошибки в форме.'); return; }
try {
const response = await createNews({ data }).unwrap();
setModalSuccess(true);
window.setTimeout(() => { navigate(`/news/edit/${response.id}`); window.location.reload(); }, 2000);
} catch (err) {
console.error('Ошибка сохранения:', err);
}
};
return (
<EditElementForm navigateBack={navigateBack} header={`Добавление: новость`} handleSave={handleSave} handleDelete={() => {}} isAddSpecialist={true}>
<div className="form-group"><label>Название</label><input type="text" className="form-control" value={form.name} onChange={handleChange('name')} />
{errors.name && <small className="text-danger">{errors.name}</small>}</div>
<div className="form-group form-check"><input type="checkbox" className="form-check-input" id="active" checked={Boolean(form.active)} onChange={handleChange('active')} /><label className="form-check-label" htmlFor="active">Активно</label></div>
<div className="form-group"><label>Регион</label><select className="form-control" value={form.regionId === '' || form.regionId == null ? '' : String(form.regionId)} onChange={handleChange('regionId')}><option value=""></option>{Object.entries(regions).map(([rid, name]) => (<option key={rid} value={rid}>{name}</option>))}</select>
{errors.regionId && <small className="text-danger">{errors.regionId}</small>}</div>
<div className="form-group"><label>Alias</label><input type="text" className="form-control" value={form.alias} onChange={handleChange('alias')} />
{errors.alias && <small className="text-danger">{errors.alias}</small>}</div>
<div className="form-group"><label>Анонс</label><TextEditor content={anons} setContent={setAnons} /></div>
<div className="form-group"><label>Контент</label><TextEditor content={content} setContent={setContent} /></div>
<div className="form-group"><label>Короткое название</label><input type="text" className="form-control" value={form.shortName} onChange={handleChange('shortName')} /></div>
<div className="form-group"><label>Ссылка на прайс</label><input type="text" className="form-control" value={form.linkElPrice} onChange={handleChange('linkElPrice')} /></div>
<div className="form-group"><label>Таймер</label><input type="text" className="form-control" value={form.timer} onChange={handleChange('timer')} /></div>
<div className="form-group"><label>Фон таймера</label><input type="text" className="form-control" value={form.timerBg} onChange={handleChange('timerBg')} /></div>
<div className="form-group"><label>formOrder (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.formOrder} onChange={handleChange('formOrder')} /></div>
<div className="form-group"><label>linkServices (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.linkServices} onChange={handleChange('linkServices')} /></div>
<div className="form-group"><label>linkStaff (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.linkStaff} onChange={handleChange('linkStaff')} /></div>
<div className="form-group"><label>photos (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.photos} onChange={handleChange('photos')} /></div>
<Modal isOpen={isModalSuccess} title="Изменения внесены" hasButtons={false}><p className="mb-1">Изменения успешно внесены.</p></Modal>
</EditElementForm>
);
}
-103
View File
@@ -1,103 +0,0 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { useCreateSitePromoMutation } from '/src/api/apiSitePromo';
import { selectRegions } from '../store/slice/regionSlice';
import { TextEditor } from '../components/Editors/TextEditor';
import { LoadingComponent } from '../components/Placeholders/LoadingComponent';
import { ErrorComponent } from '../components/Placeholders/ErrorComponent';
import { NotFindElement } from '../components/Placeholders/NotFindElement';
import { EditElementForm } from '../components/Forms/EditElementForm';
import Modal from '../components/Modals/Modal';
export function AddSitePromoPage() {
const navigate = useNavigate();
const navigateBack = () => navigate(`/site-promo`);
const regions = useSelector(selectRegions);
const [createSitePromo] = useCreateSitePromoMutation();
const [isModalSuccess, setModalSuccess] = useState(false);
const [errors, setErrors] = useState({ name: '', alias: '', regionId: '' });
const [form, setForm] = useState({
name: '',
active: false,
regionId: '',
alias: '',
shortName: '',
period: '',
timer: '',
timerBg: '',
clinics: '',
linkServices: '',
linkStaff: '',
photos: '',
});
const [anons, setAnons] = useState('');
const [content, setContent] = useState('');
const handleChange = (key) => (e) => {
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
setForm((f) => ({ ...f, [key]: value }));
if (key === 'name' || key === 'alias' || key === 'regionId') setErrors((err) => ({ ...err, [key]: '' }));
};
const handleSave = async () => {
const newErrors = { name: '', alias: '', regionId: '' };
let hasError = false;
if (!String(form.name ?? '').trim()) { newErrors.name = 'Название не может быть пустым'; hasError = true; }
if (!String(form.alias ?? '').trim()) { newErrors.alias = 'Alias не может быть пустым'; hasError = true; }
if (form.regionId === '' || form.regionId == null) { newErrors.regionId = 'Укажите регион'; hasError = true; }
if (hasError) { setErrors(newErrors); window.alert('Пожалуйста исправьте ошибки в форме.'); return; }
const data = { anons, content };
try {
data.name = form.name === '' ? null : form.name;
data.active = Boolean(form.active);
data.regionId = form.regionId === '' ? null : Number(form.regionId);
data.alias = form.alias === '' ? null : form.alias;
data.shortName = form.shortName === '' ? null : form.shortName;
data.period = form.period === '' ? null : form.period;
data.timer = form.timer === '' ? null : form.timer;
data.timerBg = form.timerBg === '' ? null : form.timerBg;
data.clinics = !form.clinics || !String(form.clinics).trim() ? null : JSON.parse(form.clinics);
data.linkServices = !form.linkServices || !String(form.linkServices).trim() ? null : JSON.parse(form.linkServices);
data.linkStaff = !form.linkStaff || !String(form.linkStaff).trim() ? null : JSON.parse(form.linkStaff);
data.photos = !form.photos || !String(form.photos).trim() ? null : JSON.parse(form.photos);
} catch (e) { window.alert('Пожалуйста исправьте ошибки в форме.'); return; }
try {
const response = await createSitePromo({ data }).unwrap();
setModalSuccess(true);
window.setTimeout(() => { navigate(`/site-promo/edit/${response.id}`); window.location.reload(); }, 2000);
} catch (err) {
console.error('Ошибка сохранения:', err);
}
};
return (
<EditElementForm navigateBack={navigateBack} header={`Добавление: промо`} handleSave={handleSave} handleDelete={() => {}} isAddSpecialist={true}>
<div className="form-group"><label>Название</label><input type="text" className="form-control" value={form.name} onChange={handleChange('name')} />
{errors.name && <small className="text-danger">{errors.name}</small>}</div>
<div className="form-group form-check"><input type="checkbox" className="form-check-input" id="active" checked={Boolean(form.active)} onChange={handleChange('active')} /><label className="form-check-label" htmlFor="active">Активно</label></div>
<div className="form-group"><label>Регион</label><select className="form-control" value={form.regionId === '' || form.regionId == null ? '' : String(form.regionId)} onChange={handleChange('regionId')}><option value=""></option>{Object.entries(regions).map(([rid, name]) => (<option key={rid} value={rid}>{name}</option>))}</select>
{errors.regionId && <small className="text-danger">{errors.regionId}</small>}</div>
<div className="form-group"><label>Alias</label><input type="text" className="form-control" value={form.alias} onChange={handleChange('alias')} />
{errors.alias && <small className="text-danger">{errors.alias}</small>}</div>
<div className="form-group"><label>Анонс</label><TextEditor content={anons} setContent={setAnons} /></div>
<div className="form-group"><label>Контент</label><TextEditor content={content} setContent={setContent} /></div>
<div className="form-group"><label>Короткое название</label><input type="text" className="form-control" value={form.shortName} onChange={handleChange('shortName')} /></div>
<div className="form-group"><label>Период</label><input type="text" className="form-control" value={form.period} onChange={handleChange('period')} /></div>
<div className="form-group"><label>Таймер</label><input type="text" className="form-control" value={form.timer} onChange={handleChange('timer')} /></div>
<div className="form-group"><label>Фон таймера</label><input type="text" className="form-control" value={form.timerBg} onChange={handleChange('timerBg')} /></div>
<div className="form-group"><label>clinics (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.clinics} onChange={handleChange('clinics')} /></div>
<div className="form-group"><label>linkServices (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.linkServices} onChange={handleChange('linkServices')} /></div>
<div className="form-group"><label>linkStaff (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.linkStaff} onChange={handleChange('linkStaff')} /></div>
<div className="form-group"><label>photos (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.photos} onChange={handleChange('photos')} /></div>
<Modal isOpen={isModalSuccess} title="Изменения внесены" hasButtons={false}><p className="mb-1">Изменения успешно внесены.</p></Modal>
</EditElementForm>
);
}
-226
View File
@@ -1,226 +0,0 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { useCreateSiteServicesMutation } from '/src/api/apiSiteServices';
import { selectRegions } from '../store/slice/regionSlice';
import { TextEditor } from '../components/Editors/TextEditor';
import { LoadingComponent } from '../components/Placeholders/LoadingComponent';
import { ErrorComponent } from '../components/Placeholders/ErrorComponent';
import { NotFindElement } from '../components/Placeholders/NotFindElement';
import { EditElementForm } from '../components/Forms/EditElementForm';
import Modal from '../components/Modals/Modal';
export function AddSiteServicesPage() {
const navigate = useNavigate();
const navigateBack = () => navigate(`/site-services`);
const regions = useSelector(selectRegions);
const [createSiteServices] = useCreateSiteServicesMutation();
const [isModalSuccess, setModalSuccess] = useState(false);
const [errors, setErrors] = useState({ name: '', alias: '', regionId: '' });
const [form, setForm] = useState({
name: '',
active: false,
regionId: '',
alias: '',
previewImg: '',
partPrice: '',
pokazaniya: '',
preparation: '',
protivopokazaniya: '',
bannerImg: '',
bannerImgM: '',
bannerImgUrl: '',
downloadFile: '',
fullWidthBanner: '',
kodUslug: '',
linkPrice: '',
photosTitle: '',
contraindicationsList: '',
customBlockText: '',
customBlockText2: '',
customBlockTitle: '',
customBlockTitle2: '',
indicationsList: '',
plusList: '',
plusText: '',
plusTitle: '',
prepareTitle: '',
processText: '',
processTitle: '',
servicesList: '',
servicesTitle: '',
textUp: '',
trainingText: '',
whyText: '',
whyTitle: '',
hidePicture: '',
linkVideoreviews: '',
faq: '',
hideSignBtn: '',
quiz: '',
tags: '',
tagsImportant: '',
clinics: '',
staffUp: '',
advantages: '',
saleId: '',
sortStaff: '',
linkArticlesServices: '',
servicesPhotos: '',
linkFaq: '',
linkServices: '',
linkStaff: '',
photos: '',
});
const [anons, setAnons] = useState('');
const [content, setContent] = useState('');
const handleChange = (key) => (e) => {
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
setForm((f) => ({ ...f, [key]: value }));
if (key === 'name' || key === 'alias' || key === 'regionId') setErrors((err) => ({ ...err, [key]: '' }));
};
const handleSave = async () => {
const newErrors = { name: '', alias: '', regionId: '' };
let hasError = false;
if (!String(form.name ?? '').trim()) { newErrors.name = 'Название не может быть пустым'; hasError = true; }
if (!String(form.alias ?? '').trim()) { newErrors.alias = 'Alias не может быть пустым'; hasError = true; }
if (form.regionId === '' || form.regionId == null) { newErrors.regionId = 'Укажите регион'; hasError = true; }
if (hasError) { setErrors(newErrors); window.alert('Пожалуйста исправьте ошибки в форме.'); return; }
const data = { anons, content };
try {
data.name = form.name === '' ? null : form.name;
data.active = Boolean(form.active);
data.regionId = form.regionId === '' ? null : Number(form.regionId);
data.alias = form.alias === '' ? null : form.alias;
data.previewImg = form.previewImg === '' ? null : form.previewImg;
data.partPrice = form.partPrice === '' ? null : form.partPrice;
data.pokazaniya = form.pokazaniya === '' ? null : form.pokazaniya;
data.preparation = form.preparation === '' ? null : form.preparation;
data.protivopokazaniya = form.protivopokazaniya === '' ? null : form.protivopokazaniya;
data.bannerImg = form.bannerImg === '' ? null : form.bannerImg;
data.bannerImgM = form.bannerImgM === '' ? null : form.bannerImgM;
data.bannerImgUrl = form.bannerImgUrl === '' ? null : form.bannerImgUrl;
data.downloadFile = form.downloadFile === '' ? null : form.downloadFile;
data.fullWidthBanner = form.fullWidthBanner === '' ? null : form.fullWidthBanner;
data.kodUslug = form.kodUslug === '' ? null : form.kodUslug;
data.linkPrice = form.linkPrice === '' ? null : form.linkPrice;
data.photosTitle = form.photosTitle === '' ? null : form.photosTitle;
data.contraindicationsList = form.contraindicationsList === '' ? null : form.contraindicationsList;
data.customBlockText = form.customBlockText === '' ? null : form.customBlockText;
data.customBlockText2 = form.customBlockText2 === '' ? null : form.customBlockText2;
data.customBlockTitle = form.customBlockTitle === '' ? null : form.customBlockTitle;
data.customBlockTitle2 = form.customBlockTitle2 === '' ? null : form.customBlockTitle2;
data.indicationsList = form.indicationsList === '' ? null : form.indicationsList;
data.plusList = form.plusList === '' ? null : form.plusList;
data.plusText = form.plusText === '' ? null : form.plusText;
data.plusTitle = form.plusTitle === '' ? null : form.plusTitle;
data.prepareTitle = form.prepareTitle === '' ? null : form.prepareTitle;
data.processText = form.processText === '' ? null : form.processText;
data.processTitle = form.processTitle === '' ? null : form.processTitle;
data.servicesList = form.servicesList === '' ? null : form.servicesList;
data.servicesTitle = form.servicesTitle === '' ? null : form.servicesTitle;
data.textUp = form.textUp === '' ? null : form.textUp;
data.trainingText = form.trainingText === '' ? null : form.trainingText;
data.whyText = form.whyText === '' ? null : form.whyText;
data.whyTitle = form.whyTitle === '' ? null : form.whyTitle;
data.hidePicture = form.hidePicture === '' ? null : Number(form.hidePicture);
data.linkVideoreviews = !form.linkVideoreviews || !String(form.linkVideoreviews).trim() ? null : JSON.parse(form.linkVideoreviews);
data.faq = !form.faq || !String(form.faq).trim() ? null : JSON.parse(form.faq);
data.hideSignBtn = !form.hideSignBtn || !String(form.hideSignBtn).trim() ? null : JSON.parse(form.hideSignBtn);
data.quiz = !form.quiz || !String(form.quiz).trim() ? null : JSON.parse(form.quiz);
data.tags = !form.tags || !String(form.tags).trim() ? null : JSON.parse(form.tags);
data.tagsImportant = !form.tagsImportant || !String(form.tagsImportant).trim() ? null : JSON.parse(form.tagsImportant);
data.clinics = !form.clinics || !String(form.clinics).trim() ? null : JSON.parse(form.clinics);
data.staffUp = !form.staffUp || !String(form.staffUp).trim() ? null : JSON.parse(form.staffUp);
data.advantages = !form.advantages || !String(form.advantages).trim() ? null : JSON.parse(form.advantages);
data.saleId = !form.saleId || !String(form.saleId).trim() ? null : JSON.parse(form.saleId);
data.sortStaff = !form.sortStaff || !String(form.sortStaff).trim() ? null : JSON.parse(form.sortStaff);
data.linkArticlesServices = !form.linkArticlesServices || !String(form.linkArticlesServices).trim() ? null : JSON.parse(form.linkArticlesServices);
data.servicesPhotos = !form.servicesPhotos || !String(form.servicesPhotos).trim() ? null : JSON.parse(form.servicesPhotos);
data.linkFaq = !form.linkFaq || !String(form.linkFaq).trim() ? null : JSON.parse(form.linkFaq);
data.linkServices = !form.linkServices || !String(form.linkServices).trim() ? null : JSON.parse(form.linkServices);
data.linkStaff = !form.linkStaff || !String(form.linkStaff).trim() ? null : JSON.parse(form.linkStaff);
data.photos = !form.photos || !String(form.photos).trim() ? null : JSON.parse(form.photos);
} catch (e) { window.alert('Пожалуйста исправьте ошибки в форме.'); return; }
try {
const response = await createSiteServices({ data }).unwrap();
setModalSuccess(true);
window.setTimeout(() => { navigate(`/site-services/edit/${response.id}`); window.location.reload(); }, 2000);
} catch (err) {
console.error('Ошибка сохранения:', err);
}
};
return (
<EditElementForm navigateBack={navigateBack} header={`Добавление: услугу`} handleSave={handleSave} handleDelete={() => {}} isAddSpecialist={true}>
<div className="form-group"><label>Название</label><input type="text" className="form-control" value={form.name} onChange={handleChange('name')} />
{errors.name && <small className="text-danger">{errors.name}</small>}</div>
<div className="form-group form-check"><input type="checkbox" className="form-check-input" id="active" checked={Boolean(form.active)} onChange={handleChange('active')} /><label className="form-check-label" htmlFor="active">Активно</label></div>
<div className="form-group"><label>Регион</label><select className="form-control" value={form.regionId === '' || form.regionId == null ? '' : String(form.regionId)} onChange={handleChange('regionId')}><option value=""></option>{Object.entries(regions).map(([rid, name]) => (<option key={rid} value={rid}>{name}</option>))}</select>
{errors.regionId && <small className="text-danger">{errors.regionId}</small>}</div>
<div className="form-group"><label>Alias</label><input type="text" className="form-control" value={form.alias} onChange={handleChange('alias')} />
{errors.alias && <small className="text-danger">{errors.alias}</small>}</div>
<div className="form-group"><label>Анонс</label><TextEditor content={anons} setContent={setAnons} /></div>
<div className="form-group"><label>Контент</label><TextEditor content={content} setContent={setContent} /></div>
<div className="form-group"><label>previewImg</label><input type="text" className="form-control" value={form.previewImg} onChange={handleChange('previewImg')} /></div>
<div className="form-group"><label>partPrice</label><input type="text" className="form-control" value={form.partPrice} onChange={handleChange('partPrice')} /></div>
<div className="form-group"><label>pokazaniya</label><input type="text" className="form-control" value={form.pokazaniya} onChange={handleChange('pokazaniya')} /></div>
<div className="form-group"><label>preparation</label><input type="text" className="form-control" value={form.preparation} onChange={handleChange('preparation')} /></div>
<div className="form-group"><label>protivopokazaniya</label><input type="text" className="form-control" value={form.protivopokazaniya} onChange={handleChange('protivopokazaniya')} /></div>
<div className="form-group"><label>bannerImg</label><input type="text" className="form-control" value={form.bannerImg} onChange={handleChange('bannerImg')} /></div>
<div className="form-group"><label>bannerImgM</label><input type="text" className="form-control" value={form.bannerImgM} onChange={handleChange('bannerImgM')} /></div>
<div className="form-group"><label>bannerImgUrl</label><input type="text" className="form-control" value={form.bannerImgUrl} onChange={handleChange('bannerImgUrl')} /></div>
<div className="form-group"><label>downloadFile</label><input type="text" className="form-control" value={form.downloadFile} onChange={handleChange('downloadFile')} /></div>
<div className="form-group"><label>fullWidthBanner</label><input type="text" className="form-control" value={form.fullWidthBanner} onChange={handleChange('fullWidthBanner')} /></div>
<div className="form-group"><label>kodUslug</label><input type="text" className="form-control" value={form.kodUslug} onChange={handleChange('kodUslug')} /></div>
<div className="form-group"><label>linkPrice</label><input type="text" className="form-control" value={form.linkPrice} onChange={handleChange('linkPrice')} /></div>
<div className="form-group"><label>photosTitle</label><input type="text" className="form-control" value={form.photosTitle} onChange={handleChange('photosTitle')} /></div>
<div className="form-group"><label>contraindicationsList</label><input type="text" className="form-control" value={form.contraindicationsList} onChange={handleChange('contraindicationsList')} /></div>
<div className="form-group"><label>customBlockText</label><input type="text" className="form-control" value={form.customBlockText} onChange={handleChange('customBlockText')} /></div>
<div className="form-group"><label>customBlockText2</label><input type="text" className="form-control" value={form.customBlockText2} onChange={handleChange('customBlockText2')} /></div>
<div className="form-group"><label>customBlockTitle</label><input type="text" className="form-control" value={form.customBlockTitle} onChange={handleChange('customBlockTitle')} /></div>
<div className="form-group"><label>customBlockTitle2</label><input type="text" className="form-control" value={form.customBlockTitle2} onChange={handleChange('customBlockTitle2')} /></div>
<div className="form-group"><label>indicationsList</label><input type="text" className="form-control" value={form.indicationsList} onChange={handleChange('indicationsList')} /></div>
<div className="form-group"><label>plusList</label><input type="text" className="form-control" value={form.plusList} onChange={handleChange('plusList')} /></div>
<div className="form-group"><label>plusText</label><input type="text" className="form-control" value={form.plusText} onChange={handleChange('plusText')} /></div>
<div className="form-group"><label>plusTitle</label><input type="text" className="form-control" value={form.plusTitle} onChange={handleChange('plusTitle')} /></div>
<div className="form-group"><label>prepareTitle</label><input type="text" className="form-control" value={form.prepareTitle} onChange={handleChange('prepareTitle')} /></div>
<div className="form-group"><label>processText</label><input type="text" className="form-control" value={form.processText} onChange={handleChange('processText')} /></div>
<div className="form-group"><label>processTitle</label><input type="text" className="form-control" value={form.processTitle} onChange={handleChange('processTitle')} /></div>
<div className="form-group"><label>servicesList</label><input type="text" className="form-control" value={form.servicesList} onChange={handleChange('servicesList')} /></div>
<div className="form-group"><label>servicesTitle</label><input type="text" className="form-control" value={form.servicesTitle} onChange={handleChange('servicesTitle')} /></div>
<div className="form-group"><label>textUp</label><input type="text" className="form-control" value={form.textUp} onChange={handleChange('textUp')} /></div>
<div className="form-group"><label>trainingText</label><input type="text" className="form-control" value={form.trainingText} onChange={handleChange('trainingText')} /></div>
<div className="form-group"><label>whyText</label><input type="text" className="form-control" value={form.whyText} onChange={handleChange('whyText')} /></div>
<div className="form-group"><label>whyTitle</label><input type="text" className="form-control" value={form.whyTitle} onChange={handleChange('whyTitle')} /></div>
<div className="form-group"><label>hidePicture</label><input type="number" className="form-control" value={form.hidePicture} onChange={handleChange('hidePicture')} /></div>
<div className="form-group"><label>linkVideoreviews (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.linkVideoreviews} onChange={handleChange('linkVideoreviews')} /></div>
<div className="form-group"><label>faq (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.faq} onChange={handleChange('faq')} /></div>
<div className="form-group"><label>hideSignBtn (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.hideSignBtn} onChange={handleChange('hideSignBtn')} /></div>
<div className="form-group"><label>quiz (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.quiz} onChange={handleChange('quiz')} /></div>
<div className="form-group"><label>tags (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.tags} onChange={handleChange('tags')} /></div>
<div className="form-group"><label>tagsImportant (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.tagsImportant} onChange={handleChange('tagsImportant')} /></div>
<div className="form-group"><label>clinics (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.clinics} onChange={handleChange('clinics')} /></div>
<div className="form-group"><label>staffUp (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.staffUp} onChange={handleChange('staffUp')} /></div>
<div className="form-group"><label>advantages (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.advantages} onChange={handleChange('advantages')} /></div>
<div className="form-group"><label>saleId (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.saleId} onChange={handleChange('saleId')} /></div>
<div className="form-group"><label>sortStaff (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.sortStaff} onChange={handleChange('sortStaff')} /></div>
<div className="form-group"><label>linkArticlesServices (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.linkArticlesServices} onChange={handleChange('linkArticlesServices')} /></div>
<div className="form-group"><label>servicesPhotos (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.servicesPhotos} onChange={handleChange('servicesPhotos')} /></div>
<div className="form-group"><label>linkFaq (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.linkFaq} onChange={handleChange('linkFaq')} /></div>
<div className="form-group"><label>linkServices (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.linkServices} onChange={handleChange('linkServices')} /></div>
<div className="form-group"><label>linkStaff (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.linkStaff} onChange={handleChange('linkStaff')} /></div>
<div className="form-group"><label>photos (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.photos} onChange={handleChange('photos')} /></div>
<Modal isOpen={isModalSuccess} title="Изменения внесены" hasButtons={false}><p className="mb-1">Изменения успешно внесены.</p></Modal>
</EditElementForm>
);
}
-124
View File
@@ -1,124 +0,0 @@
import { useState, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { useGetArticleListQuery } from '/src/api/apiArticle';
import { selectRegions } from '../store/slice/regionSlice';
import { useOutsideClick } from '../hooks/useOutsideClick';
import { LoadingComponent } from '../components/Placeholders/LoadingComponent';
import { ErrorComponent } from '../components/Placeholders/ErrorComponent';
export const ArticleListPage = () => {
const [ searchValue, setSearchValue ] = useState( '' );
const [ currentPage, setCurrentPage ] = useState( 1 );
const [ expandedId, setExpandedId ] = useState( '' );
const navigate = useNavigate();
const regions = useSelector(selectRegions);
const tableRef = useRef( null );
useOutsideClick( tableRef, () => setExpandedId( null ));
const { data: response = {}, isFetching, error: queryError } =
useGetArticleListQuery( { search: searchValue, page: currentPage } );
const pagination = response.pagination
? response.pagination
: response.meta
? {
total_pages: response.meta.totalPages ?? 1,
current_page: response.meta.page ?? 1,
has_previous_page: (response.meta.page ?? 1) > 1,
has_next_page: (response.meta.page ?? 1) < (response.meta.totalPages ?? 1),
}
: {};
const items = response.data ? response.data : [];
const renderPagination = () => {
const total = pagination.total_pages || 1;
const current = pagination.current_page || 1;
const pages = new Set( [ 1, total ] );
for ( let page = current - 2; page <= current + 2; page += 1 ) {
if ( page > 1 && page < total ) pages.add( page );
}
const sorted = Array.from( pages ).sort( ( a, b ) => a - b );
const elements = [];
let last = 0;
sorted.forEach( page => {
if ( last && page - last > 1 ) {
elements.push(
<li key={ `dots-${last}` } className="page-item disabled">
<span className="page-link"></span>
</li>
);
}
elements.push(
<li key={ page } className={`page-item ${ page === current ? 'active' : '' }`}>
<button type="button" className="page-link" onClick={ () => page !== current && setCurrentPage( page ) }>
{ page }
</button>
</li>
);
last = page;
});
return (
<nav>
<ul className="pagination justify-content-center">
<li className={`page-item ${ !pagination.has_previous_page ? 'disabled' : '' }`}>
<button type="button" className="page-link" onClick={ () => pagination.has_previous_page && setCurrentPage(current - 1) }>&laquo;</button>
</li>
{ elements }
<li className={`page-item ${ !pagination.has_next_page ? 'disabled' : '' }`}>
<button type="button" className="page-link" onClick={ () => pagination.has_next_page && setCurrentPage(current + 1) }>&raquo;</button>
</li>
</ul>
</nav>
);
};
return (
<div className="container-fluid">
<h1 className="h3 mb-4 text-gray-800">Статьи</h1>
<div className="d-flex justify-content-between mb-3" style={{ marginRight: '0.1rem', marginLeft: '0.1rem' }}>
<div className="form-group align-self-end mr-3">
<input type="button" className="btn btn-outline-primary" value="Добавить" onClick={ e => { e.stopPropagation(); navigate(`/article/create`); } } />
</div>
<div className="form-group flex-grow-1">
<label>Поиск</label>
<input type="text" className="form-control" value={ searchValue } onChange={ e => { setSearchValue( e.target.value ); setCurrentPage( 1 ); } } />
</div>
</div>
{ isFetching ? <LoadingComponent /> : queryError ? <ErrorComponent /> : (
<>
<div className="table-responsive" ref={tableRef}>
<table className="table table-hover table-bordered">
<thead><tr>
<th>ID</th>
<th>Название</th>
<th>Alias</th>
<th>Активно</th>
</tr></thead>
<tbody>
{items.map( item => (
<>
<tr key={ item.id } className={ `cursor-pointer${ expandedId === item.id ? ' table-success' : '' }` } onClick={ () => setExpandedId( expandedId === item.id ? null : item.id ) }>
<td>{ item.id }</td>
<td>{ item.name }</td>
<td>{ item.alias }</td>
<td>{ item.active ? 'Да' : 'Нет' }</td>
</tr>
{ expandedId === item.id && (
<tr className='table-success'>
<td colSpan={ 4 }>
<input type="button" className="btn btn-outline-primary" value="Редактировать" onClick={ e => { e.stopPropagation(); navigate(`/article/edit/${item.id}`) } } />
</td>
</tr>
)}
</>
))}
</tbody>
</table>
</div>
{ renderPagination() }
</>
)}
</div>
);
};
-115
View File
@@ -1,115 +0,0 @@
import { useState, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { useGetDiseaseListQuery } from '/src/api/apiDisease';
import { selectRegions } from '../store/slice/regionSlice';
import { useOutsideClick } from '../hooks/useOutsideClick';
import { LoadingComponent } from '../components/Placeholders/LoadingComponent';
import { ErrorComponent } from '../components/Placeholders/ErrorComponent';
export const DiseaseListPage = () => {
const [ searchValue, setSearchValue ] = useState( '' );
const [ currentPage, setCurrentPage ] = useState( 1 );
const [ expandedId, setExpandedId ] = useState( '' );
const navigate = useNavigate();
const regions = useSelector(selectRegions);
const tableRef = useRef( null );
useOutsideClick( tableRef, () => setExpandedId( null ));
const { data: response = {}, isFetching, error: queryError } =
useGetDiseaseListQuery( { search: searchValue, page: currentPage } );
const pagination = response.pagination || {};
const items = response.data ? response.data : [];
const renderPagination = () => {
const total = pagination.total_pages || 1;
const current = pagination.current_page || 1;
const pages = new Set( [ 1, total ] );
for ( let page = current - 2; page <= current + 2; page += 1 ) {
if ( page > 1 && page < total ) pages.add( page );
}
const sorted = Array.from( pages ).sort( ( a, b ) => a - b );
const elements = [];
let last = 0;
sorted.forEach( page => {
if ( last && page - last > 1 ) {
elements.push(
<li key={ `dots-${last}` } className="page-item disabled">
<span className="page-link"></span>
</li>
);
}
elements.push(
<li key={ page } className={`page-item ${ page === current ? 'active' : '' }`}>
<button type="button" className="page-link" onClick={ () => page !== current && setCurrentPage( page ) }>
{ page }
</button>
</li>
);
last = page;
});
return (
<nav>
<ul className="pagination justify-content-center">
<li className={`page-item ${ !pagination.has_previous_page ? 'disabled' : '' }`}>
<button type="button" className="page-link" onClick={ () => pagination.has_previous_page && setCurrentPage(current - 1) }>&laquo;</button>
</li>
{ elements }
<li className={`page-item ${ !pagination.has_next_page ? 'disabled' : '' }`}>
<button type="button" className="page-link" onClick={ () => pagination.has_next_page && setCurrentPage(current + 1) }>&raquo;</button>
</li>
</ul>
</nav>
);
};
return (
<div className="container-fluid">
<h1 className="h3 mb-4 text-gray-800">Заболевания</h1>
<div className="d-flex justify-content-between mb-3" style={{ marginRight: '0.1rem', marginLeft: '0.1rem' }}>
<div className="form-group align-self-end mr-3">
<input type="button" className="btn btn-outline-primary" value="Добавить" onClick={ e => { e.stopPropagation(); navigate(`/disease/create`); } } />
</div>
<div className="form-group flex-grow-1">
<label>Поиск</label>
<input type="text" className="form-control" value={ searchValue } onChange={ e => { setSearchValue( e.target.value ); setCurrentPage( 1 ); } } />
</div>
</div>
{ isFetching ? <LoadingComponent /> : queryError ? <ErrorComponent /> : (
<>
<div className="table-responsive" ref={tableRef}>
<table className="table table-hover table-bordered">
<thead><tr>
<th>ID</th>
<th>Название</th>
<th>Alias</th>
<th>Активно</th>
</tr></thead>
<tbody>
{items.map( item => (
<>
<tr key={ item.id } className={ `cursor-pointer${ expandedId === item.id ? ' table-success' : '' }` } onClick={ () => setExpandedId( expandedId === item.id ? null : item.id ) }>
<td>{ item.id }</td>
<td>{ item.name }</td>
<td>{ item.alias }</td>
<td>{ item.active ? 'Да' : 'Нет' }</td>
</tr>
{ expandedId === item.id && (
<tr className='table-success'>
<td colSpan={ 4 }>
<input type="button" className="btn btn-outline-primary" value="Редактировать" onClick={ e => { e.stopPropagation(); navigate(`/disease/edit/${item.id}`) } } />
</td>
</tr>
)}
</>
))}
</tbody>
</table>
</div>
{ renderPagination() }
</>
)}
</div>
);
};
-113
View File
@@ -1,113 +0,0 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { useGetArticleQuery, useUpdateArticleMutation, useDeleteArticleMutation } from '/src/api/apiArticle';
import { selectRegions } from '../store/slice/regionSlice';
import { TextEditor } from '../components/Editors/TextEditor';
import { LoadingComponent } from '../components/Placeholders/LoadingComponent';
import { ErrorComponent } from '../components/Placeholders/ErrorComponent';
import { NotFindElement } from '../components/Placeholders/NotFindElement';
import { EditElementForm } from '../components/Forms/EditElementForm';
import Modal from '../components/Modals/Modal';
export function EditArticlePage() {
const { id } = useParams();
const navigate = useNavigate();
const navigateBack = () => navigate(`/article`);
const regions = useSelector(selectRegions);
const { data: item, isFetching, error } = useGetArticleQuery({ articleId: id });
const [updateArticle] = useUpdateArticleMutation();
const [deleteArticle] = useDeleteArticleMutation();
const [isModalSuccess, setModalSuccess] = useState(false);
const [errors, setErrors] = useState({ name: '', alias: '', regionId: '' });
const [form, setForm] = useState({
name: '',
active: false,
regionId: '',
alias: '',
previewPicture: '',
doctors: '',
services: '',
});
const [anons, setAnons] = useState('');
const [content, setContent] = useState('');
useEffect(() => {
if (!item) return;
setForm({
name: item.name ?? '',
active: Boolean(item.active),
regionId: item.regionId ?? '',
alias: item.alias ?? '',
previewPicture: item.previewPicture ?? '',
doctors: item.doctors == null ? '' : JSON.stringify(item.doctors, null, 2),
services: item.services == null ? '' : JSON.stringify(item.services, null, 2),
});
setAnons(item.anons ?? '');
setContent(item.content ?? '');
}, [item]);
const handleChange = (key) => (e) => {
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
setForm((f) => ({ ...f, [key]: value }));
if (key === 'name' || key === 'alias' || key === 'regionId') setErrors((err) => ({ ...err, [key]: '' }));
};
const handleDelete = async () => {
try {
await deleteArticle({ articleId: id }).unwrap();
setModalSuccess(true);
window.setTimeout(() => navigateBack(), 2000);
} catch (err) {
console.error('Ошибка при удалении:', err);
}
};
const handleSave = async () => {
const newErrors = { name: '', alias: '', regionId: '' };
let hasError = false;
if (!String(form.name ?? '').trim()) { newErrors.name = 'Название не может быть пустым'; hasError = true; }
if (!String(form.alias ?? '').trim()) { newErrors.alias = 'Alias не может быть пустым'; hasError = true; }
if (form.regionId === '' || form.regionId == null) { newErrors.regionId = 'Укажите регион'; hasError = true; }
if (hasError) { setErrors(newErrors); window.alert('Пожалуйста исправьте ошибки в форме.'); return; }
const data = { anons, content };
try {
data.name = form.name === '' ? null : form.name;
data.active = Boolean(form.active);
data.regionId = form.regionId === '' ? null : Number(form.regionId);
data.alias = form.alias === '' ? null : form.alias;
data.previewPicture = form.previewPicture === '' ? null : form.previewPicture;
data.doctors = !form.doctors || !String(form.doctors).trim() ? null : JSON.parse(form.doctors);
data.services = !form.services || !String(form.services).trim() ? null : JSON.parse(form.services);
} catch (e) { window.alert('Пожалуйста исправьте ошибки в форме.'); return; }
try {
await updateArticle({ articleId: id, data }).unwrap();
setModalSuccess(true);
window.setTimeout(() => window.location.reload(), 2000);
} catch (err) {
console.error('Ошибка сохранения:', err);
}
};
if (isFetching) return <LoadingComponent />;
if (error) return <ErrorComponent />;
if (!item) return <NotFindElement message={`Статья с ID=${id} не найдена.`} navigateBack={navigateBack} />;
return (
<EditElementForm navigateBack={navigateBack} header={`Редактирование: статью #${id}`} handleSave={handleSave} handleDelete={handleDelete}>
<div className="form-group"><label>Название</label><input type="text" className="form-control" value={form.name} onChange={handleChange('name')} />
{errors.name && <small className="text-danger">{errors.name}</small>}</div>
<div className="form-group form-check"><input type="checkbox" className="form-check-input" id="active" checked={Boolean(form.active)} onChange={handleChange('active')} /><label className="form-check-label" htmlFor="active">Активно</label></div>
<div className="form-group"><label>Регион</label><select className="form-control" value={form.regionId === '' || form.regionId == null ? '' : String(form.regionId)} onChange={handleChange('regionId')}><option value=""></option>{Object.entries(regions).map(([rid, name]) => (<option key={rid} value={rid}>{name}</option>))}</select>
{errors.regionId && <small className="text-danger">{errors.regionId}</small>}</div>
<div className="form-group"><label>Alias</label><input type="text" className="form-control" value={form.alias} onChange={handleChange('alias')} />
{errors.alias && <small className="text-danger">{errors.alias}</small>}</div>
<div className="form-group"><label>Анонс</label><TextEditor content={anons} setContent={setAnons} /></div>
<div className="form-group"><label>Контент</label><TextEditor content={content} setContent={setContent} /></div>
<div className="form-group"><label>previewPicture</label><input type="text" className="form-control" value={form.previewPicture} onChange={handleChange('previewPicture')} /></div>
<div className="form-group"><label>doctors (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.doctors} onChange={handleChange('doctors')} /></div>
<div className="form-group"><label>services (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.services} onChange={handleChange('services')} /></div>
<Modal isOpen={isModalSuccess} title="Изменения внесены" hasButtons={false}><p className="mb-1">Изменения успешно внесены.</p></Modal>
</EditElementForm>
);
}
-165
View File
@@ -1,165 +0,0 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { useGetDiseaseQuery, useUpdateDiseaseMutation, useDeleteDiseaseMutation } from '/src/api/apiDisease';
import { selectRegions } from '../store/slice/regionSlice';
import { TextEditor } from '../components/Editors/TextEditor';
import { LoadingComponent } from '../components/Placeholders/LoadingComponent';
import { ErrorComponent } from '../components/Placeholders/ErrorComponent';
import { NotFindElement } from '../components/Placeholders/NotFindElement';
import { EditElementForm } from '../components/Forms/EditElementForm';
import Modal from '../components/Modals/Modal';
export function EditDiseasePage() {
const { id } = useParams();
const navigate = useNavigate();
const navigateBack = () => navigate(`/disease`);
const regions = useSelector(selectRegions);
const { data: item, isFetching, error } = useGetDiseaseQuery({ diseaseId: id });
const [updateDisease] = useUpdateDiseaseMutation();
const [deleteDisease] = useDeleteDiseaseMutation();
const [isModalSuccess, setModalSuccess] = useState(false);
const [errors, setErrors] = useState({ name: '', alias: '', regionId: '' });
const [form, setForm] = useState({
name: '',
active: false,
regionId: '',
alias: '',
previewPicture: '',
hidePicture: false,
readTime: '',
diseasesName: '',
diseasesOtherName: '',
symptom: '',
staff: '',
bibliography: '',
tagsImportant: '',
tags: '',
linkServices: '',
staffList: '',
staffPost: '',
staffPostExclude: '',
linkFaq: '',
staffCheck: '',
});
const [anons, setAnons] = useState('');
const [content, setContent] = useState('');
useEffect(() => {
if (!item) return;
setForm({
name: item.name ?? '',
active: Boolean(item.active),
regionId: item.regionId ?? '',
alias: item.alias ?? '',
previewPicture: item.previewPicture ?? '',
hidePicture: Boolean(item.hidePicture),
readTime: item.readTime ?? '',
diseasesName: item.diseasesName ?? '',
diseasesOtherName: item.diseasesOtherName ?? '',
symptom: item.symptom ?? '',
staff: item.staff ?? '',
bibliography: item.bibliography ?? '',
tagsImportant: item.tagsImportant == null ? '' : JSON.stringify(item.tagsImportant, null, 2),
tags: item.tags == null ? '' : JSON.stringify(item.tags, null, 2),
linkServices: item.linkServices == null ? '' : JSON.stringify(item.linkServices, null, 2),
staffList: item.staffList == null ? '' : JSON.stringify(item.staffList, null, 2),
staffPost: item.staffPost == null ? '' : JSON.stringify(item.staffPost, null, 2),
staffPostExclude: item.staffPostExclude == null ? '' : JSON.stringify(item.staffPostExclude, null, 2),
linkFaq: item.linkFaq == null ? '' : JSON.stringify(item.linkFaq, null, 2),
staffCheck: item.staffCheck == null ? '' : JSON.stringify(item.staffCheck, null, 2),
});
setAnons(item.anons ?? '');
setContent(item.content ?? '');
}, [item]);
const handleChange = (key) => (e) => {
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
setForm((f) => ({ ...f, [key]: value }));
if (key === 'name' || key === 'alias' || key === 'regionId') setErrors((err) => ({ ...err, [key]: '' }));
};
const handleDelete = async () => {
try {
await deleteDisease({ diseaseId: id }).unwrap();
setModalSuccess(true);
window.setTimeout(() => navigateBack(), 2000);
} catch (err) {
console.error('Ошибка при удалении:', err);
}
};
const handleSave = async () => {
const newErrors = { name: '', alias: '', regionId: '' };
let hasError = false;
if (!String(form.name ?? '').trim()) { newErrors.name = 'Название не может быть пустым'; hasError = true; }
if (!String(form.alias ?? '').trim()) { newErrors.alias = 'Alias не может быть пустым'; hasError = true; }
if (form.regionId === '' || form.regionId == null) { newErrors.regionId = 'Укажите регион'; hasError = true; }
if (hasError) { setErrors(newErrors); window.alert('Пожалуйста исправьте ошибки в форме.'); return; }
const data = { anons, content };
try {
data.name = form.name === '' ? null : form.name;
data.active = Boolean(form.active);
data.regionId = form.regionId === '' ? null : Number(form.regionId);
data.alias = form.alias === '' ? null : form.alias;
data.previewPicture = form.previewPicture === '' ? null : form.previewPicture;
data.hidePicture = Boolean(form.hidePicture);
data.readTime = form.readTime === '' ? null : form.readTime;
data.diseasesName = form.diseasesName === '' ? null : form.diseasesName;
data.diseasesOtherName = form.diseasesOtherName === '' ? null : form.diseasesOtherName;
data.symptom = form.symptom === '' ? null : form.symptom;
data.staff = form.staff === '' ? null : form.staff;
data.bibliography = form.bibliography === '' ? null : form.bibliography;
data.tagsImportant = !form.tagsImportant || !String(form.tagsImportant).trim() ? null : JSON.parse(form.tagsImportant);
data.tags = !form.tags || !String(form.tags).trim() ? null : JSON.parse(form.tags);
data.linkServices = !form.linkServices || !String(form.linkServices).trim() ? null : JSON.parse(form.linkServices);
data.staffList = !form.staffList || !String(form.staffList).trim() ? null : JSON.parse(form.staffList);
data.staffPost = !form.staffPost || !String(form.staffPost).trim() ? null : JSON.parse(form.staffPost);
data.staffPostExclude = !form.staffPostExclude || !String(form.staffPostExclude).trim() ? null : JSON.parse(form.staffPostExclude);
data.linkFaq = !form.linkFaq || !String(form.linkFaq).trim() ? null : JSON.parse(form.linkFaq);
data.staffCheck = !form.staffCheck || !String(form.staffCheck).trim() ? null : JSON.parse(form.staffCheck);
} catch (e) { window.alert('Пожалуйста исправьте ошибки в форме.'); return; }
try {
await updateDisease({ diseaseId: id, data }).unwrap();
setModalSuccess(true);
window.setTimeout(() => window.location.reload(), 2000);
} catch (err) {
console.error('Ошибка сохранения:', err);
}
};
if (isFetching) return <LoadingComponent />;
if (error) return <ErrorComponent />;
if (!item) return <NotFindElement message={`Заболевание с ID=${id} не найдена.`} navigateBack={navigateBack} />;
return (
<EditElementForm navigateBack={navigateBack} header={`Редактирование: заболевание #${id}`} handleSave={handleSave} handleDelete={handleDelete}>
<div className="form-group"><label>Название</label><input type="text" className="form-control" value={form.name} onChange={handleChange('name')} />
{errors.name && <small className="text-danger">{errors.name}</small>}</div>
<div className="form-group form-check"><input type="checkbox" className="form-check-input" id="active" checked={Boolean(form.active)} onChange={handleChange('active')} /><label className="form-check-label" htmlFor="active">Активно</label></div>
<div className="form-group"><label>Регион</label><select className="form-control" value={form.regionId === '' || form.regionId == null ? '' : String(form.regionId)} onChange={handleChange('regionId')}><option value=""></option>{Object.entries(regions).map(([rid, name]) => (<option key={rid} value={rid}>{name}</option>))}</select>
{errors.regionId && <small className="text-danger">{errors.regionId}</small>}</div>
<div className="form-group"><label>Alias</label><input type="text" className="form-control" value={form.alias} onChange={handleChange('alias')} />
{errors.alias && <small className="text-danger">{errors.alias}</small>}</div>
<div className="form-group"><label>Анонс</label><TextEditor content={anons} setContent={setAnons} /></div>
<div className="form-group"><label>Контент</label><TextEditor content={content} setContent={setContent} /></div>
<div className="form-group"><label>previewPicture</label><input type="text" className="form-control" value={form.previewPicture} onChange={handleChange('previewPicture')} /></div>
<div className="form-group form-check"><input type="checkbox" className="form-check-input" id="hidePicture" checked={Boolean(form.hidePicture)} onChange={handleChange('hidePicture')} /><label className="form-check-label" htmlFor="hidePicture">hidePicture</label></div>
<div className="form-group"><label>readTime</label><input type="text" className="form-control" value={form.readTime} onChange={handleChange('readTime')} /></div>
<div className="form-group"><label>diseasesName</label><input type="text" className="form-control" value={form.diseasesName} onChange={handleChange('diseasesName')} /></div>
<div className="form-group"><label>diseasesOtherName</label><input type="text" className="form-control" value={form.diseasesOtherName} onChange={handleChange('diseasesOtherName')} /></div>
<div className="form-group"><label>symptom</label><input type="text" className="form-control" value={form.symptom} onChange={handleChange('symptom')} /></div>
<div className="form-group"><label>staff</label><input type="text" className="form-control" value={form.staff} onChange={handleChange('staff')} /></div>
<div className="form-group"><label>bibliography</label><input type="text" className="form-control" value={form.bibliography} onChange={handleChange('bibliography')} /></div>
<div className="form-group"><label>tagsImportant (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.tagsImportant} onChange={handleChange('tagsImportant')} /></div>
<div className="form-group"><label>tags (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.tags} onChange={handleChange('tags')} /></div>
<div className="form-group"><label>linkServices (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.linkServices} onChange={handleChange('linkServices')} /></div>
<div className="form-group"><label>staffList (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.staffList} onChange={handleChange('staffList')} /></div>
<div className="form-group"><label>staffPost (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.staffPost} onChange={handleChange('staffPost')} /></div>
<div className="form-group"><label>staffPostExclude (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.staffPostExclude} onChange={handleChange('staffPostExclude')} /></div>
<div className="form-group"><label>linkFaq (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.linkFaq} onChange={handleChange('linkFaq')} /></div>
<div className="form-group"><label>staffCheck (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.staffCheck} onChange={handleChange('staffCheck')} /></div>
<Modal isOpen={isModalSuccess} title="Изменения внесены" hasButtons={false}><p className="mb-1">Изменения успешно внесены.</p></Modal>
</EditElementForm>
);
}
+3 -1
View File
@@ -2,6 +2,8 @@ import { useEffect, useState, useRef } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { apiUrl } from '@/config/api';
import { useGetFilialsQuery, useUpdateFilialMutation, useUploadFilialPictureMutation } from '../api/apiFilial';
import { selectUtils } from '../store/slice/utilsSlice';
import { selectRegions } from '../store/slice/regionSlice';
@@ -69,7 +71,7 @@ export const EditFilialPage = () => {
email: filial.email,
fid: filial.fid,
origin: filial.origin,
picture: `https://api.sovamed.ru${filial.pictureLink}`,
picture: apiUrl(filial.pictureLink),
policy: filial.policy,
}
setForm({... filialData})
-193
View File
@@ -1,193 +0,0 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { useGetMedicalCenterQuery, useUpdateMedicalCenterMutation, useDeleteMedicalCenterMutation } from '/src/api/apiMedicalCenter';
import { selectRegions } from '../store/slice/regionSlice';
import { TextEditor } from '../components/Editors/TextEditor';
import { LoadingComponent } from '../components/Placeholders/LoadingComponent';
import { ErrorComponent } from '../components/Placeholders/ErrorComponent';
import { NotFindElement } from '../components/Placeholders/NotFindElement';
import { EditElementForm } from '../components/Forms/EditElementForm';
import Modal from '../components/Modals/Modal';
export function EditMedicalCenterPage() {
const { id } = useParams();
const navigate = useNavigate();
const navigateBack = () => navigate(`/medical-center`);
const regions = useSelector(selectRegions);
const { data: item, isFetching, error } = useGetMedicalCenterQuery({ medicalCenterId: id });
const [updateMedicalCenter] = useUpdateMedicalCenterMutation();
const [deleteMedicalCenter] = useDeleteMedicalCenterMutation();
const [isModalSuccess, setModalSuccess] = useState(false);
const [errors, setErrors] = useState({ name: '', alias: '', regionId: '' });
const [form, setForm] = useState({
name: '',
active: false,
regionId: '',
alias: '',
mainLinkStaff: '',
plusText: '',
plusTitle: '',
processText: '',
processTitle: '',
servicesTitle: '',
trainingText: '',
trainingTextTitle: '',
whyText: '',
whyTitle: '',
hidePicture: '',
kodUslug: '',
doctors: '',
services: '',
articles: '',
txtUp: '',
contraindications: '',
indications: '',
linkSale: '',
plusList: '',
servicesList: '',
servicesPhotos: '',
sortStaff: '',
});
const [anons, setAnons] = useState('');
const [content, setContent] = useState('');
useEffect(() => {
if (!item) return;
setForm({
name: item.name ?? '',
active: Boolean(item.active),
regionId: item.regionId ?? '',
alias: item.alias ?? '',
mainLinkStaff: item.mainLinkStaff ?? '',
plusText: item.plusText ?? '',
plusTitle: item.plusTitle ?? '',
processText: item.processText ?? '',
processTitle: item.processTitle ?? '',
servicesTitle: item.servicesTitle ?? '',
trainingText: item.trainingText ?? '',
trainingTextTitle: item.trainingTextTitle ?? '',
whyText: item.whyText ?? '',
whyTitle: item.whyTitle ?? '',
hidePicture: item.hidePicture ?? '',
kodUslug: item.kodUslug == null ? '' : JSON.stringify(item.kodUslug, null, 2),
doctors: item.doctors == null ? '' : JSON.stringify(item.doctors, null, 2),
services: item.services == null ? '' : JSON.stringify(item.services, null, 2),
articles: item.articles == null ? '' : JSON.stringify(item.articles, null, 2),
txtUp: item.txtUp == null ? '' : JSON.stringify(item.txtUp, null, 2),
contraindications: item.contraindications == null ? '' : JSON.stringify(item.contraindications, null, 2),
indications: item.indications == null ? '' : JSON.stringify(item.indications, null, 2),
linkSale: item.linkSale == null ? '' : JSON.stringify(item.linkSale, null, 2),
plusList: item.plusList == null ? '' : JSON.stringify(item.plusList, null, 2),
servicesList: item.servicesList == null ? '' : JSON.stringify(item.servicesList, null, 2),
servicesPhotos: item.servicesPhotos == null ? '' : JSON.stringify(item.servicesPhotos, null, 2),
sortStaff: item.sortStaff == null ? '' : JSON.stringify(item.sortStaff, null, 2),
});
setAnons(item.anons ?? '');
setContent(item.content ?? '');
}, [item]);
const handleChange = (key) => (e) => {
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
setForm((f) => ({ ...f, [key]: value }));
if (key === 'name' || key === 'alias' || key === 'regionId') setErrors((err) => ({ ...err, [key]: '' }));
};
const handleDelete = async () => {
try {
await deleteMedicalCenter({ medicalCenterId: id }).unwrap();
setModalSuccess(true);
window.setTimeout(() => navigateBack(), 2000);
} catch (err) {
console.error('Ошибка при удалении:', err);
}
};
const handleSave = async () => {
const newErrors = { name: '', alias: '', regionId: '' };
let hasError = false;
if (!String(form.name ?? '').trim()) { newErrors.name = 'Название не может быть пустым'; hasError = true; }
if (!String(form.alias ?? '').trim()) { newErrors.alias = 'Alias не может быть пустым'; hasError = true; }
if (form.regionId === '' || form.regionId == null) { newErrors.regionId = 'Укажите регион'; hasError = true; }
if (hasError) { setErrors(newErrors); window.alert('Пожалуйста исправьте ошибки в форме.'); return; }
const data = { anons, content };
try {
data.name = form.name === '' ? null : form.name;
data.active = Boolean(form.active);
data.regionId = form.regionId === '' ? null : Number(form.regionId);
data.alias = form.alias === '' ? null : form.alias;
data.mainLinkStaff = form.mainLinkStaff === '' ? null : form.mainLinkStaff;
data.plusText = form.plusText === '' ? null : form.plusText;
data.plusTitle = form.plusTitle === '' ? null : form.plusTitle;
data.processText = form.processText === '' ? null : form.processText;
data.processTitle = form.processTitle === '' ? null : form.processTitle;
data.servicesTitle = form.servicesTitle === '' ? null : form.servicesTitle;
data.trainingText = form.trainingText === '' ? null : form.trainingText;
data.trainingTextTitle = form.trainingTextTitle === '' ? null : form.trainingTextTitle;
data.whyText = form.whyText === '' ? null : form.whyText;
data.whyTitle = form.whyTitle === '' ? null : form.whyTitle;
data.hidePicture = form.hidePicture === '' ? null : Number(form.hidePicture);
data.kodUslug = !form.kodUslug || !String(form.kodUslug).trim() ? null : JSON.parse(form.kodUslug);
data.doctors = !form.doctors || !String(form.doctors).trim() ? null : JSON.parse(form.doctors);
data.services = !form.services || !String(form.services).trim() ? null : JSON.parse(form.services);
data.articles = !form.articles || !String(form.articles).trim() ? null : JSON.parse(form.articles);
data.txtUp = !form.txtUp || !String(form.txtUp).trim() ? null : JSON.parse(form.txtUp);
data.contraindications = !form.contraindications || !String(form.contraindications).trim() ? null : JSON.parse(form.contraindications);
data.indications = !form.indications || !String(form.indications).trim() ? null : JSON.parse(form.indications);
data.linkSale = !form.linkSale || !String(form.linkSale).trim() ? null : JSON.parse(form.linkSale);
data.plusList = !form.plusList || !String(form.plusList).trim() ? null : JSON.parse(form.plusList);
data.servicesList = !form.servicesList || !String(form.servicesList).trim() ? null : JSON.parse(form.servicesList);
data.servicesPhotos = !form.servicesPhotos || !String(form.servicesPhotos).trim() ? null : JSON.parse(form.servicesPhotos);
data.sortStaff = !form.sortStaff || !String(form.sortStaff).trim() ? null : JSON.parse(form.sortStaff);
} catch (e) { window.alert('Пожалуйста исправьте ошибки в форме.'); return; }
try {
await updateMedicalCenter({ medicalCenterId: id, data }).unwrap();
setModalSuccess(true);
window.setTimeout(() => window.location.reload(), 2000);
} catch (err) {
console.error('Ошибка сохранения:', err);
}
};
if (isFetching) return <LoadingComponent />;
if (error) return <ErrorComponent />;
if (!item) return <NotFindElement message={`Медцентр с ID=${id} не найдена.`} navigateBack={navigateBack} />;
return (
<EditElementForm navigateBack={navigateBack} header={`Редактирование: медцентр #${id}`} handleSave={handleSave} handleDelete={handleDelete}>
<div className="form-group"><label>Название</label><input type="text" className="form-control" value={form.name} onChange={handleChange('name')} />
{errors.name && <small className="text-danger">{errors.name}</small>}</div>
<div className="form-group form-check"><input type="checkbox" className="form-check-input" id="active" checked={Boolean(form.active)} onChange={handleChange('active')} /><label className="form-check-label" htmlFor="active">Активно</label></div>
<div className="form-group"><label>Регион</label><select className="form-control" value={form.regionId === '' || form.regionId == null ? '' : String(form.regionId)} onChange={handleChange('regionId')}><option value=""></option>{Object.entries(regions).map(([rid, name]) => (<option key={rid} value={rid}>{name}</option>))}</select>
{errors.regionId && <small className="text-danger">{errors.regionId}</small>}</div>
<div className="form-group"><label>Alias</label><input type="text" className="form-control" value={form.alias} onChange={handleChange('alias')} />
{errors.alias && <small className="text-danger">{errors.alias}</small>}</div>
<div className="form-group"><label>Анонс</label><TextEditor content={anons} setContent={setAnons} /></div>
<div className="form-group"><label>Контент</label><TextEditor content={content} setContent={setContent} /></div>
<div className="form-group"><label>mainLinkStaff</label><input type="text" className="form-control" value={form.mainLinkStaff} onChange={handleChange('mainLinkStaff')} /></div>
<div className="form-group"><label>plusText</label><input type="text" className="form-control" value={form.plusText} onChange={handleChange('plusText')} /></div>
<div className="form-group"><label>plusTitle</label><input type="text" className="form-control" value={form.plusTitle} onChange={handleChange('plusTitle')} /></div>
<div className="form-group"><label>processText</label><input type="text" className="form-control" value={form.processText} onChange={handleChange('processText')} /></div>
<div className="form-group"><label>processTitle</label><input type="text" className="form-control" value={form.processTitle} onChange={handleChange('processTitle')} /></div>
<div className="form-group"><label>servicesTitle</label><input type="text" className="form-control" value={form.servicesTitle} onChange={handleChange('servicesTitle')} /></div>
<div className="form-group"><label>trainingText</label><input type="text" className="form-control" value={form.trainingText} onChange={handleChange('trainingText')} /></div>
<div className="form-group"><label>trainingTextTitle</label><input type="text" className="form-control" value={form.trainingTextTitle} onChange={handleChange('trainingTextTitle')} /></div>
<div className="form-group"><label>whyText</label><input type="text" className="form-control" value={form.whyText} onChange={handleChange('whyText')} /></div>
<div className="form-group"><label>whyTitle</label><input type="text" className="form-control" value={form.whyTitle} onChange={handleChange('whyTitle')} /></div>
<div className="form-group"><label>hidePicture</label><input type="number" className="form-control" value={form.hidePicture} onChange={handleChange('hidePicture')} /></div>
<div className="form-group"><label>kodUslug (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.kodUslug} onChange={handleChange('kodUslug')} /></div>
<div className="form-group"><label>doctors (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.doctors} onChange={handleChange('doctors')} /></div>
<div className="form-group"><label>services (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.services} onChange={handleChange('services')} /></div>
<div className="form-group"><label>articles (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.articles} onChange={handleChange('articles')} /></div>
<div className="form-group"><label>txtUp (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.txtUp} onChange={handleChange('txtUp')} /></div>
<div className="form-group"><label>contraindications (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.contraindications} onChange={handleChange('contraindications')} /></div>
<div className="form-group"><label>indications (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.indications} onChange={handleChange('indications')} /></div>
<div className="form-group"><label>linkSale (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.linkSale} onChange={handleChange('linkSale')} /></div>
<div className="form-group"><label>plusList (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.plusList} onChange={handleChange('plusList')} /></div>
<div className="form-group"><label>servicesList (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.servicesList} onChange={handleChange('servicesList')} /></div>
<div className="form-group"><label>servicesPhotos (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.servicesPhotos} onChange={handleChange('servicesPhotos')} /></div>
<div className="form-group"><label>sortStaff (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.sortStaff} onChange={handleChange('sortStaff')} /></div>
<Modal isOpen={isModalSuccess} title="Изменения внесены" hasButtons={false}><p className="mb-1">Изменения успешно внесены.</p></Modal>
</EditElementForm>
);
}
-133
View File
@@ -1,133 +0,0 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { useGetNewsQuery, useUpdateNewsMutation, useDeleteNewsMutation } from '/src/api/apiNews';
import { selectRegions } from '../store/slice/regionSlice';
import { TextEditor } from '../components/Editors/TextEditor';
import { LoadingComponent } from '../components/Placeholders/LoadingComponent';
import { ErrorComponent } from '../components/Placeholders/ErrorComponent';
import { NotFindElement } from '../components/Placeholders/NotFindElement';
import { EditElementForm } from '../components/Forms/EditElementForm';
import Modal from '../components/Modals/Modal';
export function EditNewsPage() {
const { id } = useParams();
const navigate = useNavigate();
const navigateBack = () => navigate(`/news`);
const regions = useSelector(selectRegions);
const { data: item, isFetching, error } = useGetNewsQuery({ newsId: id });
const [updateNews] = useUpdateNewsMutation();
const [deleteNews] = useDeleteNewsMutation();
const [isModalSuccess, setModalSuccess] = useState(false);
const [errors, setErrors] = useState({ name: '', alias: '', regionId: '' });
const [form, setForm] = useState({
name: '',
active: false,
regionId: '',
alias: '',
shortName: '',
linkElPrice: '',
timer: '',
timerBg: '',
formOrder: '',
linkServices: '',
linkStaff: '',
photos: '',
});
const [anons, setAnons] = useState('');
const [content, setContent] = useState('');
useEffect(() => {
if (!item) return;
setForm({
name: item.name ?? '',
active: Boolean(item.active),
regionId: item.regionId ?? '',
alias: item.alias ?? '',
shortName: item.shortName ?? '',
linkElPrice: item.linkElPrice ?? '',
timer: item.timer ?? '',
timerBg: item.timerBg ?? '',
formOrder: item.formOrder == null ? '' : JSON.stringify(item.formOrder, null, 2),
linkServices: item.linkServices == null ? '' : JSON.stringify(item.linkServices, null, 2),
linkStaff: item.linkStaff == null ? '' : JSON.stringify(item.linkStaff, null, 2),
photos: item.photos == null ? '' : JSON.stringify(item.photos, null, 2),
});
setAnons(item.anons ?? '');
setContent(item.content ?? '');
}, [item]);
const handleChange = (key) => (e) => {
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
setForm((f) => ({ ...f, [key]: value }));
if (key === 'name' || key === 'alias' || key === 'regionId') setErrors((err) => ({ ...err, [key]: '' }));
};
const handleDelete = async () => {
try {
await deleteNews({ newsId: id }).unwrap();
setModalSuccess(true);
window.setTimeout(() => navigateBack(), 2000);
} catch (err) {
console.error('Ошибка при удалении:', err);
}
};
const handleSave = async () => {
const newErrors = { name: '', alias: '', regionId: '' };
let hasError = false;
if (!String(form.name ?? '').trim()) { newErrors.name = 'Название не может быть пустым'; hasError = true; }
if (!String(form.alias ?? '').trim()) { newErrors.alias = 'Alias не может быть пустым'; hasError = true; }
if (form.regionId === '' || form.regionId == null) { newErrors.regionId = 'Укажите регион'; hasError = true; }
if (hasError) { setErrors(newErrors); window.alert('Пожалуйста исправьте ошибки в форме.'); return; }
const data = { anons, content };
try {
data.name = form.name === '' ? null : form.name;
data.active = Boolean(form.active);
data.regionId = form.regionId === '' ? null : Number(form.regionId);
data.alias = form.alias === '' ? null : form.alias;
data.shortName = form.shortName === '' ? null : form.shortName;
data.linkElPrice = form.linkElPrice === '' ? null : form.linkElPrice;
data.timer = form.timer === '' ? null : form.timer;
data.timerBg = form.timerBg === '' ? null : form.timerBg;
data.formOrder = !form.formOrder || !String(form.formOrder).trim() ? null : JSON.parse(form.formOrder);
data.linkServices = !form.linkServices || !String(form.linkServices).trim() ? null : JSON.parse(form.linkServices);
data.linkStaff = !form.linkStaff || !String(form.linkStaff).trim() ? null : JSON.parse(form.linkStaff);
data.photos = !form.photos || !String(form.photos).trim() ? null : JSON.parse(form.photos);
} catch (e) { window.alert('Пожалуйста исправьте ошибки в форме.'); return; }
try {
await updateNews({ newsId: id, data }).unwrap();
setModalSuccess(true);
window.setTimeout(() => window.location.reload(), 2000);
} catch (err) {
console.error('Ошибка сохранения:', err);
}
};
if (isFetching) return <LoadingComponent />;
if (error) return <ErrorComponent />;
if (!item) return <NotFindElement message={`Новость с ID=${id} не найдена.`} navigateBack={navigateBack} />;
return (
<EditElementForm navigateBack={navigateBack} header={`Редактирование: новость #${id}`} handleSave={handleSave} handleDelete={handleDelete}>
<div className="form-group"><label>Название</label><input type="text" className="form-control" value={form.name} onChange={handleChange('name')} />
{errors.name && <small className="text-danger">{errors.name}</small>}</div>
<div className="form-group form-check"><input type="checkbox" className="form-check-input" id="active" checked={Boolean(form.active)} onChange={handleChange('active')} /><label className="form-check-label" htmlFor="active">Активно</label></div>
<div className="form-group"><label>Регион</label><select className="form-control" value={form.regionId === '' || form.regionId == null ? '' : String(form.regionId)} onChange={handleChange('regionId')}><option value=""></option>{Object.entries(regions).map(([rid, name]) => (<option key={rid} value={rid}>{name}</option>))}</select>
{errors.regionId && <small className="text-danger">{errors.regionId}</small>}</div>
<div className="form-group"><label>Alias</label><input type="text" className="form-control" value={form.alias} onChange={handleChange('alias')} />
{errors.alias && <small className="text-danger">{errors.alias}</small>}</div>
<div className="form-group"><label>Анонс</label><TextEditor content={anons} setContent={setAnons} /></div>
<div className="form-group"><label>Контент</label><TextEditor content={content} setContent={setContent} /></div>
<div className="form-group"><label>Короткое название</label><input type="text" className="form-control" value={form.shortName} onChange={handleChange('shortName')} /></div>
<div className="form-group"><label>Ссылка на прайс</label><input type="text" className="form-control" value={form.linkElPrice} onChange={handleChange('linkElPrice')} /></div>
<div className="form-group"><label>Таймер</label><input type="text" className="form-control" value={form.timer} onChange={handleChange('timer')} /></div>
<div className="form-group"><label>Фон таймера</label><input type="text" className="form-control" value={form.timerBg} onChange={handleChange('timerBg')} /></div>
<div className="form-group"><label>formOrder (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.formOrder} onChange={handleChange('formOrder')} /></div>
<div className="form-group"><label>linkServices (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.linkServices} onChange={handleChange('linkServices')} /></div>
<div className="form-group"><label>linkStaff (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.linkStaff} onChange={handleChange('linkStaff')} /></div>
<div className="form-group"><label>photos (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.photos} onChange={handleChange('photos')} /></div>
<Modal isOpen={isModalSuccess} title="Изменения внесены" hasButtons={false}><p className="mb-1">Изменения успешно внесены.</p></Modal>
</EditElementForm>
);
}
-133
View File
@@ -1,133 +0,0 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { useGetSitePromoQuery, useUpdateSitePromoMutation, useDeleteSitePromoMutation } from '/src/api/apiSitePromo';
import { selectRegions } from '../store/slice/regionSlice';
import { TextEditor } from '../components/Editors/TextEditor';
import { LoadingComponent } from '../components/Placeholders/LoadingComponent';
import { ErrorComponent } from '../components/Placeholders/ErrorComponent';
import { NotFindElement } from '../components/Placeholders/NotFindElement';
import { EditElementForm } from '../components/Forms/EditElementForm';
import Modal from '../components/Modals/Modal';
export function EditSitePromoPage() {
const { id } = useParams();
const navigate = useNavigate();
const navigateBack = () => navigate(`/site-promo`);
const regions = useSelector(selectRegions);
const { data: item, isFetching, error } = useGetSitePromoQuery({ promoId: id });
const [updateSitePromo] = useUpdateSitePromoMutation();
const [deleteSitePromo] = useDeleteSitePromoMutation();
const [isModalSuccess, setModalSuccess] = useState(false);
const [errors, setErrors] = useState({ name: '', alias: '', regionId: '' });
const [form, setForm] = useState({
name: '',
active: false,
regionId: '',
alias: '',
shortName: '',
period: '',
timer: '',
timerBg: '',
clinics: '',
linkServices: '',
linkStaff: '',
photos: '',
});
const [anons, setAnons] = useState('');
const [content, setContent] = useState('');
useEffect(() => {
if (!item) return;
setForm({
name: item.name ?? '',
active: Boolean(item.active),
regionId: item.regionId ?? '',
alias: item.alias ?? '',
shortName: item.shortName ?? '',
period: item.period ?? '',
timer: item.timer ?? '',
timerBg: item.timerBg ?? '',
clinics: item.clinics == null ? '' : JSON.stringify(item.clinics, null, 2),
linkServices: item.linkServices == null ? '' : JSON.stringify(item.linkServices, null, 2),
linkStaff: item.linkStaff == null ? '' : JSON.stringify(item.linkStaff, null, 2),
photos: item.photos == null ? '' : JSON.stringify(item.photos, null, 2),
});
setAnons(item.anons ?? '');
setContent(item.content ?? '');
}, [item]);
const handleChange = (key) => (e) => {
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
setForm((f) => ({ ...f, [key]: value }));
if (key === 'name' || key === 'alias' || key === 'regionId') setErrors((err) => ({ ...err, [key]: '' }));
};
const handleDelete = async () => {
try {
await deleteSitePromo({ promoId: id }).unwrap();
setModalSuccess(true);
window.setTimeout(() => navigateBack(), 2000);
} catch (err) {
console.error('Ошибка при удалении:', err);
}
};
const handleSave = async () => {
const newErrors = { name: '', alias: '', regionId: '' };
let hasError = false;
if (!String(form.name ?? '').trim()) { newErrors.name = 'Название не может быть пустым'; hasError = true; }
if (!String(form.alias ?? '').trim()) { newErrors.alias = 'Alias не может быть пустым'; hasError = true; }
if (form.regionId === '' || form.regionId == null) { newErrors.regionId = 'Укажите регион'; hasError = true; }
if (hasError) { setErrors(newErrors); window.alert('Пожалуйста исправьте ошибки в форме.'); return; }
const data = { anons, content };
try {
data.name = form.name === '' ? null : form.name;
data.active = Boolean(form.active);
data.regionId = form.regionId === '' ? null : Number(form.regionId);
data.alias = form.alias === '' ? null : form.alias;
data.shortName = form.shortName === '' ? null : form.shortName;
data.period = form.period === '' ? null : form.period;
data.timer = form.timer === '' ? null : form.timer;
data.timerBg = form.timerBg === '' ? null : form.timerBg;
data.clinics = !form.clinics || !String(form.clinics).trim() ? null : JSON.parse(form.clinics);
data.linkServices = !form.linkServices || !String(form.linkServices).trim() ? null : JSON.parse(form.linkServices);
data.linkStaff = !form.linkStaff || !String(form.linkStaff).trim() ? null : JSON.parse(form.linkStaff);
data.photos = !form.photos || !String(form.photos).trim() ? null : JSON.parse(form.photos);
} catch (e) { window.alert('Пожалуйста исправьте ошибки в форме.'); return; }
try {
await updateSitePromo({ promoId: id, data }).unwrap();
setModalSuccess(true);
window.setTimeout(() => window.location.reload(), 2000);
} catch (err) {
console.error('Ошибка сохранения:', err);
}
};
if (isFetching) return <LoadingComponent />;
if (error) return <ErrorComponent />;
if (!item) return <NotFindElement message={`Промо с ID=${id} не найдена.`} navigateBack={navigateBack} />;
return (
<EditElementForm navigateBack={navigateBack} header={`Редактирование: промо #${id}`} handleSave={handleSave} handleDelete={handleDelete}>
<div className="form-group"><label>Название</label><input type="text" className="form-control" value={form.name} onChange={handleChange('name')} />
{errors.name && <small className="text-danger">{errors.name}</small>}</div>
<div className="form-group form-check"><input type="checkbox" className="form-check-input" id="active" checked={Boolean(form.active)} onChange={handleChange('active')} /><label className="form-check-label" htmlFor="active">Активно</label></div>
<div className="form-group"><label>Регион</label><select className="form-control" value={form.regionId === '' || form.regionId == null ? '' : String(form.regionId)} onChange={handleChange('regionId')}><option value=""></option>{Object.entries(regions).map(([rid, name]) => (<option key={rid} value={rid}>{name}</option>))}</select>
{errors.regionId && <small className="text-danger">{errors.regionId}</small>}</div>
<div className="form-group"><label>Alias</label><input type="text" className="form-control" value={form.alias} onChange={handleChange('alias')} />
{errors.alias && <small className="text-danger">{errors.alias}</small>}</div>
<div className="form-group"><label>Анонс</label><TextEditor content={anons} setContent={setAnons} /></div>
<div className="form-group"><label>Контент</label><TextEditor content={content} setContent={setContent} /></div>
<div className="form-group"><label>Короткое название</label><input type="text" className="form-control" value={form.shortName} onChange={handleChange('shortName')} /></div>
<div className="form-group"><label>Период</label><input type="text" className="form-control" value={form.period} onChange={handleChange('period')} /></div>
<div className="form-group"><label>Таймер</label><input type="text" className="form-control" value={form.timer} onChange={handleChange('timer')} /></div>
<div className="form-group"><label>Фон таймера</label><input type="text" className="form-control" value={form.timerBg} onChange={handleChange('timerBg')} /></div>
<div className="form-group"><label>clinics (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.clinics} onChange={handleChange('clinics')} /></div>
<div className="form-group"><label>linkServices (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.linkServices} onChange={handleChange('linkServices')} /></div>
<div className="form-group"><label>linkStaff (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.linkStaff} onChange={handleChange('linkStaff')} /></div>
<div className="form-group"><label>photos (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.photos} onChange={handleChange('photos')} /></div>
<Modal isOpen={isModalSuccess} title="Изменения внесены" hasButtons={false}><p className="mb-1">Изменения успешно внесены.</p></Modal>
</EditElementForm>
);
}
-297
View File
@@ -1,297 +0,0 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { useGetSiteServicesQuery, useUpdateSiteServicesMutation, useDeleteSiteServicesMutation } from '/src/api/apiSiteServices';
import { selectRegions } from '../store/slice/regionSlice';
import { TextEditor } from '../components/Editors/TextEditor';
import { LoadingComponent } from '../components/Placeholders/LoadingComponent';
import { ErrorComponent } from '../components/Placeholders/ErrorComponent';
import { NotFindElement } from '../components/Placeholders/NotFindElement';
import { EditElementForm } from '../components/Forms/EditElementForm';
import Modal from '../components/Modals/Modal';
export function EditSiteServicesPage() {
const { id } = useParams();
const navigate = useNavigate();
const navigateBack = () => navigate(`/site-services`);
const regions = useSelector(selectRegions);
const { data: item, isFetching, error } = useGetSiteServicesQuery({ siteServicesId: id });
const [updateSiteServices] = useUpdateSiteServicesMutation();
const [deleteSiteServices] = useDeleteSiteServicesMutation();
const [isModalSuccess, setModalSuccess] = useState(false);
const [errors, setErrors] = useState({ name: '', alias: '', regionId: '' });
const [form, setForm] = useState({
name: '',
active: false,
regionId: '',
alias: '',
previewImg: '',
partPrice: '',
pokazaniya: '',
preparation: '',
protivopokazaniya: '',
bannerImg: '',
bannerImgM: '',
bannerImgUrl: '',
downloadFile: '',
fullWidthBanner: '',
kodUslug: '',
linkPrice: '',
photosTitle: '',
contraindicationsList: '',
customBlockText: '',
customBlockText2: '',
customBlockTitle: '',
customBlockTitle2: '',
indicationsList: '',
plusList: '',
plusText: '',
plusTitle: '',
prepareTitle: '',
processText: '',
processTitle: '',
servicesList: '',
servicesTitle: '',
textUp: '',
trainingText: '',
whyText: '',
whyTitle: '',
hidePicture: '',
linkVideoreviews: '',
faq: '',
hideSignBtn: '',
quiz: '',
tags: '',
tagsImportant: '',
clinics: '',
staffUp: '',
advantages: '',
saleId: '',
sortStaff: '',
linkArticlesServices: '',
servicesPhotos: '',
linkFaq: '',
linkServices: '',
linkStaff: '',
photos: '',
});
const [anons, setAnons] = useState('');
const [content, setContent] = useState('');
useEffect(() => {
if (!item) return;
setForm({
name: item.name ?? '',
active: Boolean(item.active),
regionId: item.regionId ?? '',
alias: item.alias ?? '',
previewImg: item.previewImg ?? '',
partPrice: item.partPrice ?? '',
pokazaniya: item.pokazaniya ?? '',
preparation: item.preparation ?? '',
protivopokazaniya: item.protivopokazaniya ?? '',
bannerImg: item.bannerImg ?? '',
bannerImgM: item.bannerImgM ?? '',
bannerImgUrl: item.bannerImgUrl ?? '',
downloadFile: item.downloadFile ?? '',
fullWidthBanner: item.fullWidthBanner ?? '',
kodUslug: item.kodUslug ?? '',
linkPrice: item.linkPrice ?? '',
photosTitle: item.photosTitle ?? '',
contraindicationsList: item.contraindicationsList ?? '',
customBlockText: item.customBlockText ?? '',
customBlockText2: item.customBlockText2 ?? '',
customBlockTitle: item.customBlockTitle ?? '',
customBlockTitle2: item.customBlockTitle2 ?? '',
indicationsList: item.indicationsList ?? '',
plusList: item.plusList ?? '',
plusText: item.plusText ?? '',
plusTitle: item.plusTitle ?? '',
prepareTitle: item.prepareTitle ?? '',
processText: item.processText ?? '',
processTitle: item.processTitle ?? '',
servicesList: item.servicesList ?? '',
servicesTitle: item.servicesTitle ?? '',
textUp: item.textUp ?? '',
trainingText: item.trainingText ?? '',
whyText: item.whyText ?? '',
whyTitle: item.whyTitle ?? '',
hidePicture: item.hidePicture ?? '',
linkVideoreviews: item.linkVideoreviews == null ? '' : JSON.stringify(item.linkVideoreviews, null, 2),
faq: item.faq == null ? '' : JSON.stringify(item.faq, null, 2),
hideSignBtn: item.hideSignBtn == null ? '' : JSON.stringify(item.hideSignBtn, null, 2),
quiz: item.quiz == null ? '' : JSON.stringify(item.quiz, null, 2),
tags: item.tags == null ? '' : JSON.stringify(item.tags, null, 2),
tagsImportant: item.tagsImportant == null ? '' : JSON.stringify(item.tagsImportant, null, 2),
clinics: item.clinics == null ? '' : JSON.stringify(item.clinics, null, 2),
staffUp: item.staffUp == null ? '' : JSON.stringify(item.staffUp, null, 2),
advantages: item.advantages == null ? '' : JSON.stringify(item.advantages, null, 2),
saleId: item.saleId == null ? '' : JSON.stringify(item.saleId, null, 2),
sortStaff: item.sortStaff == null ? '' : JSON.stringify(item.sortStaff, null, 2),
linkArticlesServices: item.linkArticlesServices == null ? '' : JSON.stringify(item.linkArticlesServices, null, 2),
servicesPhotos: item.servicesPhotos == null ? '' : JSON.stringify(item.servicesPhotos, null, 2),
linkFaq: item.linkFaq == null ? '' : JSON.stringify(item.linkFaq, null, 2),
linkServices: item.linkServices == null ? '' : JSON.stringify(item.linkServices, null, 2),
linkStaff: item.linkStaff == null ? '' : JSON.stringify(item.linkStaff, null, 2),
photos: item.photos == null ? '' : JSON.stringify(item.photos, null, 2),
});
setAnons(item.anons ?? '');
setContent(item.content ?? '');
}, [item]);
const handleChange = (key) => (e) => {
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
setForm((f) => ({ ...f, [key]: value }));
if (key === 'name' || key === 'alias' || key === 'regionId') setErrors((err) => ({ ...err, [key]: '' }));
};
const handleDelete = async () => {
try {
await deleteSiteServices({ siteServicesId: id }).unwrap();
setModalSuccess(true);
window.setTimeout(() => navigateBack(), 2000);
} catch (err) {
console.error('Ошибка при удалении:', err);
}
};
const handleSave = async () => {
const newErrors = { name: '', alias: '', regionId: '' };
let hasError = false;
if (!String(form.name ?? '').trim()) { newErrors.name = 'Название не может быть пустым'; hasError = true; }
if (!String(form.alias ?? '').trim()) { newErrors.alias = 'Alias не может быть пустым'; hasError = true; }
if (form.regionId === '' || form.regionId == null) { newErrors.regionId = 'Укажите регион'; hasError = true; }
if (hasError) { setErrors(newErrors); window.alert('Пожалуйста исправьте ошибки в форме.'); return; }
const data = { anons, content };
try {
data.name = form.name === '' ? null : form.name;
data.active = Boolean(form.active);
data.regionId = form.regionId === '' ? null : Number(form.regionId);
data.alias = form.alias === '' ? null : form.alias;
data.previewImg = form.previewImg === '' ? null : form.previewImg;
data.partPrice = form.partPrice === '' ? null : form.partPrice;
data.pokazaniya = form.pokazaniya === '' ? null : form.pokazaniya;
data.preparation = form.preparation === '' ? null : form.preparation;
data.protivopokazaniya = form.protivopokazaniya === '' ? null : form.protivopokazaniya;
data.bannerImg = form.bannerImg === '' ? null : form.bannerImg;
data.bannerImgM = form.bannerImgM === '' ? null : form.bannerImgM;
data.bannerImgUrl = form.bannerImgUrl === '' ? null : form.bannerImgUrl;
data.downloadFile = form.downloadFile === '' ? null : form.downloadFile;
data.fullWidthBanner = form.fullWidthBanner === '' ? null : form.fullWidthBanner;
data.kodUslug = form.kodUslug === '' ? null : form.kodUslug;
data.linkPrice = form.linkPrice === '' ? null : form.linkPrice;
data.photosTitle = form.photosTitle === '' ? null : form.photosTitle;
data.contraindicationsList = form.contraindicationsList === '' ? null : form.contraindicationsList;
data.customBlockText = form.customBlockText === '' ? null : form.customBlockText;
data.customBlockText2 = form.customBlockText2 === '' ? null : form.customBlockText2;
data.customBlockTitle = form.customBlockTitle === '' ? null : form.customBlockTitle;
data.customBlockTitle2 = form.customBlockTitle2 === '' ? null : form.customBlockTitle2;
data.indicationsList = form.indicationsList === '' ? null : form.indicationsList;
data.plusList = form.plusList === '' ? null : form.plusList;
data.plusText = form.plusText === '' ? null : form.plusText;
data.plusTitle = form.plusTitle === '' ? null : form.plusTitle;
data.prepareTitle = form.prepareTitle === '' ? null : form.prepareTitle;
data.processText = form.processText === '' ? null : form.processText;
data.processTitle = form.processTitle === '' ? null : form.processTitle;
data.servicesList = form.servicesList === '' ? null : form.servicesList;
data.servicesTitle = form.servicesTitle === '' ? null : form.servicesTitle;
data.textUp = form.textUp === '' ? null : form.textUp;
data.trainingText = form.trainingText === '' ? null : form.trainingText;
data.whyText = form.whyText === '' ? null : form.whyText;
data.whyTitle = form.whyTitle === '' ? null : form.whyTitle;
data.hidePicture = form.hidePicture === '' ? null : Number(form.hidePicture);
data.linkVideoreviews = !form.linkVideoreviews || !String(form.linkVideoreviews).trim() ? null : JSON.parse(form.linkVideoreviews);
data.faq = !form.faq || !String(form.faq).trim() ? null : JSON.parse(form.faq);
data.hideSignBtn = !form.hideSignBtn || !String(form.hideSignBtn).trim() ? null : JSON.parse(form.hideSignBtn);
data.quiz = !form.quiz || !String(form.quiz).trim() ? null : JSON.parse(form.quiz);
data.tags = !form.tags || !String(form.tags).trim() ? null : JSON.parse(form.tags);
data.tagsImportant = !form.tagsImportant || !String(form.tagsImportant).trim() ? null : JSON.parse(form.tagsImportant);
data.clinics = !form.clinics || !String(form.clinics).trim() ? null : JSON.parse(form.clinics);
data.staffUp = !form.staffUp || !String(form.staffUp).trim() ? null : JSON.parse(form.staffUp);
data.advantages = !form.advantages || !String(form.advantages).trim() ? null : JSON.parse(form.advantages);
data.saleId = !form.saleId || !String(form.saleId).trim() ? null : JSON.parse(form.saleId);
data.sortStaff = !form.sortStaff || !String(form.sortStaff).trim() ? null : JSON.parse(form.sortStaff);
data.linkArticlesServices = !form.linkArticlesServices || !String(form.linkArticlesServices).trim() ? null : JSON.parse(form.linkArticlesServices);
data.servicesPhotos = !form.servicesPhotos || !String(form.servicesPhotos).trim() ? null : JSON.parse(form.servicesPhotos);
data.linkFaq = !form.linkFaq || !String(form.linkFaq).trim() ? null : JSON.parse(form.linkFaq);
data.linkServices = !form.linkServices || !String(form.linkServices).trim() ? null : JSON.parse(form.linkServices);
data.linkStaff = !form.linkStaff || !String(form.linkStaff).trim() ? null : JSON.parse(form.linkStaff);
data.photos = !form.photos || !String(form.photos).trim() ? null : JSON.parse(form.photos);
} catch (e) { window.alert('Пожалуйста исправьте ошибки в форме.'); return; }
try {
await updateSiteServices({ siteServicesId: id, data }).unwrap();
setModalSuccess(true);
window.setTimeout(() => window.location.reload(), 2000);
} catch (err) {
console.error('Ошибка сохранения:', err);
}
};
if (isFetching) return <LoadingComponent />;
if (error) return <ErrorComponent />;
if (!item) return <NotFindElement message={`Услуга с ID=${id} не найдена.`} navigateBack={navigateBack} />;
return (
<EditElementForm navigateBack={navigateBack} header={`Редактирование: услугу #${id}`} handleSave={handleSave} handleDelete={handleDelete}>
<div className="form-group"><label>Название</label><input type="text" className="form-control" value={form.name} onChange={handleChange('name')} />
{errors.name && <small className="text-danger">{errors.name}</small>}</div>
<div className="form-group form-check"><input type="checkbox" className="form-check-input" id="active" checked={Boolean(form.active)} onChange={handleChange('active')} /><label className="form-check-label" htmlFor="active">Активно</label></div>
<div className="form-group"><label>Регион</label><select className="form-control" value={form.regionId === '' || form.regionId == null ? '' : String(form.regionId)} onChange={handleChange('regionId')}><option value=""></option>{Object.entries(regions).map(([rid, name]) => (<option key={rid} value={rid}>{name}</option>))}</select>
{errors.regionId && <small className="text-danger">{errors.regionId}</small>}</div>
<div className="form-group"><label>Alias</label><input type="text" className="form-control" value={form.alias} onChange={handleChange('alias')} />
{errors.alias && <small className="text-danger">{errors.alias}</small>}</div>
<div className="form-group"><label>Анонс</label><TextEditor content={anons} setContent={setAnons} /></div>
<div className="form-group"><label>Контент</label><TextEditor content={content} setContent={setContent} /></div>
<div className="form-group"><label>previewImg</label><input type="text" className="form-control" value={form.previewImg} onChange={handleChange('previewImg')} /></div>
<div className="form-group"><label>partPrice</label><input type="text" className="form-control" value={form.partPrice} onChange={handleChange('partPrice')} /></div>
<div className="form-group"><label>pokazaniya</label><input type="text" className="form-control" value={form.pokazaniya} onChange={handleChange('pokazaniya')} /></div>
<div className="form-group"><label>preparation</label><input type="text" className="form-control" value={form.preparation} onChange={handleChange('preparation')} /></div>
<div className="form-group"><label>protivopokazaniya</label><input type="text" className="form-control" value={form.protivopokazaniya} onChange={handleChange('protivopokazaniya')} /></div>
<div className="form-group"><label>bannerImg</label><input type="text" className="form-control" value={form.bannerImg} onChange={handleChange('bannerImg')} /></div>
<div className="form-group"><label>bannerImgM</label><input type="text" className="form-control" value={form.bannerImgM} onChange={handleChange('bannerImgM')} /></div>
<div className="form-group"><label>bannerImgUrl</label><input type="text" className="form-control" value={form.bannerImgUrl} onChange={handleChange('bannerImgUrl')} /></div>
<div className="form-group"><label>downloadFile</label><input type="text" className="form-control" value={form.downloadFile} onChange={handleChange('downloadFile')} /></div>
<div className="form-group"><label>fullWidthBanner</label><input type="text" className="form-control" value={form.fullWidthBanner} onChange={handleChange('fullWidthBanner')} /></div>
<div className="form-group"><label>kodUslug</label><input type="text" className="form-control" value={form.kodUslug} onChange={handleChange('kodUslug')} /></div>
<div className="form-group"><label>linkPrice</label><input type="text" className="form-control" value={form.linkPrice} onChange={handleChange('linkPrice')} /></div>
<div className="form-group"><label>photosTitle</label><input type="text" className="form-control" value={form.photosTitle} onChange={handleChange('photosTitle')} /></div>
<div className="form-group"><label>contraindicationsList</label><input type="text" className="form-control" value={form.contraindicationsList} onChange={handleChange('contraindicationsList')} /></div>
<div className="form-group"><label>customBlockText</label><input type="text" className="form-control" value={form.customBlockText} onChange={handleChange('customBlockText')} /></div>
<div className="form-group"><label>customBlockText2</label><input type="text" className="form-control" value={form.customBlockText2} onChange={handleChange('customBlockText2')} /></div>
<div className="form-group"><label>customBlockTitle</label><input type="text" className="form-control" value={form.customBlockTitle} onChange={handleChange('customBlockTitle')} /></div>
<div className="form-group"><label>customBlockTitle2</label><input type="text" className="form-control" value={form.customBlockTitle2} onChange={handleChange('customBlockTitle2')} /></div>
<div className="form-group"><label>indicationsList</label><input type="text" className="form-control" value={form.indicationsList} onChange={handleChange('indicationsList')} /></div>
<div className="form-group"><label>plusList</label><input type="text" className="form-control" value={form.plusList} onChange={handleChange('plusList')} /></div>
<div className="form-group"><label>plusText</label><input type="text" className="form-control" value={form.plusText} onChange={handleChange('plusText')} /></div>
<div className="form-group"><label>plusTitle</label><input type="text" className="form-control" value={form.plusTitle} onChange={handleChange('plusTitle')} /></div>
<div className="form-group"><label>prepareTitle</label><input type="text" className="form-control" value={form.prepareTitle} onChange={handleChange('prepareTitle')} /></div>
<div className="form-group"><label>processText</label><input type="text" className="form-control" value={form.processText} onChange={handleChange('processText')} /></div>
<div className="form-group"><label>processTitle</label><input type="text" className="form-control" value={form.processTitle} onChange={handleChange('processTitle')} /></div>
<div className="form-group"><label>servicesList</label><input type="text" className="form-control" value={form.servicesList} onChange={handleChange('servicesList')} /></div>
<div className="form-group"><label>servicesTitle</label><input type="text" className="form-control" value={form.servicesTitle} onChange={handleChange('servicesTitle')} /></div>
<div className="form-group"><label>textUp</label><input type="text" className="form-control" value={form.textUp} onChange={handleChange('textUp')} /></div>
<div className="form-group"><label>trainingText</label><input type="text" className="form-control" value={form.trainingText} onChange={handleChange('trainingText')} /></div>
<div className="form-group"><label>whyText</label><input type="text" className="form-control" value={form.whyText} onChange={handleChange('whyText')} /></div>
<div className="form-group"><label>whyTitle</label><input type="text" className="form-control" value={form.whyTitle} onChange={handleChange('whyTitle')} /></div>
<div className="form-group"><label>hidePicture</label><input type="number" className="form-control" value={form.hidePicture} onChange={handleChange('hidePicture')} /></div>
<div className="form-group"><label>linkVideoreviews (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.linkVideoreviews} onChange={handleChange('linkVideoreviews')} /></div>
<div className="form-group"><label>faq (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.faq} onChange={handleChange('faq')} /></div>
<div className="form-group"><label>hideSignBtn (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.hideSignBtn} onChange={handleChange('hideSignBtn')} /></div>
<div className="form-group"><label>quiz (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.quiz} onChange={handleChange('quiz')} /></div>
<div className="form-group"><label>tags (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.tags} onChange={handleChange('tags')} /></div>
<div className="form-group"><label>tagsImportant (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.tagsImportant} onChange={handleChange('tagsImportant')} /></div>
<div className="form-group"><label>clinics (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.clinics} onChange={handleChange('clinics')} /></div>
<div className="form-group"><label>staffUp (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.staffUp} onChange={handleChange('staffUp')} /></div>
<div className="form-group"><label>advantages (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.advantages} onChange={handleChange('advantages')} /></div>
<div className="form-group"><label>saleId (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.saleId} onChange={handleChange('saleId')} /></div>
<div className="form-group"><label>sortStaff (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.sortStaff} onChange={handleChange('sortStaff')} /></div>
<div className="form-group"><label>linkArticlesServices (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.linkArticlesServices} onChange={handleChange('linkArticlesServices')} /></div>
<div className="form-group"><label>servicesPhotos (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.servicesPhotos} onChange={handleChange('servicesPhotos')} /></div>
<div className="form-group"><label>linkFaq (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.linkFaq} onChange={handleChange('linkFaq')} /></div>
<div className="form-group"><label>linkServices (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.linkServices} onChange={handleChange('linkServices')} /></div>
<div className="form-group"><label>linkStaff (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.linkStaff} onChange={handleChange('linkStaff')} /></div>
<div className="form-group"><label>photos (JSON)</label><textarea className="form-control font-monospace" rows={4} value={form.photos} onChange={handleChange('photos')} /></div>
<Modal isOpen={isModalSuccess} title="Изменения внесены" hasButtons={false}><p className="mb-1">Изменения успешно внесены.</p></Modal>
</EditElementForm>
);
}
+3 -1
View File
@@ -2,6 +2,8 @@ import {useEffect, useState, useMemo, useRef} from 'react'
import {useParams, useNavigate, useLocation } from 'react-router-dom'
import DatePicker from "react-datepicker";
import { apiUrl } from '@/config/api';
import { useUpdateSpecialistMutation, useDeleteSpecialistMutation, useUploadSpecialistPictureMutation } from '../api/apiSpecialist'
import { useCreateLocationMutation, useUpdateLocationMutation, useDeleteLocationMutation } from '/src/api/apiLocation.js'
import {
@@ -386,7 +388,7 @@ export const EditSpecialistPage = () => {
}
updateField('category', formatCategory(specialist.category));
updateField('previewPicture', `https://api.sovamed.ru${specialist.pictureLink}`);
updateField('previewPicture', apiUrl(specialist.pictureLink));
const formattedDate = specialist.experience
? `${specialist.experience}-01-01`
+3 -1
View File
@@ -1,6 +1,8 @@
import { useState, useEffect, useRef } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { apiUrl } from '@/config/api';
import {
useGetStockQuery,
useUpdateStockMutation,
@@ -85,7 +87,7 @@ export function EditStockPage() {
name: stock.name ?? '',
startDate: datetimeLocalStart,
endDate: datetimeLocalEnd,
picture: stock.picture ? `https://api.sovamed.ru/uploads/${stock.picture}` : '',
picture: stock.picture ? apiUrl(`/uploads/${stock.picture}`) : '',
}));
setAnons( stock.anons )
-115
View File
@@ -1,115 +0,0 @@
import { useState, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { useGetMedicalCenterListQuery } from '/src/api/apiMedicalCenter';
import { selectRegions } from '../store/slice/regionSlice';
import { useOutsideClick } from '../hooks/useOutsideClick';
import { LoadingComponent } from '../components/Placeholders/LoadingComponent';
import { ErrorComponent } from '../components/Placeholders/ErrorComponent';
export const MedicalCenterListPage = () => {
const [ searchValue, setSearchValue ] = useState( '' );
const [ currentPage, setCurrentPage ] = useState( 1 );
const [ expandedId, setExpandedId ] = useState( '' );
const navigate = useNavigate();
const regions = useSelector(selectRegions);
const tableRef = useRef( null );
useOutsideClick( tableRef, () => setExpandedId( null ));
const { data: response = {}, isFetching, error: queryError } =
useGetMedicalCenterListQuery( { search: searchValue, page: currentPage } );
const pagination = response.pagination || {};
const items = response.data ? response.data : [];
const renderPagination = () => {
const total = pagination.total_pages || 1;
const current = pagination.current_page || 1;
const pages = new Set( [ 1, total ] );
for ( let page = current - 2; page <= current + 2; page += 1 ) {
if ( page > 1 && page < total ) pages.add( page );
}
const sorted = Array.from( pages ).sort( ( a, b ) => a - b );
const elements = [];
let last = 0;
sorted.forEach( page => {
if ( last && page - last > 1 ) {
elements.push(
<li key={ `dots-${last}` } className="page-item disabled">
<span className="page-link"></span>
</li>
);
}
elements.push(
<li key={ page } className={`page-item ${ page === current ? 'active' : '' }`}>
<button type="button" className="page-link" onClick={ () => page !== current && setCurrentPage( page ) }>
{ page }
</button>
</li>
);
last = page;
});
return (
<nav>
<ul className="pagination justify-content-center">
<li className={`page-item ${ !pagination.has_previous_page ? 'disabled' : '' }`}>
<button type="button" className="page-link" onClick={ () => pagination.has_previous_page && setCurrentPage(current - 1) }>&laquo;</button>
</li>
{ elements }
<li className={`page-item ${ !pagination.has_next_page ? 'disabled' : '' }`}>
<button type="button" className="page-link" onClick={ () => pagination.has_next_page && setCurrentPage(current + 1) }>&raquo;</button>
</li>
</ul>
</nav>
);
};
return (
<div className="container-fluid">
<h1 className="h3 mb-4 text-gray-800">Медцентры</h1>
<div className="d-flex justify-content-between mb-3" style={{ marginRight: '0.1rem', marginLeft: '0.1rem' }}>
<div className="form-group align-self-end mr-3">
<input type="button" className="btn btn-outline-primary" value="Добавить" onClick={ e => { e.stopPropagation(); navigate(`/medical-center/create`); } } />
</div>
<div className="form-group flex-grow-1">
<label>Поиск</label>
<input type="text" className="form-control" value={ searchValue } onChange={ e => { setSearchValue( e.target.value ); setCurrentPage( 1 ); } } />
</div>
</div>
{ isFetching ? <LoadingComponent /> : queryError ? <ErrorComponent /> : (
<>
<div className="table-responsive" ref={tableRef}>
<table className="table table-hover table-bordered">
<thead><tr>
<th>ID</th>
<th>Название</th>
<th>Alias</th>
<th>Активно</th>
</tr></thead>
<tbody>
{items.map( item => (
<>
<tr key={ item.id } className={ `cursor-pointer${ expandedId === item.id ? ' table-success' : '' }` } onClick={ () => setExpandedId( expandedId === item.id ? null : item.id ) }>
<td>{ item.id }</td>
<td>{ item.name }</td>
<td>{ item.alias }</td>
<td>{ item.active ? 'Да' : 'Нет' }</td>
</tr>
{ expandedId === item.id && (
<tr className='table-success'>
<td colSpan={ 4 }>
<input type="button" className="btn btn-outline-primary" value="Редактировать" onClick={ e => { e.stopPropagation(); navigate(`/medical-center/edit/${item.id}`) } } />
</td>
</tr>
)}
</>
))}
</tbody>
</table>
</div>
{ renderPagination() }
</>
)}
</div>
);
};
-117
View File
@@ -1,117 +0,0 @@
import { useState, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { useGetNewsListQuery } from '/src/api/apiNews';
import { selectRegions } from '../store/slice/regionSlice';
import { useOutsideClick } from '../hooks/useOutsideClick';
import { LoadingComponent } from '../components/Placeholders/LoadingComponent';
import { ErrorComponent } from '../components/Placeholders/ErrorComponent';
export const NewsListPage = () => {
const [ searchValue, setSearchValue ] = useState( '' );
const [ currentPage, setCurrentPage ] = useState( 1 );
const [ expandedId, setExpandedId ] = useState( '' );
const navigate = useNavigate();
const regions = useSelector(selectRegions);
const tableRef = useRef( null );
useOutsideClick( tableRef, () => setExpandedId( null ));
const { data: response = {}, isFetching, error: queryError } =
useGetNewsListQuery( { search: searchValue, page: currentPage } );
const pagination = response.pagination || {};
const items = response.data ? response.data : [];
const renderPagination = () => {
const total = pagination.total_pages || 1;
const current = pagination.current_page || 1;
const pages = new Set( [ 1, total ] );
for ( let page = current - 2; page <= current + 2; page += 1 ) {
if ( page > 1 && page < total ) pages.add( page );
}
const sorted = Array.from( pages ).sort( ( a, b ) => a - b );
const elements = [];
let last = 0;
sorted.forEach( page => {
if ( last && page - last > 1 ) {
elements.push(
<li key={ `dots-${last}` } className="page-item disabled">
<span className="page-link"></span>
</li>
);
}
elements.push(
<li key={ page } className={`page-item ${ page === current ? 'active' : '' }`}>
<button type="button" className="page-link" onClick={ () => page !== current && setCurrentPage( page ) }>
{ page }
</button>
</li>
);
last = page;
});
return (
<nav>
<ul className="pagination justify-content-center">
<li className={`page-item ${ !pagination.has_previous_page ? 'disabled' : '' }`}>
<button type="button" className="page-link" onClick={ () => pagination.has_previous_page && setCurrentPage(current - 1) }>&laquo;</button>
</li>
{ elements }
<li className={`page-item ${ !pagination.has_next_page ? 'disabled' : '' }`}>
<button type="button" className="page-link" onClick={ () => pagination.has_next_page && setCurrentPage(current + 1) }>&raquo;</button>
</li>
</ul>
</nav>
);
};
return (
<div className="container-fluid">
<h1 className="h3 mb-4 text-gray-800">Новости</h1>
<div className="d-flex justify-content-between mb-3" style={{ marginRight: '0.1rem', marginLeft: '0.1rem' }}>
<div className="form-group align-self-end mr-3">
<input type="button" className="btn btn-outline-primary" value="Добавить" onClick={ e => { e.stopPropagation(); navigate(`/news/create`); } } />
</div>
<div className="form-group flex-grow-1">
<label>Поиск</label>
<input type="text" className="form-control" value={ searchValue } onChange={ e => { setSearchValue( e.target.value ); setCurrentPage( 1 ); } } />
</div>
</div>
{ isFetching ? <LoadingComponent /> : queryError ? <ErrorComponent /> : (
<>
<div className="table-responsive" ref={tableRef}>
<table className="table table-hover table-bordered">
<thead><tr>
<th>ID</th>
<th>Название</th>
<th>Alias</th>
<th>Активно</th>
<th>Регион</th>
</tr></thead>
<tbody>
{items.map( item => (
<>
<tr key={ item.id } className={ `cursor-pointer${ expandedId === item.id ? ' table-success' : '' }` } onClick={ () => setExpandedId( expandedId === item.id ? null : item.id ) }>
<td>{ item.id }</td>
<td>{ item.name }</td>
<td>{ item.alias }</td>
<td>{ item.active ? 'Да' : 'Нет' }</td>
<td>{ regions[item.regionId] ?? item.regionId }</td>
</tr>
{ expandedId === item.id && (
<tr className='table-success'>
<td colSpan={ 5 }>
<input type="button" className="btn btn-outline-primary" value="Редактировать" onClick={ e => { e.stopPropagation(); navigate(`/news/edit/${item.id}`) } } />
</td>
</tr>
)}
</>
))}
</tbody>
</table>
</div>
{ renderPagination() }
</>
)}
</div>
);
};
-117
View File
@@ -1,117 +0,0 @@
import { useState, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { useGetSitePromoListQuery } from '/src/api/apiSitePromo';
import { selectRegions } from '../store/slice/regionSlice';
import { useOutsideClick } from '../hooks/useOutsideClick';
import { LoadingComponent } from '../components/Placeholders/LoadingComponent';
import { ErrorComponent } from '../components/Placeholders/ErrorComponent';
export const SitePromoListPage = () => {
const [ searchValue, setSearchValue ] = useState( '' );
const [ currentPage, setCurrentPage ] = useState( 1 );
const [ expandedId, setExpandedId ] = useState( '' );
const navigate = useNavigate();
const regions = useSelector(selectRegions);
const tableRef = useRef( null );
useOutsideClick( tableRef, () => setExpandedId( null ));
const { data: response = {}, isFetching, error: queryError } =
useGetSitePromoListQuery( { search: searchValue, page: currentPage } );
const pagination = response.pagination || {};
const items = response.data ? response.data : [];
const renderPagination = () => {
const total = pagination.total_pages || 1;
const current = pagination.current_page || 1;
const pages = new Set( [ 1, total ] );
for ( let page = current - 2; page <= current + 2; page += 1 ) {
if ( page > 1 && page < total ) pages.add( page );
}
const sorted = Array.from( pages ).sort( ( a, b ) => a - b );
const elements = [];
let last = 0;
sorted.forEach( page => {
if ( last && page - last > 1 ) {
elements.push(
<li key={ `dots-${last}` } className="page-item disabled">
<span className="page-link"></span>
</li>
);
}
elements.push(
<li key={ page } className={`page-item ${ page === current ? 'active' : '' }`}>
<button type="button" className="page-link" onClick={ () => page !== current && setCurrentPage( page ) }>
{ page }
</button>
</li>
);
last = page;
});
return (
<nav>
<ul className="pagination justify-content-center">
<li className={`page-item ${ !pagination.has_previous_page ? 'disabled' : '' }`}>
<button type="button" className="page-link" onClick={ () => pagination.has_previous_page && setCurrentPage(current - 1) }>&laquo;</button>
</li>
{ elements }
<li className={`page-item ${ !pagination.has_next_page ? 'disabled' : '' }`}>
<button type="button" className="page-link" onClick={ () => pagination.has_next_page && setCurrentPage(current + 1) }>&raquo;</button>
</li>
</ul>
</nav>
);
};
return (
<div className="container-fluid">
<h1 className="h3 mb-4 text-gray-800">Промо (контент)</h1>
<div className="d-flex justify-content-between mb-3" style={{ marginRight: '0.1rem', marginLeft: '0.1rem' }}>
<div className="form-group align-self-end mr-3">
<input type="button" className="btn btn-outline-primary" value="Добавить" onClick={ e => { e.stopPropagation(); navigate(`/site-promo/create`); } } />
</div>
<div className="form-group flex-grow-1">
<label>Поиск</label>
<input type="text" className="form-control" value={ searchValue } onChange={ e => { setSearchValue( e.target.value ); setCurrentPage( 1 ); } } />
</div>
</div>
{ isFetching ? <LoadingComponent /> : queryError ? <ErrorComponent /> : (
<>
<div className="table-responsive" ref={tableRef}>
<table className="table table-hover table-bordered">
<thead><tr>
<th>ID</th>
<th>Название</th>
<th>Alias</th>
<th>Активно</th>
<th>Регион</th>
</tr></thead>
<tbody>
{items.map( item => (
<>
<tr key={ item.id } className={ `cursor-pointer${ expandedId === item.id ? ' table-success' : '' }` } onClick={ () => setExpandedId( expandedId === item.id ? null : item.id ) }>
<td>{ item.id }</td>
<td>{ item.name }</td>
<td>{ item.alias }</td>
<td>{ item.active ? 'Да' : 'Нет' }</td>
<td>{ regions[item.regionId] ?? item.regionId }</td>
</tr>
{ expandedId === item.id && (
<tr className='table-success'>
<td colSpan={ 5 }>
<input type="button" className="btn btn-outline-primary" value="Редактировать" onClick={ e => { e.stopPropagation(); navigate(`/site-promo/edit/${item.id}`) } } />
</td>
</tr>
)}
</>
))}
</tbody>
</table>
</div>
{ renderPagination() }
</>
)}
</div>
);
};
-115
View File
@@ -1,115 +0,0 @@
import { useState, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { useGetSiteServicesListQuery } from '/src/api/apiSiteServices';
import { selectRegions } from '../store/slice/regionSlice';
import { useOutsideClick } from '../hooks/useOutsideClick';
import { LoadingComponent } from '../components/Placeholders/LoadingComponent';
import { ErrorComponent } from '../components/Placeholders/ErrorComponent';
export const SiteServicesListPage = () => {
const [ searchValue, setSearchValue ] = useState( '' );
const [ currentPage, setCurrentPage ] = useState( 1 );
const [ expandedId, setExpandedId ] = useState( '' );
const navigate = useNavigate();
const regions = useSelector(selectRegions);
const tableRef = useRef( null );
useOutsideClick( tableRef, () => setExpandedId( null ));
const { data: response = {}, isFetching, error: queryError } =
useGetSiteServicesListQuery( { search: searchValue, page: currentPage } );
const pagination = response.pagination || {};
const items = response.data ? response.data : [];
const renderPagination = () => {
const total = pagination.total_pages || 1;
const current = pagination.current_page || 1;
const pages = new Set( [ 1, total ] );
for ( let page = current - 2; page <= current + 2; page += 1 ) {
if ( page > 1 && page < total ) pages.add( page );
}
const sorted = Array.from( pages ).sort( ( a, b ) => a - b );
const elements = [];
let last = 0;
sorted.forEach( page => {
if ( last && page - last > 1 ) {
elements.push(
<li key={ `dots-${last}` } className="page-item disabled">
<span className="page-link"></span>
</li>
);
}
elements.push(
<li key={ page } className={`page-item ${ page === current ? 'active' : '' }`}>
<button type="button" className="page-link" onClick={ () => page !== current && setCurrentPage( page ) }>
{ page }
</button>
</li>
);
last = page;
});
return (
<nav>
<ul className="pagination justify-content-center">
<li className={`page-item ${ !pagination.has_previous_page ? 'disabled' : '' }`}>
<button type="button" className="page-link" onClick={ () => pagination.has_previous_page && setCurrentPage(current - 1) }>&laquo;</button>
</li>
{ elements }
<li className={`page-item ${ !pagination.has_next_page ? 'disabled' : '' }`}>
<button type="button" className="page-link" onClick={ () => pagination.has_next_page && setCurrentPage(current + 1) }>&raquo;</button>
</li>
</ul>
</nav>
);
};
return (
<div className="container-fluid">
<h1 className="h3 mb-4 text-gray-800">Услуги сайта</h1>
<div className="d-flex justify-content-between mb-3" style={{ marginRight: '0.1rem', marginLeft: '0.1rem' }}>
<div className="form-group align-self-end mr-3">
<input type="button" className="btn btn-outline-primary" value="Добавить" onClick={ e => { e.stopPropagation(); navigate(`/site-services/create`); } } />
</div>
<div className="form-group flex-grow-1">
<label>Поиск</label>
<input type="text" className="form-control" value={ searchValue } onChange={ e => { setSearchValue( e.target.value ); setCurrentPage( 1 ); } } />
</div>
</div>
{ isFetching ? <LoadingComponent /> : queryError ? <ErrorComponent /> : (
<>
<div className="table-responsive" ref={tableRef}>
<table className="table table-hover table-bordered">
<thead><tr>
<th>ID</th>
<th>Название</th>
<th>Alias</th>
<th>Активно</th>
</tr></thead>
<tbody>
{items.map( item => (
<>
<tr key={ item.id } className={ `cursor-pointer${ expandedId === item.id ? ' table-success' : '' }` } onClick={ () => setExpandedId( expandedId === item.id ? null : item.id ) }>
<td>{ item.id }</td>
<td>{ item.name }</td>
<td>{ item.alias }</td>
<td>{ item.active ? 'Да' : 'Нет' }</td>
</tr>
{ expandedId === item.id && (
<tr className='table-success'>
<td colSpan={ 4 }>
<input type="button" className="btn btn-outline-primary" value="Редактировать" onClick={ e => { e.stopPropagation(); navigate(`/site-services/edit/${item.id}`) } } />
</td>
</tr>
)}
</>
))}
</tbody>
</table>
</div>
{ renderPagination() }
</>
)}
</div>
);
};
+370
View File
@@ -0,0 +1,370 @@
import { useEffect, useRef, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { useSelector } from 'react-redux'
import { selectRegions } from '../../store/slice/regionSlice'
import { TextEditor } from '../../components/Editors/TextEditor'
import { LoadingComponent } from '../../components/Placeholders/LoadingComponent'
import { ErrorComponent } from '../../components/Placeholders/ErrorComponent'
import { NotFindElement } from '../../components/Placeholders/NotFindElement'
import { EditElementForm } from '../../components/Forms/EditElementForm'
import Modal from '../../components/Modals/Modal'
import { parseSaveError } from '../../utils/parseSaveError'
const SUCCESS_MODAL_MS = 2000
const emptyFormFromConfig = (fields) => {
const form = {}
fields.forEach((field) => {
if (field.type === 'checkbox') {
form[field.key] = false
} else if (field.type === 'number' || field.type === 'region') {
form[field.key] = ''
} else {
form[field.key] = ''
}
})
return form
}
const itemToForm = (item, fields) => {
const form = {}
fields.forEach((field) => {
const value = item[field.key]
if (field.type === 'json') {
form[field.key] = value == null ? '' : JSON.stringify(value, null, 2)
} else if (field.type === 'checkbox') {
form[field.key] = Boolean(value)
} else if (field.type === 'region' || field.type === 'number') {
form[field.key] = value ?? ''
} else {
form[field.key] = value ?? ''
}
})
return form
}
const formToPayload = (form, fields) => {
const data = {}
fields.forEach((field) => {
const raw = form[field.key]
if (field.type === 'json') {
if (!raw || !String(raw).trim()) {
data[field.key] = null
return
}
try {
data[field.key] = JSON.parse(raw)
} catch {
const error = new Error('Невалидный JSON — проверьте синтаксис')
error.fieldKey = field.key
throw error
}
return
}
if (field.type === 'checkbox') {
data[field.key] = Boolean(raw)
return
}
if (field.type === 'region' || field.type === 'number') {
data[field.key] = raw === '' ? null : Number(raw)
return
}
data[field.key] = raw === '' ? null : raw
})
return data
}
const FieldHint = ({ message }) =>
message ? <span className="content-field-error-msg" role="alert">{message}</span> : null
const fieldWrapperClass = (hasError, extra = '') =>
['form-group', extra, hasError ? 'content-field--has-error' : ''].filter(Boolean).join(' ')
const ContentField = ({ field, form, updateField, regions, fieldErrors }) => {
const value = form[field.key]
const errorMessage = fieldErrors[field.key]
const hasError = Boolean(errorMessage)
const labelClass = hasError ? 'content-field-error-label' : undefined
const controlClass = `form-control${hasError ? ' is-invalid' : ''}`
if (field.type === 'checkbox') {
return (
<div className={fieldWrapperClass(hasError, 'form-check')} data-field-key={field.key}>
<input
type="checkbox"
className={`form-check-input${hasError ? ' is-invalid' : ''}`}
id={field.key}
checked={Boolean(value)}
onChange={(e) => updateField(field.key, e.target.checked)}
/>
<label className={`form-check-label${hasError ? ' content-field-error-label' : ''}`} htmlFor={field.key}>
{field.label}
</label>
<FieldHint message={errorMessage} />
</div>
)
}
if (field.type === 'region') {
const selectValue = value === '' || value == null ? '' : String(value)
return (
<div className={fieldWrapperClass(hasError)} data-field-key={field.key}>
<label className={labelClass}>{field.label}</label>
<select
className={controlClass}
value={selectValue}
onChange={(e) => updateField(field.key, e.target.value)}
>
<option value=""></option>
{Object.entries(regions).map(([id, name]) => (
<option key={id} value={id}>
{name}
</option>
))}
</select>
<FieldHint message={errorMessage} />
</div>
)
}
if (field.type === 'html') {
return (
<div className={fieldWrapperClass(hasError)} data-field-key={field.key}>
<label className={labelClass}>{field.label}</label>
<TextEditor content={value} setContent={(html) => updateField(field.key, html)} />
<FieldHint message={errorMessage} />
</div>
)
}
if (field.type === 'json') {
return (
<div className={fieldWrapperClass(hasError)} data-field-key={field.key}>
<label className={labelClass}>{field.label}</label>
<textarea
className={`${controlClass} font-monospace`}
rows={4}
value={value}
onChange={(e) => updateField(field.key, e.target.value)}
/>
<FieldHint message={errorMessage} />
</div>
)
}
return (
<div className={fieldWrapperClass(hasError)} data-field-key={field.key}>
<label className={labelClass}>{field.label}</label>
<input
type={field.type === 'number' ? 'number' : 'text'}
className={controlClass}
value={value}
onChange={(e) => updateField(field.key, e.target.value)}
/>
<FieldHint message={errorMessage} />
</div>
)
}
export const ContentEditPage = ({ config, hooks, isCreate = false }) => {
const { id } = useParams()
const navigate = useNavigate()
const regions = useSelector(selectRegions)
const navigateBack = () => navigate(`/${config.slug}`)
const formTopRef = useRef(null)
const successTimerRef = useRef(null)
const skip = isCreate || !id
const { data: item, isLoading, error, refetch } = hooks.useItemQuery(id, { skip })
const [createItem] = hooks.useCreateMutation()
const [updateItem] = hooks.useUpdateMutation()
const [deleteItem] = hooks.useDeleteMutation()
const [form, setForm] = useState(() => emptyFormFromConfig(config.fields))
const [fieldErrors, setFieldErrors] = useState({})
const [globalError, setGlobalError] = useState(null)
const [isModalSuccess, setModalSuccess] = useState(false)
useEffect(() => {
return () => {
if (successTimerRef.current) {
window.clearTimeout(successTimerRef.current)
}
}
}, [])
useEffect(() => {
if (isCreate || !item) {
return
}
setForm(itemToForm(item, config.fields))
}, [item, isCreate, config.fields])
const showSuccessModal = (onClose) => {
setModalSuccess(true)
if (successTimerRef.current) {
window.clearTimeout(successTimerRef.current)
}
successTimerRef.current = window.setTimeout(() => {
setModalSuccess(false)
onClose?.()
}, SUCCESS_MODAL_MS)
}
useEffect(() => {
const keys = Object.keys(fieldErrors)
if (!keys.length && !globalError) {
return
}
formTopRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
const firstKey = keys[0]
if (firstKey) {
document
.querySelector(`[data-field-key="${firstKey}"]`)
?.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
}, [fieldErrors, globalError])
const updateField = (key, value) => {
setForm((prev) => ({ ...prev, [key]: value }))
setFieldErrors((prev) => {
if (!prev[key]) {
return prev
}
const next = { ...prev }
delete next[key]
return next
})
setGlobalError(null)
}
const validateClient = () => {
const next = {}
const has = (key) => config.fields.some((f) => f.key === key)
if (has('name') && !String(form.name ?? '').trim()) {
next.name = 'Название не может быть пустым'
}
if (has('alias') && !String(form.alias ?? '').trim()) {
next.alias = 'Alias не может быть пустым'
}
if (has('regionId') && (form.regionId === '' || form.regionId == null)) {
next.regionId = 'Укажите регион'
}
return next
}
const handleSave = async () => {
setFieldErrors({})
setGlobalError(null)
const clientErrors = validateClient()
if (Object.keys(clientErrors).length) {
setFieldErrors(clientErrors)
return
}
let data
try {
data = formToPayload(form, config.fields)
} catch (err) {
const key = err.fieldKey
if (key) {
setFieldErrors({ [key]: err.message || 'Ошибка в поле' })
} else {
setGlobalError(err.message || 'Ошибка в форме')
}
return
}
try {
if (isCreate) {
const created = await createItem(data).unwrap()
showSuccessModal(() => navigate(`/${config.slug}/edit/${created.id}`))
return
}
await updateItem({ id: Number(id), data }).unwrap()
showSuccessModal(() => refetch())
} catch (err) {
const { fieldErrors: nextFieldErrors, globalMessage } = parseSaveError(err)
setFieldErrors(nextFieldErrors)
if (globalMessage) {
setGlobalError(globalMessage)
} else if (!Object.keys(nextFieldErrors).length) {
setGlobalError('Не удалось сохранить запись')
} else {
setGlobalError(null)
}
console.error('Ошибка сохранения контента:', err, nextFieldErrors)
}
}
const handleDelete = async () => {
if (!window.confirm('Удалить запись?')) {
return
}
try {
await deleteItem(Number(id)).unwrap()
navigateBack()
} catch (err) {
const { fieldErrors: nextFieldErrors, globalMessage } = parseSaveError(err)
setFieldErrors(nextFieldErrors)
setGlobalError(globalMessage || 'Не удалось удалить запись')
console.error(err)
}
}
const fieldsWithErrors = config.fields.filter((field) => fieldErrors[field.key])
if (!isCreate && isLoading) {
return <LoadingComponent />
}
if (!isCreate && error) {
return <ErrorComponent />
}
if (!isCreate && !isLoading && !item) {
return <NotFindElement />
}
return (
<EditElementForm
navigateBack={navigateBack}
header={isCreate ? `Добавление: ${config.titleSingle}` : `Редактирование: ${config.titleSingle}`}
handleSave={handleSave}
handleDelete={isCreate ? null : handleDelete}
isAddSpecialist={isCreate}
>
<div ref={formTopRef} />
{globalError ? (
<div className="alert alert-danger" role="alert">
{globalError}
</div>
) : null}
{fieldsWithErrors.length > 0 ? (
<div className="alert alert-danger" role="alert">
<strong>Исправьте поля:</strong>
<ul className="mb-0 mt-2">
{fieldsWithErrors.map((field) => (
<li key={field.key}>
{field.label}: {fieldErrors[field.key]}
</li>
))}
</ul>
</div>
) : null}
{config.fields.map((field) => (
<ContentField
key={field.key}
field={field}
form={form}
updateField={updateField}
regions={regions}
fieldErrors={fieldErrors}
/>
))}
<Modal isOpen={isModalSuccess} title="Изменения внесены" hasButtons={false}>
<p className="mb-1">Изменения успешно внесены.</p>
</Modal>
</EditElementForm>
)
}
+210
View File
@@ -0,0 +1,210 @@
import { Fragment, useState, useRef } from 'react'
import { useNavigate } from 'react-router-dom'
import { useSelector } from 'react-redux'
import { selectRegions } from '../../store/slice/regionSlice'
import { useOutsideClick } from '../../hooks/useOutsideClick'
import { LoadingComponent } from '../../components/Placeholders/LoadingComponent'
import { ErrorComponent } from '../../components/Placeholders/ErrorComponent'
const normalizePagination = (response) => {
if (response.pagination) {
return response.pagination
}
if (response.meta) {
const page = response.meta.page ?? 1
const totalPages = response.meta.totalPages ?? 1
return {
total_pages: totalPages,
current_page: page,
has_previous_page: page > 1,
has_next_page: page < totalPages,
}
}
return {
total_pages: 1,
current_page: 1,
has_previous_page: false,
has_next_page: false,
}
}
const formatCell = (item, column, regions) => {
const value = item[column.key]
if (column.format === 'bool') {
return value ? 'Да' : 'Нет'
}
if (column.key === 'regionId' && regions[value]) {
return regions[value]
}
return value ?? ''
}
export const ContentListPage = ({ config, hooks }) => {
const [searchValue, setSearchValue] = useState('')
const [currentPage, setCurrentPage] = useState(1)
const [expandedId, setExpandedId] = useState(null)
const navigate = useNavigate()
const regions = useSelector(selectRegions)
const tableRef = useRef(null)
useOutsideClick(tableRef, () => setExpandedId(null))
const { data: response = {}, isFetching, error: queryError } = hooks.useListQuery({
search: searchValue,
page: currentPage,
perPage: 20,
})
const items = response.data ?? []
const pagination = normalizePagination(response)
const renderPagination = () => {
const total = pagination.total_pages || 1
const current = pagination.current_page || 1
const pages = new Set([1, total])
for (let page = current - 2; page <= current + 2; page += 1) {
if (page > 1 && page < total) {
pages.add(page)
}
}
const sorted = Array.from(pages).sort((a, b) => a - b)
const elements = []
let last = 0
sorted.forEach((page) => {
if (last && page - last > 1) {
elements.push(
<li key={`dots-${last}`} className="page-item disabled">
<span className="page-link"></span>
</li>,
)
}
elements.push(
<li key={page} className={`page-item ${page === current ? 'active' : ''}`}>
<button
type="button"
className="page-link"
onClick={() => page !== current && setCurrentPage(page)}
>
{page}
</button>
</li>,
)
last = page
})
return (
<nav>
<ul className="pagination justify-content-center">
<li className={`page-item ${!pagination.has_previous_page ? 'disabled' : ''}`}>
<button
type="button"
className="page-link"
onClick={() => pagination.has_previous_page && setCurrentPage(current - 1)}
>
&laquo;
</button>
</li>
{elements}
<li className={`page-item ${!pagination.has_next_page ? 'disabled' : ''}`}>
<button
type="button"
className="page-link"
onClick={() => pagination.has_next_page && setCurrentPage(current + 1)}
>
&raquo;
</button>
</li>
</ul>
</nav>
)
}
return (
<div className="container-fluid">
<h1 className="h3 mb-4 text-gray-800">{config.title}</h1>
<div
className="d-flex justify-content-between mb-3"
style={{ marginRight: '0.1rem', marginLeft: '0.1rem' }}
>
<div className="form-group align-self-end mr-3">
<input
type="button"
className="btn btn-outline-primary"
value="Добавить"
onClick={(e) => {
e.stopPropagation()
navigate(`/${config.slug}/create`)
}}
/>
</div>
<div className="form-group flex-grow-1">
<label>Поиск</label>
<input
type="text"
className="form-control"
value={searchValue}
onChange={(e) => {
setSearchValue(e.target.value)
setCurrentPage(1)
}}
/>
</div>
</div>
{isFetching ? (
<LoadingComponent />
) : queryError ? (
<ErrorComponent />
) : (
<>
<div className="table-responsive" ref={tableRef}>
<table className="table table-hover table-bordered">
<thead>
<tr>
{config.listColumns.map((col) => (
<th key={col.key}>{col.label}</th>
))}
</tr>
</thead>
<tbody>
{items.map((item) => (
<Fragment key={item.id}>
<tr
className={`cursor-pointer${expandedId === item.id ? ' table-success' : ''}`}
onClick={() => {
setExpandedId(expandedId === item.id ? null : item.id)
}}
>
{config.listColumns.map((col) => (
<td key={col.key}>{formatCell(item, col, regions)}</td>
))}
</tr>
{expandedId === item.id && (
<tr className="table-success">
<td colSpan={config.listColumns.length}>
<input
type="button"
className="btn btn-outline-primary"
value="Редактировать"
onClick={(e) => {
e.stopPropagation()
navigate(`/${config.slug}/edit/${item.id}`)
}}
/>
</td>
</tr>
)}
</Fragment>
))}
</tbody>
</table>
</div>
{renderPagination()}
</>
)}
</div>
)
}
+46
View File
@@ -0,0 +1,46 @@
import { CONTENT_RESOURCES } from '../../config/contentResources'
import { contentHooks } from '../../api/apiContent'
import { ContentListPage } from './ContentListPage'
import { ContentEditPage } from './ContentEditPage'
const bind = (resourceKey) => {
const config = CONTENT_RESOURCES[resourceKey]
const hooks = contentHooks[resourceKey]
return {
ListPage: () => <ContentListPage config={config} hooks={hooks} />,
EditPage: () => <ContentEditPage config={config} hooks={hooks} isCreate={false} />,
CreatePage: () => <ContentEditPage config={config} hooks={hooks} isCreate />,
}
}
const news = bind('news')
const promo = bind('promo')
const disease = bind('disease')
const medicalCenter = bind('medical-center')
const article = bind('article')
const siteServices = bind('site-services')
export const NewsListPage = news.ListPage
export const NewsEditPage = news.EditPage
export const NewsCreatePage = news.CreatePage
export const SitePromoListPage = promo.ListPage
export const SitePromoEditPage = promo.EditPage
export const SitePromoCreatePage = promo.CreatePage
export const DiseaseListPage = disease.ListPage
export const DiseaseEditPage = disease.EditPage
export const DiseaseCreatePage = disease.CreatePage
export const MedicalCenterListPage = medicalCenter.ListPage
export const MedicalCenterEditPage = medicalCenter.EditPage
export const MedicalCenterCreatePage = medicalCenter.CreatePage
export const ArticleListPage = article.ListPage
export const ArticleEditPage = article.EditPage
export const ArticleCreatePage = article.CreatePage
export const SiteServicesListPage = siteServices.ListPage
export const SiteServicesEditPage = siteServices.EditPage
export const SiteServicesCreatePage = siteServices.CreatePage
+1 -6
View File
@@ -7,12 +7,7 @@ import { API } from '../api/apiSlice';
import '../api/apiDepartment'
import '../api/apiFilial'
import '../api/apiSpecialist'
import '../api/apiNews'
import '../api/apiSitePromo'
import '../api/apiDisease'
import '../api/apiMedicalCenter'
import '../api/apiArticle'
import '../api/apiSiteServices'
import '../api/apiContent'
export const store = configureStore({
reducer: {
+28
View File
@@ -147,3 +147,31 @@ input[type="number"]:focus {
flex-direction: column;
}
}
// Ошибки валидации в формах контента (SB Admin перебивает .text-danger на label)
.content-field--has-error {
margin-bottom: 1rem;
padding: 0.75rem;
border: 1px solid #e74a3b;
border-radius: 0.35rem;
background-color: #fff5f5;
}
.content-field--has-error > label,
.content-field--has-error .content-field-error-label {
color: #e74a3b !important;
font-weight: 700;
}
.content-field--has-error .form-control,
.content-field--has-error .form-check-input {
border-color: #e74a3b !important;
}
.content-field-error-msg {
display: block;
margin-top: 0.35rem;
color: #e74a3b !important;
font-size: 0.875rem;
font-weight: 600;
}
+123
View File
@@ -0,0 +1,123 @@
const snakeToCamel = (key) =>
key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
const normalizePropertyPath = (path) => {
if (!path) {
return ''
}
const last = String(path).replace(/^\[|\]$/g, '').split('.').pop()
return snakeToCamel(last)
}
const addViolation = (fieldErrors, path, message) => {
const key = normalizePropertyPath(path)
if (!key || !message) {
return
}
if (!fieldErrors[key]) {
fieldErrors[key] = message
}
}
const extractFieldFromMessage = (message) => {
const text = String(message)
const patterns = [
/"(\w+)" attribute/,
/attribute "\w+" of type "\w+" expects [\w\\]+, "(\w+)" given/,
/at property path "([^"]+)"/,
/for property "(\w+)"/,
]
for (const pattern of patterns) {
const match = text.match(pattern)
if (match?.[1]) {
return normalizePropertyPath(match[1])
}
}
return null
}
/**
* Разбирает ошибку сохранения (клиент, RTK Query, Symfony validation / denormalize).
* @returns {{ fieldErrors: Record<string, string>, globalMessage: string | null }}
*/
export const parseSaveError = (err) => {
const fieldErrors = {}
const fieldKey = err?.fieldKey
if (fieldKey) {
fieldErrors[fieldKey] = err?.message || 'Ошибка поля'
return { fieldErrors, globalMessage: null }
}
let data = err?.data
if (data == null && err?.error && typeof err.error === 'object') {
data = err.error.data ?? err.error
}
if (typeof data === 'string') {
try {
data = JSON.parse(data)
} catch {
const fieldFromText = extractFieldFromMessage(data)
if (fieldFromText) {
fieldErrors[fieldFromText] = data
return { fieldErrors, globalMessage: null }
}
return { fieldErrors, globalMessage: data }
}
}
const collectViolations = (violations) => {
if (!Array.isArray(violations)) {
return
}
violations.forEach((violation) => {
addViolation(
fieldErrors,
violation.propertyPath ?? violation.property_path,
violation.message ?? violation.title,
)
})
}
if (Array.isArray(data)) {
collectViolations(data)
return {
fieldErrors,
globalMessage: Object.keys(fieldErrors).length ? null : 'Ошибка сохранения',
}
}
if (data && typeof data === 'object') {
collectViolations(data.violations ?? data['@violations'])
if (data.error) {
const message = String(data.error)
const fieldFromText = extractFieldFromMessage(message)
if (fieldFromText) {
const detail = message.replace(/^Ошибка десериализации:\s*/u, '').trim()
fieldErrors[fieldFromText] = detail
}
return {
fieldErrors,
globalMessage: Object.keys(fieldErrors).length ? null : message,
}
}
if (data.message && !data.error) {
return { fieldErrors, globalMessage: String(data.message) }
}
}
if (err instanceof Error && err.status == null) {
return { fieldErrors, globalMessage: err.message || 'Ошибка сохранения' }
}
return {
fieldErrors,
globalMessage:
(typeof err?.data === 'string' ? err.data : null) ||
err?.error ||
err?.message ||
'Ошибка сохранения',
}
}