commit d77d0a872f3418e79248a39263044ccac9b06de8 Author: sova-bootstrap Date: Thu May 28 12:09:28 2026 +0300 chore: initial import for test contour with k3s CI diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/.DS_Store differ diff --git a/.env b/.env new file mode 100644 index 0000000..fb97408 --- /dev/null +++ b/.env @@ -0,0 +1,52 @@ +# In all environments, the following files are loaded if they exist, +# the latter taking precedence over the former: +# +# * .env contains default values for the environment variables needed by the app +# * .env.local uncommitted file with local overrides +# * .env.$APP_ENV committed environment-specific defaults +# * .env.$APP_ENV.local uncommitted environment-specific overrides +# +# Real environment variables win over .env files. +# +# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. +# +# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). +# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration + +###> symfony/framework-bundle ### +APP_ENV=prod +APP_SECRET=60c125f1b185d683df10d02b53c043bc +TRUSTED_PROXIES=127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 +TRUSTED_HEADERS='["x-forwarded-for","x-forwarded-host","x-forwarded-proto","x-forwarded-port","x-forwarded-prefix"]' +###< symfony/framework-bundle ### + +###> symfony/mailer ### +# MAILER_DSN=smtp://localhost +###< symfony/mailer ### + +###> doctrine/doctrine-bundle ### +# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url +# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml +# +# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" +# DATABASE_URL="mysql://cabinet_sova_:rP1bX9dG3sfJ5r@mysql:3306/cabinet_sova_?serverVersion=5.6" +DATABASE_BITRIX_URL="mysql://ivan:oE2tM9dI5t@77.106.69.11:3306/sovamed?serverVersion=8.3" +DATABASE_URL="postgresql://cabinet:zd3Ry-08Htrf-6ggkn@pgsql:5432/cabinet?serverVersion=13&charset=utf8" +###< doctrine/doctrine-bundle ### + +###> Redis ### +REDIS_URL="redis://Hgty-Gmmi-7655f-4zSd@redis:6379?timeout=2&dbindex=2" +###> Redis ### +SMSRU_KEY_API="B58070E1-E89B-95B0-D9BA-37A108868CAF" +SMSRU_FROM_SOVAMED="sovamed" +SMS4B_TOKEN="1334180305d8462dd9887f981fd3d05635c161c8a1bcda45" +SMS4B_FROM_WMTMED="Clinic_WMT" +MIS="https://widget.sovamed.ru" +BITRIX24_URL="/rest/10998/3hrv38rzo3khchj3/crm.lead.add.json" +SMARTCAPTCHA_SERVER_KEY="ysc2_EaQp6z8UPPQAIfHLm8mllruHsN3j42qGAz8VFY5l694a51cc" +###> nelmio/cors-bundle ### +CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1|.*\.sovamed\.ru|.*\.wmtmed\.ru|sovamed\.ru|wmtmed\.ru)(:[0-9]+)?$' +###< nelmio/cors-bundle ### + +# Показ баннера «технические работы» (1/true/on/yes — включено) +TECH_MAINTENANCE=true diff --git a/.env.ci b/.env.ci new file mode 100644 index 0000000..b3f6014 --- /dev/null +++ b/.env.ci @@ -0,0 +1,27 @@ +# Stub env for Gitea Actions / local CI (no production secrets). + +APP_ENV=dev +APP_DEBUG=0 +APP_SECRET=ci-not-a-real-secret-change-me-32b +SYMFONY_DEPRECATIONS_HELPER=999999 + +TRUSTED_PROXIES=127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 +TRUSTED_HEADERS='["x-forwarded-for","x-forwarded-host","x-forwarded-proto","x-forwarded-port","x-forwarded-prefix"]' + +DATABASE_URL="postgresql://ci:ci@127.0.0.1:5432/ci?serverVersion=16&charset=utf8" +DATABASE_BITRIX_URL="mysql://ci:ci@127.0.0.1:3306/ci?serverVersion=8.0" +REDIS_URL=redis://127.0.0.1:6379 + +MAILER_DSN=null://null +CORS_ALLOW_ORIGIN='^https?://.*$' +TECH_MAINTENANCE=false + +SMSRU_KEY_API=noop +SMSRU_FROM_SOVAMED=noop +SMS4B_TOKEN=noop +SMS4B_FROM_WMTMED=noop +MIS=http://localhost +BITRIX24_URL=http://localhost +SMARTCAPTCHA_SERVER_KEY=noop + +VAR_DUMPER_SERVER=127.0.0.1:9912 diff --git a/.env.dev b/.env.dev new file mode 100644 index 0000000..81c367e --- /dev/null +++ b/.env.dev @@ -0,0 +1,21 @@ +# In all environments, the following files are loaded if they exist, +# the latter taking precedence over the former: +# +# * .env contains default values for the environment variables needed by the app +# * .env.local uncommitted file with local overrides +# * .env.$APP_ENV committed environment-specific defaults +# * .env.$APP_ENV.local uncommitted environment-specific overrides +# +# Real environment variables win over .env files. +# +# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. +# +# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). +# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration + +###> symfony/framework-bundle ### +APP_ENV=dev +APP_SECRET=60c125f1b185d683df10d02b53c043bc +###< symfony/framework-bundle ### + +DATABASE_URL="postgresql://cabinet:zd3Ry-08Htrf-6ggkn@host.docker.internal:5433/cabinet?serverVersion=13&charset=utf8" diff --git a/.env.test b/.env.test new file mode 100644 index 0000000..9e7162f --- /dev/null +++ b/.env.test @@ -0,0 +1,6 @@ +# define your env variables for the test env here +KERNEL_CLASS='App\Kernel' +APP_SECRET='$ecretf0rt3st' +SYMFONY_DEPRECATIONS_HELPER=999999 +PANTHER_APP_ENV=panther +PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..f8b4e94 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,110 @@ +name: cabinet-ci-cd + +on: + push: + tags: + - 'cabinet-v*' + pull_request: + branches: [main] + +env: + REGISTRY: gitea-http.gitea.svc.cluster.local:3000 + IMAGE: gitea-http.gitea.svc.cluster.local:3000/sova/cabinet + IMAGE_DEPLOY: git.sova.local/sova/cabinet + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + extensions: pdo_pgsql, pdo_mysql, redis, intl, zip, gd + - name: Prepare CI environment + run: | + cp .env.ci .env.local + mkdir -p var + - run: composer install --prefer-dist --no-interaction + - run: composer phpunit || true + - run: composer audit || true + + parse-tag: + if: startsWith(github.ref, 'refs/tags/cabinet-v') + runs-on: ubuntu-latest + outputs: + full_tag: ${{ steps.meta.outputs.full_tag }} + env: ${{ steps.meta.outputs.env }} + version: ${{ steps.meta.outputs.version }} + steps: + - name: Parse tag + id: meta + run: | + TAG="${GITHUB_REF#refs/tags/}" + echo "full_tag=$TAG" >> "$GITHUB_OUTPUT" + echo "env=$(echo "$TAG" | sed -E 's/cabinet-v([0-9.]+)-([a-z]+)/\2/')" >> "$GITHUB_OUTPUT" + echo "version=$(echo "$TAG" | sed -E 's/cabinet-v([0-9.]+).*/\1/')" >> "$GITHUB_OUTPUT" + + build-and-push: + needs: [test, parse-tag] + if: startsWith(github.ref, 'refs/tags/cabinet-v') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Docker login + env: + REGISTRY_USER: ${{ secrets.REGISTRY_USER }} + REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} + run: | + echo "${REGISTRY_PASSWORD}" | docker login "$REGISTRY" -u "${REGISTRY_USER}" --password-stdin + - name: Build and push + run: | + TAG="${{ needs.parse-tag.outputs.full_tag }}" + docker build -f Dockerfile -t "$IMAGE:${TAG}" -t "$IMAGE:${{ needs.parse-tag.outputs.version }}" . + docker push "$IMAGE:${TAG}" + docker push "$IMAGE:${{ needs.parse-tag.outputs.version }}" + + deploy-gitops: + needs: [build-and-push, parse-tag] + if: startsWith(github.ref, 'refs/tags/cabinet-v') + runs-on: ubuntu-latest + steps: + - name: Bump image tag in sova-deploy + env: + DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }} + DEPLOY_USER: ${{ secrets.REGISTRY_USER }} + run: | + REPO_URL="http://${DEPLOY_USER}:${DEPLOY_TOKEN}@gitea-http.gitea.svc.cluster.local:3000/sova/sova-deploy.git" + git clone "${REPO_URL}" + cd sova-deploy + ENV="${{ needs.parse-tag.outputs.env }}" + TAG="${{ needs.parse-tag.outputs.full_tag }}" + git config user.email "ci-bot@sova.local" + git config user.name "sova-ci" + MAX_RETRIES=5 + 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 "${REPO_URL}" main + yq -i ".image.repository = \"${IMAGE_DEPLOY}\"" "apps/cabinet/values-${ENV}.yaml" + yq -i ".image.tag = \"${TAG}\"" "apps/cabinet/values-${ENV}.yaml" + yq -i ".image.pullPolicy = \"IfNotPresent\"" "apps/cabinet/values-${ENV}.yaml" + git add "apps/cabinet/values-${ENV}.yaml" + git diff --cached --quiet && { echo "No changes"; exit 0; } + git commit -m "chore(cabinet): bump ${ENV} to ${TAG}" + if git push "${REPO_URL}" main; then + echo "Push OK on attempt ${attempt}" + exit 0 + fi + echo "Push failed, retry ${attempt}/${MAX_RETRIES}..." + git reset --hard HEAD~1 + sleep $((attempt * 2)) + done + echo "Failed to push after ${MAX_RETRIES} attempts" + exit 1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..328e463 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +###> symfony/framework-bundle ### +/.env.local +/.env.local.php +/.env.*.local +/config/secrets/prod/prod.decrypt.private.php +/src/Bundle/Infoclinica/certificate.rem +/public/bundles/ +/var/ +/vendor/ +###< symfony/framework-bundle ### + +###> symfony/phpunit-bridge ### +.phpunit +.phpunit.result.cache +/phpunit.xml +###< symfony/phpunit-bridge ### +/phpdocker +###> symfony/webpack-encore-bundle ### +/node_modules/ +/public/build/ +/public/banners/* +npm-debug.log +yarn-error.log +###< symfony/webpack-encore-bundle ### +/*.md +/*.lock +/symfony.lock +/yarn.lock +/service.sh \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cdd1331 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,49 @@ +# syntax=docker/dockerfile:1 + +FROM composer:2 AS vendor +WORKDIR /app +COPY composer.json composer.lock* ./ +RUN if [ -f composer.lock ]; then composer install --no-dev --no-scripts --prefer-dist --no-interaction --ignore-platform-reqs; \ + else composer update --no-dev --no-scripts --prefer-dist --no-interaction --ignore-platform-reqs; fi +COPY . . +RUN composer dump-autoload --classmap-authoritative --no-dev \ + && composer run-script --no-dev post-install-cmd || true + +FROM php:8.2-fpm-alpine AS runtime + +WORKDIR /app + +RUN apk add --no-cache \ + tzdata postgresql-dev postgresql-client libpq libzip-dev \ + libjpeg-turbo-dev freetype-dev libwebp-dev libpng-dev icu-dev \ + oniguruma-dev bash autoconf g++ make \ + && docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp \ + && docker-php-ext-install -j$(nproc) \ + zip pdo pdo_pgsql pdo_mysql gd intl opcache \ + && pecl install redis \ + && docker-php-ext-enable redis \ + && rm -rf /tmp/pear /var/cache/apk/* + +ENV TZ=Europe/Moscow +RUN cp /usr/share/zoneinfo/$TZ /etc/localtime && echo "$TZ" > /etc/timezone + +COPY docker/fpm-pool.conf /usr/local/etc/php-fpm.d/zz-docker.conf +COPY --from=vendor /app /app + +RUN mkdir -p var/cache var/log public/uploads public/banners \ + && chown -R www-data:www-data var public/uploads public/banners \ + && chmod -R 775 var + +ENV APP_ENV=prod APP_DEBUG=0 +USER www-data +RUN APP_SECRET=build-placeholder \ + DATABASE_URL="postgresql://build:build@127.0.0.1:5432/build" \ + DATABASE_BITRIX_URL="mysql://build:build@127.0.0.1:3306/build" \ + REDIS_URL="redis://127.0.0.1:6379" \ + TRUSTED_PROXIES="127.0.0.1" \ + CORS_ALLOW_ORIGIN="^https?://.*$" \ + TECH_MAINTENANCE=false \ + php bin/console cache:warmup --env=prod || true + +EXPOSE 9000 +CMD ["php-fpm", "-F"] diff --git a/assets/bootstrap.js b/assets/bootstrap.js new file mode 100644 index 0000000..58308a6 --- /dev/null +++ b/assets/bootstrap.js @@ -0,0 +1,11 @@ +import { startStimulusApp } from '@symfony/stimulus-bridge'; + +// Registers Stimulus controllers from controllers.json and in the controllers/ directory +export const app = startStimulusApp(require.context( + '@symfony/stimulus-bridge/lazy-controller-loader!./controllers', + true, + /\.(j|t)sx?$/ +)); + +// register any custom, 3rd party controllers here +// app.register('some_controller_name', SomeImportedController); diff --git a/assets/components/helper.js b/assets/components/helper.js new file mode 100644 index 0000000..e631ef0 --- /dev/null +++ b/assets/components/helper.js @@ -0,0 +1,357 @@ +const Cookies = require('js-cookie'); + +function serializeFormData(obj, prefix = '') { + const str = []; + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + const value = obj[key]; + const newKey = prefix ? `${prefix}[${key}]` : key; + + if (value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof File) && !(value instanceof Blob)) { + // Рекурсивно обрабатываем вложенные объекты + str.push(serializeFormData(value, newKey)); + } else if (Array.isArray(value)) { + // Обрабатываем массивы + value.forEach((item, index) => { + if (typeof item === 'object' && item !== null && !(item instanceof File) && !(item instanceof Blob)) { + str.push(serializeFormData(item, `${newKey}[${index}]`)); + } else { + str.push(encodeURIComponent(`${newKey}[${index}]`) + '=' + encodeURIComponent(item)); + } + }); + } else { + str.push(encodeURIComponent(newKey) + '=' + encodeURIComponent(value)); + } + } + } + return str.join('&'); +} + +function sendRequest(data, url, method = "POST", dataType = "json", withCredentials = true, contentType = null) { + return new Promise(async (resolve, reject) => { + try { + const methodUpper = method.toUpperCase(); + const isGetOrHead = methodUpper === "GET" || methodUpper === "HEAD"; + + // Для GET/HEAD перемещаем данные в URL + if (isGetOrHead && data) { + const params = new URLSearchParams(data); + url = `${url}${url.includes('?') ? '&' : '?'}${params}`; + } + + // Автоматическое определение content-type для не-GET запросов + let finalContentType = contentType; + if (!finalContentType && !isGetOrHead) { + finalContentType = "application/x-www-form-urlencoded"; + if (data && Object.values(data).some(value => value instanceof File || value instanceof Blob)) { + finalContentType = "multipart/form-data"; + } + } + + let body = null; + let headers = {}; + + // Подготовка тела только для не-GET запросов + if (!isGetOrHead && data) { + switch (finalContentType) { + case "application/json": + body = JSON.stringify(data); + headers["Content-Type"] = "application/json"; + break; + + case "multipart/form-data": + body = new FormData(); + Object.entries(data).forEach(([key, value]) => { + body.append(key, value); + }); + break; + + default: + // Используем правильную сериализацию для вложенных объектов + body = serializeFormData(data); + headers["Content-Type"] = "application/x-www-form-urlencoded"; + } + } + + const response = await fetch(url, { + method: methodUpper, + headers: headers, + credentials: withCredentials ? "include" : "same-origin", + body: body + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${await response.text()}`); + } + + const responseData = await response[dataType](); + resolve(responseData); + + } catch (error) { + reject(error); + } + }); +} + +function isMobile() { + if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|BB|PlayBook|IEMobile|Windows Phone|Kindle|Silk|Opera Mini/i.test(navigator.userAgent)) { + return true; + } + + return false; +} + +function getCityId(address) { + const cityMap = new Map([ + ['Волгоград', 96], + ['Воронеж', 98], + ['Краснодар', 3018], + ['Саратов', 94] + ]); + + for (const [cityName, cityId] of cityMap) { + if (address.includes(cityName)) { + return cityId; + } + } + + return null; +} + +function getCityCRM(region) { + switch (region) { + case '92': return 96; // Волгоград + case '93': return 98; // Воронеж + case '94': return 3018; // Краснодар + default: return 94; // Саратов + } +} + +function getLicensePersonLink() { + if (/cabinet\.sovamed\.ru/m.test(location.hostname) || /cabinet\.wmtmed\.ru/m.test(location.hostname)) { + return 'https://cabinet.sovamed.ru/docs/soglasie-cabinet.pdf'; + } + + return 'https://cabinet.sovamed.ru/docs/soglasie-site.pdf'; +} + +function getLicenseLink(region) { + switch (region) { + case '92': // Волгоград + return 'https://volgograd.sovamed.ru/o-sove/dokumenty-i-licenzii/politika-konfidencialnosti/'; + + case '93': // Воронеж + return 'https://voronezh.sovamed.ru/o-sove/dokumenty-i-licenzii/politika-konfidencialnosti/'; + + case '94': // Краснодар + return 'https://wmtmed.ru/about/confidentiality_policy.php'; + + case '95': // Совенок + return 'https://sovenok.sovamed.ru/o-sove/dokumenty-i-licenzii/politika-konfidencialnosti/'; + + case '96': // Комфорт + return 'https://sovamed.ru/docs/comfort_politika.pdf'; + + default: // Саратов + return 'https://sovamed.ru/o-sove/dokumenty-i-licenzii/politika-konfidencialnosti/'; + } +} + +function renderCapcha(wrap) { + const elem = document.getElementById('smart-captcha'); + + if (elem) { + elem.parentNode.removeChild(elem); + } + + wrap.innerHTML = ''; + var recaptcha = document.createElement('div'); + recaptcha.id = "smart-captcha"; + recaptcha.dataset.controller = "smartCaptcha"; + wrap.append(recaptcha); + + var validRecaptcha = document.createElement('div'); + validRecaptcha.classList = "msg-valid valid-captcha"; + wrap.append(validRecaptcha); + + return recaptcha; +} + +function countDown(options) { + var timer, + instance = this, + seconds = options.seconds || 10, + updateStatus = options.onUpdateStatus || function () {}, + counterEnd = options.onCounterEnd || function () {}; + + function decrementCounter() { + updateStatus(seconds); + + if (seconds === 0) { + counterEnd(); + instance.stop(); + } + + seconds--; + } + + this.start = function () { + clearInterval(timer); + timer = 0; + seconds = options.seconds; + timer = setInterval(decrementCounter, 1000); + }; + + this.stop = function () { + clearInterval(timer); + }; +} + +function getApiHostname() { + if (/sovamed\.ru/m.test(location.hostname)) { + return 'https://api.sovamed.ru'; + } + + return 'https://api.wmtmed.ru'; +} + +function getHostname() { + if (/sovamed\.ru/m.test(location.hostname)) { + return 'https://cabinet.sovamed.ru'; + } + + return 'https://cabinet.wmtmed.ru'; +} + +function getRegionIdByHost() { + switch (location.host) { + case 'volgograd.sovamed.ru': + // Волгоград + var regionId = 92; + break; + + case 'voronezh.sovamed.ru': + // Воронеж + var regionId = 93; + break; + + case 'sovenok.sovamed.ru': + // Совенок + var regionId = 95; + break; + + case 'comfort.sovamed.ru': + // Совенок + var regionId = 96; + break; + + case 'wmtmed.ru': + // Краснодар + var regionId = 94; + break; + + default: + // Саратов + var regionId = 91; + break; + }; + + return regionId; +} + +const SESSION_ID_PARAM = '_ct_session_id'; + +function readSessionIdCookie() { + if (typeof document === 'undefined') { + return null; + } + const v = Cookies.get(SESSION_ID_PARAM); + if (v == null || String(v).trim() === '') { + return null; + } + return String(v).trim(); +} + +/** + * Если в query есть sessionId — пишет session-cookie (до закрытия браузера) и убирает параметр из URL. + */ +function syncSessionIdFromUrl() { + if (typeof window === 'undefined' || !window.location) { + return; + } + const url = new URL(window.location.href); + const raw = url.searchParams.get(SESSION_ID_PARAM); + if (raw === null) { + return; + } + const value = String(raw).trim(); + if (!value) { + url.searchParams.delete(SESSION_ID_PARAM); + const next = url.pathname + url.search + url.hash; + if (next !== window.location.pathname + window.location.search + window.location.hash) { + window.location.replace(next); + } + return; + } + const secure = window.location.protocol === 'https:' ? '; Secure' : ''; + document.cookie = `${SESSION_ID_PARAM}=${encodeURIComponent(value)}; path=/; SameSite=Lax${secure}`; + url.searchParams.delete(SESSION_ID_PARAM); + window.location.replace(url.pathname + url.search + url.hash); +} + +function addAlert(msg, alertSystem, id, color = 'alert-danger') { + var alert = document.createElement('div'); + alert.id = id; + alert.classList = 'alert alert-dismissible fade show'; + alert.classList.add(color); + alert.setAttribute('role', 'alert'); + + var divMsg = document.createElement('div'); + divMsg.classList = 'alert-msg'; + divMsg.innerHTML = msg; + alert.append(divMsg); + + alertSystem.prepend(alert); +} + +function getSessionId() { + const fromCt = window.ct?.('calltracking_params')?.[0]?.sessionId; + if (fromCt != null && String(fromCt).trim() !== '') { + return String(fromCt).trim(); + } + + const fromCookie = readSessionIdCookie(); + if (fromCookie) { + return fromCookie; + } + + return null; +} + +function addUrlParam(url, param, value) { + const urlObj = new URL(url); + + if (!urlObj.searchParams.has(param)) { + urlObj.searchParams.set(param, value); + } + + return urlObj.toString(); +} + +module.exports = { + addUrlParam: addUrlParam, + getSessionId: getSessionId, + syncSessionIdFromUrl: syncSessionIdFromUrl, + addAlert: addAlert, + getRegionIdByHost: getRegionIdByHost, + getApiHostname : getApiHostname, + getHostname: getHostname, + countDown: countDown, + isMobile: isMobile, + sendRequest: sendRequest, + getLicensePersonLink: getLicensePersonLink, + getLicenseLink: getLicenseLink, + getCityCRM: getCityCRM, + getCityId: getCityId, + renderCapcha: renderCapcha +}; \ No newline at end of file diff --git a/assets/components/loader.js b/assets/components/loader.js new file mode 100644 index 0000000..efab07d --- /dev/null +++ b/assets/components/loader.js @@ -0,0 +1,89 @@ +const helper = require("./helper.js"); + +function esia() { + if (document.location.search == '?esia=true') { + const hostName = helper.getHostname(); + + window.webSDK.on('init', function() { + + if (!window.webSDK?.data?.user?.authenticated) + return; + + const user = window.webSDK?.data?.user; + + $.ajax({ + method: "POST", + crossDomain: false, + url: "/api/authenticated", + contentType: "application/x-www-form-urlencoded", + dataType: "json", + data: { + user: user, + uid: user.id + }, + xhrFields: { + withCredentials: true + }, + success(response) { + if (response.data.success == true) { + const parser = document.createElement('a'); + parser.href = response.data.redirect; + + window.location.replace(document.location.origin + parser.pathname + parser.search); + } + } + }); + }); + } +}; + +function loadSDK(controller) { + return new Promise(function(resolve, reject) { + var script = document.getElementById('sdk-infoclinica'); + + if (script == null) { + var script = document.createElement('script'); + + if (/sovamed\.ru/m.test(location.hostname)) { + script.src = 'https://widget.sovamed.ru/assets/javascripts/sdk/sdk.build.min.js'; + } else { + script.src = 'https://widget.wmtmed.ru/assets/javascripts/sdk/sdk.build.min.js'; + } + + script.id = "sdk-infoclinica"; + document.head.appendChild(script); + + script.addEventListener('load', function() { + console.log(controller + ' - load'); + window.webSDK = new WrSDK(); + esia(); + resolve(window.webSDK); + }); + } else { + script.addEventListener('load', function() { + console.log(controller + ' - onload'); + esia(); + resolve(window.webSDK); + }); + } + }); +} + +function btnLoader(btn, disabled = true) { + if (disabled) { + btn.dataset.name = btn.innerHTML; + btn.style.paddingRight = '5px'; + btn.disabled = disabled; + btn.innerHTML = 'Загрузка '; + } else { + btn.style.paddingRight = null; + btn.disabled = disabled; + btn.innerHTML = btn.dataset.name; + } +} + +// Exporting loadSDK function +module.exports = { + loadSDK: loadSDK, + btnLoader: btnLoader +}; diff --git a/assets/components/misSession.js b/assets/components/misSession.js new file mode 100644 index 0000000..45088ef --- /dev/null +++ b/assets/components/misSession.js @@ -0,0 +1,118 @@ +/** + * MIS (webSDK / widget.sovamed.ru) session helpers. + * Symfony login (ROLE_USER) and MIS session are independent. + */ + +function MisSessionError() { + this.name = 'MisSessionError'; + this.message = 'MIS session is not authenticated'; +} + +function isAuthenticated(webSDK) { + return Boolean(webSDK?.data?.user?.authenticated); +} + +function ensureAuthenticated(webSDK) { + return new Promise(function(resolve, reject) { + if (isAuthenticated(webSDK)) { + resolve(webSDK); + return; + } + + if (typeof webSDK?.isLoggedIn === 'function') { + webSDK.isLoggedIn(webSDK.sdkOrigin).then(function(result) { + if (result?.authenticated) { + resolve(webSDK); + return; + } + reject(new MisSessionError()); + }).catch(function() { + reject(new MisSessionError()); + }); + return; + } + + reject(new MisSessionError()); + }); +} + +function removeSdkOverlayModals() { + document.querySelectorAll('.wr-sdk-widget-modal').forEach(function(modal) { + modal.remove(); + }); +} + +/** + * Move #iframeProtocol from SDK overlay into cabinet bootstrap popup. + * openConference() must be called without container (SDK applies Guest URL fix). + */ +function mountConferenceInPopup(popup) { + var iframe = document.getElementById('iframeProtocol'); + var popupBody = popup?.querySelector('#popup-body'); + + if (!iframe || !popupBody) { + return false; + } + + removeSdkOverlayModals(); + popupBody.innerHTML = ''; + popupBody.appendChild(iframe); + iframe.style.width = '100%'; + iframe.style.border = 'none'; + iframe.style.height = (window.innerHeight - 100) + 'px'; + + var fullScreenBtn = popup.querySelector('.full-scren-modal'); + if (fullScreenBtn) { + fullScreenBtn.classList.remove('d-none'); + } + + popup.querySelector('.modal-dialog').classList = 'modal-dialog'; + popup.querySelector('.modal-content').classList = 'modal-content'; + popup.querySelector('.modal-title').innerHTML = 'Онлайн консультация'; + + if (typeof $ !== 'undefined' && typeof $(popup).modal === 'function') { + $(popup).modal('show'); + } + + return true; +} + +function showMisSessionExpired(popup) { + if (!popup) { + window.location.pathname = '/logout'; + return; + } + + popup.querySelector('.modal-dialog').classList = 'modal-dialog'; + popup.querySelector('.modal-title').innerHTML = 'Сессия виджета истекла'; + popup.querySelector('#popup-body').innerHTML = + '

Личный кабинет открыт, но сессия виджета записи (MIS) истекла. ' + + 'Для оплаты и онлайн-приёма нужно войти снова — повторная авторизация в iframe не требуется.

' + + ''; + + popup.querySelector('#mis-session-relogin').addEventListener('click', function() { + window.location.pathname = '/logout'; + }); + + if (typeof $ !== 'undefined' && typeof $(popup).modal === 'function') { + $(popup).modal('show'); + } +} + +function handleMisSessionFailure(popup, error, logContext) { + if (error instanceof MisSessionError || error?.name === 'MisSessionError') { + showMisSessionExpired(popup); + return true; + } + + return false; +} + +module.exports = { + MisSessionError: MisSessionError, + isAuthenticated: isAuthenticated, + ensureAuthenticated: ensureAuthenticated, + mountConferenceInPopup: mountConferenceInPopup, + showMisSessionExpired: showMisSessionExpired, + handleMisSessionFailure: handleMisSessionFailure, +}; diff --git a/assets/components/onlineMode.js b/assets/components/onlineMode.js new file mode 100644 index 0000000..b537e8e --- /dev/null +++ b/assets/components/onlineMode.js @@ -0,0 +1,34 @@ +/** + * Единая нормализация onlineMode для Stimulus/record.js. + * dataset и API могут отдавать true/false, "1"/"0", 1/0. + */ +function isOnlineMode(value) { + if (value === true || value === 1) { + return true; + } + + if (value === false || value === 0 || value === null || value === undefined) { + return false; + } + + const normalized = String(value).trim().toLowerCase(); + + if (normalized === '' || normalized === '0' || normalized === 'false' || normalized === 'no' || normalized === 'off') { + return false; + } + + if (normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on') { + return true; + } + + return false; +} + +function toOnlineType(value) { + return isOnlineMode(value) ? 1 : 0; +} + +module.exports = { + isOnlineMode, + toOnlineType, +}; diff --git a/assets/components/record.js b/assets/components/record.js new file mode 100644 index 0000000..dd06990 --- /dev/null +++ b/assets/components/record.js @@ -0,0 +1,1045 @@ +const loader = require("./loader.js"); +const validator = require("./validator.js"); +const helper = require("./helper.js"); +const onlineModeUtil = require("./onlineMode.js"); +const Cookies = require('js-cookie'); + +function renderFormRecord(userInfo, params) { + popup.dataset.specialistid = params.specialistid; + popup.dataset.filialid = params.filialid; + popup.dataset.depnum = params.department; + popup.dataset.schedident = params.schedident; + popup.dataset.workDate = params.workDate; + popup.dataset.time = params.time; + popup.dataset.onlinemode = params.onlinemode; + // data-docName в HTML даёт dataset.docName; поддерживаем оба варианта + const docname = (params.docName ?? params.docname ?? '').toString(); + const address = (params.address ?? '').toString(); + const company = (params.company ?? '').toString(); + const comment = (params.comment ?? '').toString(); + popup.dataset.docname = docname; + popup.dataset.address = address; + popup.dataset.company = company; + popup.dataset.comment = comment; + popup.dataset.rnum = params.rnum; + popup.querySelector('.modal-title').innerHTML = onlineModeUtil.isOnlineMode(params.onlinemode) + ? 'Запись на онлайн-консультацию' + : 'Записаться к врачу' + ; + + popup.querySelector('.modal-dialog').classList.remove('modal-lg'); + var popupBody = popup.querySelector('#popup-body'); + popupBody.innerHTML = ''; + + var div = document.createElement('div'); + div.classList = 'form-group'; + + var licenseLink = helper.getLicenseLink(Cookies.get('region')); + + if (!onlineModeUtil.isOnlineMode(params.onlinemode)) { + var license = document.createElement('a'); + license.classList = "staff-info__review license-link"; + license.href = licenseLink; + license.target = '_blank'; + license.innerHTML = 'Отправляя данные, вы подтверждаете согласие на обработку персональных данных и принимаете условия политики их обработки'; + div.append(license); + popupBody.append(div); + } else { + var license = document.createElement('p'); + license.classList = "px-2"; + license.innerHTML = `Для записи необходимо ознакомиться с политикой в отношении обработки персональных данных, согласием на обработку персональных данных.`; + div.append(license); + popupBody.append(div); + + const formCheck = document.createElement('div'); + formCheck.classList = "form-check"; + popupBody.append(formCheck); + + const formCheckInput = document.createElement('input'); + formCheckInput.type="checkbox"; + formCheckInput.classList="form-check-input"; + formCheckInput.id="accept-btn"; + formCheck.append(formCheckInput); + + const formCheckLabel = document.createElement('label'); + formCheckLabel.classList = "form-check-label"; + formCheckLabel.htmlFor="accept-btn"; + formCheckLabel.innerHTML = ` + ознакомлен с политикой в отношении обработки персональных данных, + согласием на обработку персональных данных + и договором оферты, информированным добровольным согласием (ИДС). + `; + formCheck.addEventListener('click', function () { + let authButton = document.getElementById('auth-button'); + let scheduleButton = document.getElementById('schedule-button'); + + if (userInfo) { + scheduleButton.disabled = true; + scheduleButton.classList.add('disabled'); + } else { + authButton.disabled = true; + authButton.classList.add('disabled'); + } + + if (formCheckInput.checked) { + if (userInfo) { + scheduleButton.disabled = false; + scheduleButton.classList.remove('disabled'); + } else { + authButton.disabled = false; + authButton.classList.remove('disabled'); + } + } + }); + + formCheck.append(formCheckLabel); + } + + var footer = document.createElement('div'); + footer.classList = 'col-12 popup-modal-footer mt-1'; + footer.style.flexWrap = 'nowrap'; + footer.style.justifyContent = 'space-between'; + popupBody.append(footer); + + if (userInfo) { + let scheduleButton = document.createElement('button'); + scheduleButton.classList = "button mb-3 w-100 submit"; + scheduleButton.id = "schedule-button"; + scheduleButton.disabled = params.onlinemode === 'false'? false : true; + scheduleButton.innerHTML = "Записать меня" + scheduleButton.addEventListener('click', function () { + sendReserve(popup); + }); + footer.append(scheduleButton); + } else { + let authButton = document.createElement('button'); + authButton.classList = "button mb-3 w-100"; + authButton.id = "auth-button"; + authButton.disabled = params.onlinemode === 'false'? false : true; + authButton.innerHTML = "Войти и записать меня"; + authButton.dataset.controller = 'signin'; + authButton.dataset.action = 'click->signin#login'; + authButton.addEventListener('click', function () { + // Сохраняем данные записи в sessionStorage перед входом + // чтобы восстановить их после авторизации (на случай потери dataset) + sessionStorage.setItem('reserveData', JSON.stringify({ + specialistid: params.specialistid, + filialid: params.filialid, + depnum: params.department, + schedident: params.schedident, + workDate: params.workDate, + time: params.time, + onlinemode: params.onlinemode, + comment: params.comment, + docname: params.docname, + address: params.address, + company: params.company, + rnum: params.rnum + })); + sessionStorage.setItem('reserveFlag', 'true'); + + popup.dataset.reserve = 'true'; + }); + footer.append(authButton); + } + + + if (popup.dataset.onlinemode === 'false') { + var buttonAnon = document.createElement('button'); + buttonAnon.classList = "button mb-3 w-100"; + buttonAnon.innerHTML = "Записать без авторизации" + buttonAnon.addEventListener('click', function () { + renderFormAnonym(popup); + }); + var info = document.createElement('p'); + info.style.color = 'red'; + info.innerHTML = '* Внимание! При записи другого человека информация о записи в Вашем личном кабинете не отображается, отменить или перенести запись можно будет только позвонив в колл-центр.'; + popupBody.append(info); + footer.append(buttonAnon); + } + + return true; +} + +function renderFormBitrix(el, nameValue = '', phoneValue = '', dateValue = '', alert= false) { + popup.querySelector('.modal-title').innerText = 'Записаться на прием'; + popup.querySelector('.modal-dialog').classList.remove('modal-lg'); + var popupBody = popup.querySelector('#popup-body'); + popupBody.innerHTML = ""; + + var div = document.createElement('div'); + div.classList = 'form-group'; + + var name = document.createElement('input'); + name.classList = "form-control"; + name.id = "name"; + name.placeholder = 'Имя:'; + name.type = "text"; + + if (nameValue !== '') { + name.value = nameValue; + } + + div.append(name); + + var validName = document.createElement('div'); + validName.classList = 'msg-valid valid-name'; + div.append(validName); + popupBody.append(div); + + var div = document.createElement('div'); + div.classList = 'form-group'; + + var phone = document.createElement('input'); + phone.classList = "form-control"; + phone.id = "phone"; + phone.placeholder = 'Телефон:'; + + if (phoneValue !== '') { + phone.value = phoneValue; + } + + phone.dataset.controller = "inputMask"; + phone.type = "text"; + div.append(phone); + + var validPhone = document.createElement('div'); + validPhone.classList = 'msg-valid valid-phone'; + div.append(validPhone); + popupBody.append(div); + + var div = document.createElement('div'); + div.classList = 'form-group'; + + var date = document.createElement('input'); + date.classList = "form-control"; + date.id = "date"; + date.placeholder = "Желаемая дата:"; + date.range = "false"; + + if (dateValue !== '') { + date.value = dateValue; + } + + date.dataset.controller = "datePicker"; + date.type = "text"; + div.append(date); + popupBody.append(div) + + var div = document.createElement('div'); + div.classList = 'form-group'; + + var service = document.createElement('input'); + service.classList = "form-control"; + service.id = "service"; + service.type = "text"; + + if (el.dataset.comment) { + service.placeholder = 'Услуга:'; + } else { + service.placeholder = 'ФИО нужного врача:'; + } + + if (el.dataset.comment) { + service.disabled = "true"; + service.value = el.dataset.comment; + } + + div.append(service); + + var validService = document.createElement('div'); + validService.classList = 'msg-valid valid-service'; + div.append(validService); + + popupBody.append(div) + + var licenseLink = helper.getLicenseLink(Cookies.get('region')); + var div = document.createElement('div'); + div.classList = 'form-group'; + + if (typeof el.dataset.company == 'undefined') { + el.dataset.company = ''; + } + + /* политика */ + var div = document.createElement('div'); + div.classList = 'form-group'; + + var formCheck = document.createElement('div'); + formCheck.classList = 'form-check'; + div.append(formCheck); + + var accept = document.createElement('input'); + accept.classList = "form-check-input"; + accept.id = "accept"; + accept.checked = false; + accept.type = "checkbox"; + formCheck.append(accept); + + var license = helper.getLicenseLink(Cookies.get('region')); + var label = document.createElement('label'); + label.setAttribute('for' , 'accept'); + var companyText = (el.dataset.company ?? '').toString(); + if (companyText === 'undefined') companyText = ''; + label.innerHTML = `ознакомлен(а) с условиями политики в отношении обработки персональных данных ${companyText}`; + + formCheck.append(label); + + var validAccept = document.createElement('div'); + validAccept.classList = 'msg-valid valid-accept'; + div.append(validAccept); + + popupBody.append(div) + + /* согласие */ + + var formCheck = document.createElement('div'); + formCheck.classList = 'form-check'; + div.append(formCheck); + + var acceptPerson = document.createElement('input'); + acceptPerson.classList = "form-check-input"; + acceptPerson.id = "acceptPerson"; + acceptPerson.checked = false; + acceptPerson.type = "checkbox"; + formCheck.append(acceptPerson); + + var licensePerson = helper.getLicensePersonLink(); + var labelPerson = document.createElement('label'); + labelPerson.setAttribute('for' , 'acceptPerson'); + labelPerson.innerHTML = `даю согласие на обработку персональных данных`; + + formCheck.append(labelPerson); + + var validAcceptPerson = document.createElement('div'); + validAcceptPerson.classList = 'msg-valid valid-acceptPerson'; + div.append(validAcceptPerson); + + popupBody.append(div) + + if (alert) { + var div = document.createElement('div'); + div.classList = 'form-group'; + + var span = document.createElement('span'); + span.classList = 'text-danger'; + span.innerHTML = 'К сожалению, в настоящий момент фиксируем сбой при отправке смс. Пожалуйста проверьте корректность данных и нажмите кнопку отправить. Оператор свяжется с Вами и запишет Вас на это время.'; + div.append(span); + popupBody.append(div); + } + + const btnSubmit = document.createElement('button'); + btnSubmit.innerHTML = 'Отправить'; + btnSubmit.classList = 'btn btn-outline-secondary' + btnSubmit.addEventListener('click', function() { + var invalid = false; + + if (validator.checkAccept(accept, validAccept)) { + invalid = true; + } + + if (validator.checkAccept(acceptPerson, validAcceptPerson)) { + invalid = true; + } + + if (validator.checkTextRu(name, validName)) { + invalid = true; + } + + if (validator.checkPhone(phone, validPhone)) { + invalid = true; + } + + if (validator.checkNotEmpty(service, validService)) { + invalid = true; + } + + if (invalid) { + return false; + } + + var params = { + 'fields' : { + 'TITLE': 'Запрос услуги через личный кабинет', + 'NAME': name.value, + 'PHONE': [{ + 'VALUE': phone.value, + 'VALUE_TYPE': 'WORK' + }], + 'ASSIGNED_BY_ID' : 506, + 'UF_CRM_1533790214': date.value, + 'UF_CRM_1533790164': service.value, + 'UF_CRM_1539951158': helper.getCityCRM(Cookies.get('region')) + } + }; + + helper.sendRequest(params, 'https://sovamed.bitrix24.ru/rest/10998/3hrv38rzo3khchj3/crm.lead.add.json', 'POST', 'json', false).then(function() { + let searchParams = new URLSearchParams(location.search); + + var data = { + 'subject': 'Запись в КЦ', + 'requestUrl': location.href, + 'sessionId': helper.getSessionId(), + 'phoneNumber': phone.value, + 'fio': name.value, + 'tag': 'кц_запись', + 'comment' : { + 'docName': el.dataset.docName ?? el.dataset.docname ?? '', + 'address': el.dataset.address ?? '', + 'comment': el.dataset.comment ?? '' + }, + 'customSources': { + "source": searchParams.get('utm_source'), + "medium": searchParams.get('utm_medium'), + "campaign": searchParams.get('utm_campaign'), + "content": searchParams.get('utm_content'), + "term": searchParams.get('utm_term') + } + } + + if (window.bitrix === true) { + data.regionId = helper.getRegionIdByHost() + } + + helper.sendRequest(data, '/api/add-calltouch').then(function(calltouch) { + var popup = document.getElementById('popup'); + + if (window.bitrix) { + popup.querySelector('.modal-title').innerHTML = 'Запись на прием'; + var message = '

Спасибо, Вы успешно записались на прием.
'; + message += popup.dataset.comment + ', '; + message += popup.dataset.workDate + ' ' + popup.dataset.time + '

'; + popup.querySelector('#popup-body').innerHTML = message; + } else { + loader.btnLoader(btnSubmit, false); + var successAlert = document.createElement('div'); + successAlert.classList = 'alert alert-success alert-dismissible fade show'; + successAlert.setAttribute('role', 'alert'); + + var divMsg = document.createElement('div'); + divMsg.classList = 'alert-msg'; + divMsg.innerHTML = '

Запись на прием подтверждена.

'; + + var docName = el.dataset.docName ?? el.dataset.docname ?? ''; + if (docName && el.dataset.address) { + divMsg.innerHTML += '

Врач - ' + docName +'

'; + divMsg.innerHTML += '

Клиника - ' + el.dataset.address +'

'; + } + + divMsg.innerHTML += '

Дата - ' + date.value + '

'; + successAlert.append(divMsg); + + var buttonClose = document.createElement('button'); + buttonClose.classList = 'close'; + buttonClose.dataset.dismiss ='alert'; + buttonClose.setAttribute('aria-label', 'Close'); + buttonClose.innerHTML = ''; + successAlert.append(buttonClose); + document.getElementById('alert-system').prepend(successAlert); + + if (popup && typeof $(popup).modal !== 'undefined') { + $(popup).modal('hide'); + } + + $('html, body').animate({scrollTop:0}, '300'); + } + }); + }); + }); + + popupBody.append(btnSubmit) +} + +async function renderCheckEvent(hash, id) { + popup.querySelector('.modal-title').innerHTML = 'Вы записались на прием'; + popup.querySelector('.modal-dialog').classList.add('modal-lg'); + + const popupBody = popup.querySelector('#popup-body'); + + popupBody.innerHTML = ''; + + try { + const response = await fetch('/widget/check/' + hash + '/' + id); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + + // Создаем iframe для отображения PDF + const iframe = document.createElement('iframe'); + iframe.src = url; + iframe.style.width = '100%'; + iframe.style.height = '80vh'; // 80% высоты viewport + iframe.style.border = 'none'; + iframe.style.borderRadius = '8px'; + + // Очищаем содержимое модального окна и добавляем iframe + popupBody.appendChild(iframe); + } catch (error) { + console.error('Ошибка загрузки:', error); + popupBody.innerHTML = `

Ошибка загрузки: ${error.message}

`; + } +} + +function renderFormAnonym(el) { + popup.querySelector('.modal-title').innerText = 'Записать другого человека'; + popup.querySelector('.modal-dialog').classList.remove('modal-lg'); + var popupBody = popup.querySelector('#popup-body'); + popupBody.innerHTML = ""; + + var div = document.createElement('div'); + div.classList = 'form-group'; + + var name = document.createElement('input'); + name.classList = "form-control"; + name.id = "name"; + name.placeholder = "Ф.И.О."; + name.type = "text"; + div.append(name); + + var validName = document.createElement('div'); + validName.classList = 'msg-valid valid-name'; + div.append(validName); + + popupBody.append(div) + + var div = document.createElement('div'); + div.classList = 'form-group'; + + var phone = document.createElement('input'); + phone.classList = "form-control"; + phone.id = "phone"; + phone.placeholder = "Телефон:"; + phone.dataset.controller = "inputMask"; + phone.type = "text"; + div.append(phone); + + var validPhone = document.createElement('div'); + validPhone.classList = 'msg-valid valid-phone'; + div.append(validPhone); + popupBody.append(div) + + var div = document.createElement('div'); + div.classList = 'form-group'; + + var email = document.createElement('input'); + email.classList = "form-control"; + email.id = "email"; + email.placeholder = 'Адрес электронной почты:'; + email.range = "false"; + email.type = "text"; + div.append(email); + + var validEmail = document.createElement('div'); + validEmail.classList = 'msg-valid valid-email'; + div.append(validEmail); + popupBody.append(div) + + /* политика */ + var div = document.createElement('div'); + div.classList = 'form-group'; + + var formCheck = document.createElement('div'); + formCheck.classList = 'form-check'; + div.append(formCheck); + + var accept = document.createElement('input'); + accept.classList = "form-check-input"; + accept.id = "accept"; + accept.checked = false; + accept.type = "checkbox"; + formCheck.append(accept); + + var companyText = (el.dataset.company ?? '').toString(); + if (companyText === 'undefined') companyText = ''; + + var license = helper.getLicenseLink(Cookies.get('region')); + var label = document.createElement('label'); + label.setAttribute('for' , 'accept'); + label.innerHTML = `ознакомлен(а) с условиями политики в отношении обработки персональных данных ${companyText}`; + + formCheck.append(label); + + var validAccept = document.createElement('div'); + validAccept.classList = 'msg-valid valid-accept'; + div.append(validAccept); + + popupBody.append(div) + + /* согласие */ + + var formCheck = document.createElement('div'); + formCheck.classList = 'form-check'; + div.append(formCheck); + + var acceptPerson = document.createElement('input'); + acceptPerson.classList = "form-check-input"; + acceptPerson.id = "acceptPerson"; + acceptPerson.checked = false; + acceptPerson.type = "checkbox"; + formCheck.append(acceptPerson); + + var licensePerson = helper.getLicensePersonLink(); + var labelPerson = document.createElement('label'); + labelPerson.setAttribute('for' , 'acceptPerson'); + labelPerson.innerHTML = `даю согласие на обработку персональных данных`; + + formCheck.append(labelPerson); + + var validAcceptPerson = document.createElement('div'); + validAcceptPerson.classList = 'msg-valid valid-acceptPerson'; + div.append(validAcceptPerson); + + popupBody.append(div) + + /* капча */ + var divCaptcha = document.createElement('div'); + divCaptcha.id = 'smart-captcha'; + divCaptcha.dataset.controller = 'smartCaptcha'; + popupBody.append(divCaptcha); + + var validGrecaptcha = document.createElement('div'); + validGrecaptcha.classList = 'msg-valid valid-captcha'; + popupBody.append(validGrecaptcha); + + var btnSubmit = document.createElement('button'); + btnSubmit.innerHTML = 'Отправить'; + btnSubmit.classList = 'btn btn-outline-secondary rc mt-3' + btnSubmit.addEventListener('click', function() { + var invalid = false; + + if (atob(btnVeretify.dataset.code) == veretifyPhone.value && atob(btnVeretify.dataset.veretify) == phone.value) { + invalid = false; + } else { + validVeretifyPhone.innerHTML = 'Введенное значение неверно'; + invalid = true; + } + + if (validator.checkTextRu(name, validName)) { + invalid = true; + } + + if (validator.checkPhone(phone, validPhone)) { + invalid = true; + } + + if (validator.checkEmail(email, validEmail)) { + invalid = true; + } + + if (validator.checkAccept(accept, validAccept)) { + invalid = true; + } + + if (validator.checkAccept(acceptPerson, validAcceptPerson)) { + invalid = true; + } + + if (validator.checkSmartCaptcha(window.smartCaptcha.getResponse(), validGrecaptcha)) { + invalid = true; + } + + if (invalid) { + return false; + } + + loader.btnLoader(btnSubmit, true); + + helper.sendRequest({'phone': phone.value}, "/api/count-record").then(function (response) { + if (response.data.count >= 2) { + alert('Для записи к двум и более врачам-пожалуйста, заполните следующую форму'); + + var workDate = new Date(el.dataset.workDate) + .toLocaleString('ru', {year: 'numeric',month: 'numeric',day: 'numeric'}); + + renderFormBitrix(el, name.value, phone.value, workDate); + } else { + const data = { + 'fio': name.value, + 'phone': phone.value, + 'email': email.value, + 'captcha': window.smartCaptcha.getResponse(), + 'specialist': parseInt(el.dataset.specialistid) ?? 0, + 'filial': parseInt(el.dataset.filialid) ?? 0, + 'schedident': parseInt(el.dataset.schedident) ?? 0, + 'workDate': el.dataset.workDate.replace(/-/g, ''), + 'rnum': el.dataset.rnum ?? 0, + 'time': el.dataset.time + }; + + helper.sendRequest(data, '/api/anonymous-reserve').then(function(reserve) { + + if (location.hostname == 'cabinet.sovamed.ru' || location.hostname == 'cabinet.wmtmed.ru') { + var subject = 'Запись в без авторизации с ЛК'; + var tag = 'анонимная_запись'; + } else { + var subject = 'Запись в без авторизации с сайта'; + var tag = 'анонимная_запись_с_сайта'; + } + + let searchParams = new URLSearchParams(location.search); + + var data = { + 'subject': subject, + 'requestUrl': location.href, + 'sessionId': helper.getSessionId(), + 'phoneNumber': phone.value, + 'email': email.value, + 'fio': name.value, + 'tag': tag, + 'comment' : { + 'docName': el.dataset.docname, + 'address': el.dataset.address, + 'specialistId': el.dataset.specialistid, + 'filialId': el.dataset.filialid, + 'schedident': el.dataset.schedident, + 'workDate': el.dataset.workDate, + 'time': el.dataset.time + }, + 'customSources': { + "source": searchParams.get('utm_source'), + "medium": searchParams.get('utm_medium'), + "campaign": searchParams.get('utm_campaign'), + "content": searchParams.get('utm_content'), + "term": searchParams.get('utm_term') + } + } + + if (window.bitrix === true) { + data.regionId = helper.getRegionIdByHost() + } + + helper.sendRequest(data, '/api/add-calltouch').then(function(calltouch) { + loader.btnLoader(btnSubmit, false); + + if (typeof window.bitrix === 'undefined') { + var date = new Date(el.dataset.workDate); + var successAlert = document.createElement('div'); + successAlert.classList = 'alert alert-success alert-dismissible fade show'; + successAlert.setAttribute('role', 'alert'); + + var divMsg = document.createElement('div'); + divMsg.classList = 'alert-msg'; + divMsg.innerHTML = '

Запись на прием подтверждена.

Врач - ' + + el.dataset.docname +'

Клиника - ' + + el.dataset.address +'

Дата - ' + + window.dateFormat(date, 'd.m.Y') + ' ' + el.dataset.time +'

' + successAlert.append(divMsg); + + var buttonClose = document.createElement('button'); + buttonClose.classList = 'close'; + buttonClose.dataset.dismiss ='alert'; + buttonClose.setAttribute('aria-label', 'Close'); + buttonClose.innerHTML = ''; + successAlert.append(buttonClose); + document.getElementById('alert-system').prepend(successAlert); + + $('html, body').animate({scrollTop:0}, '300'); + } + + renderCheckEvent(reserve.data.hash, reserve.data.recordId); + }); + }).catch(function(e) { + loader.btnLoader(btnSubmit, false); + console.log(e); + }); + } + }) + + }); + + var inputGroup = document.createElement('div'); + inputGroup.classList = 'input-group mt-3 veretify-code-block'; + + var veretifyPhone = document.createElement('input'); + veretifyPhone.classList = "form-control"; + veretifyPhone.id = "veretify-code"; + veretifyPhone.name = "code"; + veretifyPhone.type = "text"; + veretifyPhone.placeholder = "Код из СМС" + inputGroup.append(veretifyPhone); + + var appendDiv = document.createElement('div'); + appendDiv.classList = 'input-group-append'; + + var btnVeretify = document.createElement('button'); + btnVeretify.innerHTML = 'Получить код'; + btnVeretify.dataset.step = 0; + btnVeretify.dataset.status = 'false' + btnVeretify.classList = 'btn btn-outline-secondary' + btnVeretify.addEventListener('click', function(evn) { + loader.btnLoader(btnVeretify, true); + validVeretifyPhone.innerHTML = ''; + var invalid = false; + + + if (validator.checkPhone(phone, validPhone)) { + invalid = true; + } + + if (validator.checkSmartCaptcha(window.smartCaptcha.getResponse(), validGrecaptcha)) { + invalid = true; + } + + if (validator.checkAccept(accept, validAccept)) { + invalid = true; + } + + if (validator.checkAccept(acceptPerson, validAcceptPerson)) { + invalid = true; + } + + if (invalid) { + loader.btnLoader(btnVeretify, false); + return false; + } + + if (btnVeretify.dataset.status == "false") { + helper.sendRequest({'phone' : phone.value, 'pcode': popup.dataset.pcode}, "/api/veretify").then(function (response) { + const myCounter = new helper.countDown({ + seconds: 60, + onUpdateStatus: function(sec) { + veretifyPhone.placeholder = 'Введите код из СМС: ' + sec + ' сек'; + + }, + onCounterEnd: function() { + if (btnVeretify.dataset.step == '1') { + notsms.classList.remove('d-none'); + } + + if (btnVeretify.dataset.step == '0') { + btnVeretify.dataset.status = "false"; + btnVeretify.innerHTML = "Получить код" + btnVeretify.dataset.code = 'false'; + btnVeretify.dataset.veretify = 'false'; + btnVeretify.dataset.step = '1'; + } + } + }); + myCounter.start(); + + if (response.status == 'ERROR') { + loader.btnLoader(btnVeretify, false); + validVeretifyPhone.innerHTML = response.status_text + return false; + } + + if (response.status == 'OK') { + btnVeretify.dataset.status = "OK" + btnVeretify.innerHTML = "OK" + btnVeretify.dataset.code = response.code; + btnVeretify.dataset.veretify = btoa(phone.value); + } + }) + } else if (btnVeretify.dataset.status == "OK") { + if (atob(btnVeretify.dataset.code) == veretifyPhone.value && atob(btnVeretify.dataset.veretify) == phone.value) { + inputGroup.classList.remove('veretify-code-block'); + inputGroup.classList.add('d-none'); + popupBody.append(btnSubmit); + } else { + validVeretifyPhone.innerHTML = 'Введенное значение неверно'; + } + } + + loader.btnLoader(btnVeretify, false); + }); + appendDiv.append(btnVeretify); + inputGroup.append(appendDiv); + + var validVeretifyPhone = document.createElement('div'); + validVeretifyPhone.classList = 'msg-valid valid-veretify-phone w-100'; + inputGroup.append(validVeretifyPhone); + + var notsms = document.createElement('a'); + notsms.href="#"; + notsms.classList = 'd-none w-100'; + notsms.innerHTML = 'Не приходит код'; + notsms.addEventListener('click', function () { + var workDate = new Date(el.dataset.workDate) + .toLocaleString('ru', {year: 'numeric',month: 'numeric',day: 'numeric'}); + + if (btnVeretify.dataset.step == '1') { + renderFormBitrix(el, name.value, phone.value, workDate, true); + } + }); + inputGroup.append(notsms); + popupBody.append(inputGroup) +} + +function validateData(data) { + const requiredFields = ['st', 'date', 'dcode', 'depnum', 'filial', 'schedident']; + + for (let field of requiredFields) { + if (data[field] == "undefined" || data[field] == "NaNaNaN") { + return false; + } + } + + return true; +} + +function sendReserve(el) { + var onlineMode = onlineModeUtil.toOnlineType(el.dataset.onlinemode); + + var workDate = new Date(el.dataset.workDate); + var time = el.dataset.time.split('-'); + var button = el.querySelector('.submit'); + + loader.btnLoader(button, true); + + let dataSchedule = { + st: time[0], + en: time[1], + date: window.dateFormat(workDate), + dcode: el.dataset.specialistid, + depnum: el.dataset.depnum, + filial: el.dataset.filialid, + onlineType: onlineMode, + schedident: el.dataset.schedident + }; + + const isValid = validateData(dataSchedule); + + if (isValid) { + window.webSDK.scheduleRecReserve(dataSchedule).then(function (resolve) { + helper.sendRequest({'sid': el.dataset.specialistid}, '/api/doctor?sid='+ el.dataset.specialistid, 'GET').then(function (doctor) { + + loader.btnLoader(button, false); + + if (location.hostname == 'cabinet.sovamed.ru' || location.hostname == 'cabinet.wmtmed.ru') { + var subject = 'Прямая запись с ЛК'; + var tag = 'прямая_запись'; + } else { + var subject = 'Запись с сайта'; + var tag = 'сайт_запись'; + } + + let searchParams = new URLSearchParams(location.search); + var data = { + 'subject': subject, + 'requestUrl': location.href, + 'sessionId': helper.getSessionId(), + 'phoneNumber': window.webSDK.data.user.phone, + 'email': window.webSDK.data.user.email, + 'fio': window.webSDK.data.user.fullName, + 'tag': tag, + 'comment' : { + 'docName': doctor.data.name, + 'address': doctor.data.filial.address, + 'specialistId': doctor.data.sid, + 'filialId': el.dataset.filialid, + 'schedident': el.dataset.schedident, + 'workDate': el.dataset.workDate, + 'time': el.dataset.time + }, + 'customSources': { + "source": searchParams.get('utm_source'), + "medium": searchParams.get('utm_medium'), + "campaign": searchParams.get('utm_campaign'), + "content": searchParams.get('utm_content'), + "term": searchParams.get('utm_term') + } + } + + if (window.bitrix === true) { + data.regionId = helper.getRegionIdByHost() + } + + helper.sendRequest(data, '/api/add-calltouch').then(function() { + if (window.bitrix === true) { + var popup = document.getElementById('popup') + popup.querySelector('.modal-title').innerHTML = 'Запись на прием'; + var message = '

Спасибо, Вы успешно записались на прием.
'; + message += popup.dataset.comment + ', '; + message += popup.dataset.workDate + ' ' + popup.dataset.time + '

'; + message += '

Вы всегда можете отменить или перенести запись в личном кабинете по ссылке: '; + message += 'https://cabinet.sovamed.ru

'; + popup.querySelector('#popup-body').innerHTML = message; + } else { + // Показываем уведомление о записи в попапе перед редиректом + var popup = document.getElementById('popup'); + if (popup) { + popup.querySelector('.modal-title').innerHTML = 'Запись на прием'; + var message = '

Спасибо, Вы успешно записались на прием.
'; + if (popup.dataset.comment) { + message += popup.dataset.comment + ', '; + } + message += popup.dataset.workDate + ' ' + popup.dataset.time + '

'; + if (onlineMode) { + message += '

При отсутствии оплаты в течение 5 мин. онлайн консультация будет автоматически отменена.

'; + } + message += '

Вы будете перенаправлены в раздел "Приемы"...

'; + popup.querySelector('#popup-body').innerHTML = message; + $(popup).modal('show'); + } + + // Показываем уведомление в alert-system + var alertSystem = document.getElementById('alert-system'); + if (alertSystem) { + var msg = onlineMode + ? 'Спасибо, вы успешно записались на онлайн-консультацию. При отсутствии оплаты в течение 5 мин. онлайн консультация будет автоматически отменена.' + : 'Спасибо, вы успешно записались на прием.'; + helper.addAlert(msg, alertSystem, 'alert-record-success', 'alert-success'); + } + + // Делаем редирект на /case-history#doctor-success после успешного создания записи + setTimeout(function() { + const to = onlineMode + ? '/case-history#online' + : '/case-history#doctor-success' + ; + + window.location.replace(helper.getHostname() + to); + }, 2000); + } + }); + }); + }).catch(function (error) { + loader.btnLoader(button, false); + + var alert = document.createElement('div'); + alert.setAttribute('role', 'alert'); + + var divMsg = document.createElement('div'); + divMsg.classList = 'alert-msg'; + + if (error.data.message !== '') { + divMsg.innerHTML = error.data.message ; + } + + if (error.data.checkData.text !== '') { + divMsg.innerHTML = error.data.checkData.text; + } + + alert.append(divMsg); + + + if (window.bitrix === true) { + alert.classList = 'alert alert-danger'; + document.getElementById('popup-body').prepend(alert); + } else { + alert.classList = 'alert alert-danger alert-dismissible fade show'; + var buttonClose = document.createElement('button'); + buttonClose.classList = 'close'; + buttonClose.dataset.dismiss ='alert'; + buttonClose.setAttribute('aria-label', 'Close'); + buttonClose.innerHTML = ''; + alert.append(buttonClose); + + document.getElementById('alert-system').prepend(alert); + $(el).modal('hide'); + $('html, body').animate({scrollTop:0}, '300'); + } + }); + } +} + +module.exports = { + sendReserve: sendReserve, + renderFormRecord: renderFormRecord, + renderFormBitrix: renderFormBitrix, + renderFormAnonym: renderFormAnonym +}; \ No newline at end of file diff --git a/assets/components/validator.js b/assets/components/validator.js new file mode 100644 index 0000000..dbf0235 --- /dev/null +++ b/assets/components/validator.js @@ -0,0 +1,209 @@ +function checkEmail(input, msg) { + if (window.validateEmail(input.value)) { + msg.innerHTML = ''; + input.classList.remove('is-invalid'); + input.classList.add('is-valid'); + return false; + } else { + msg.innerHTML = 'поле является обязательным для заполнения'; + input.classList.remove('is-valid'); + input.classList.add('is-invalid'); + return true; + } +} + +function checkPhone(input, msg) { + if (window.validatePhone(input.value)) { + msg.innerHTML = ''; + input.classList.remove('is-invalid'); + input.classList.add('is-valid'); + return false; + } else { + msg.innerHTML = 'поле является обязательным для заполнения'; + input.classList.remove('is-valid'); + input.classList.add('is-invalid'); + return true; + } +} + +function checkNotEmpty(input, msg) { + var valid = false; + + if (input.value === '') { + msg.innerHTML = 'поле является обязательным для заполнения'; + input.classList.add('is-invalid'); + input.classList.remove('is-valid'); + valid = true; + } else { + msg.innerHTML = ''; + input.classList.add('is-valid'); + input.classList.remove('is-invalid'); + } + + return valid; +} + +function checkTextRu(input, msg) { + var valid = false; + + if (input.value === '') { + msg.innerHTML = 'поле является обязательным для заполнения'; + input.classList.add('is-invalid'); + input.classList.remove('is-valid'); + valid = true; + } else { + msg.innerHTML = ''; + input.classList.add('is-valid'); + input.classList.remove('is-invalid'); + } + + if (/[A-Za-z0-9]/.test(input.value)) { + msg.innerHTML = 'поле должно заполняться кириллицей'; + input.classList.add('is-invalid'); + input.classList.remove('is-valid'); + valid = true; + } + + return valid; +} + +function checkAccept(ckeckBox, msg) { + var valid = false; + + if (ckeckBox.checked) { + msg.innerHTML = ''; + ckeckBox.classList.add('is-valid'); + ckeckBox.classList.remove('is-invalid'); + } else { + msg.innerHTML = 'укажите согласие, на обработку персональных данных'; + ckeckBox.classList.add('is-invalid'); + ckeckBox.classList.remove('is-valid'); + valid = true; + } + + return valid; +} + +function checkDate(input, msg, range = false) { + var valid = false; + + if (range) { + if (/^(0[1-9]|[12][0-9]|3[01])\.(0[1-9]|1[0-2])\.\d{4}.\-.(0[1-9]|[12][0-9]|3[01])\.(0[1-9]|1[0-2])\.\d{4}$/.test(input.value)) { + msg.innerHTML = ''; + input.classList.add('is-valid'); + input.classList.remove('is-invalid'); + } else { + msg.innerHTML = 'поле является обязательным для заполнения'; + input.classList.add('is-invalid'); + input.classList.remove('is-valid'); + valid = true; + } + } else { + if (/^(0[1-9]|[12][0-9]|3[01])\.(0[1-9]|1[0-2])\.\d{4}$/.test(input.value)) { + msg.innerHTML = ''; + input.classList.add('is-valid'); + input.classList.remove('is-invalid'); + } else { + msg.innerHTML = 'поле является обязательным для заполнения'; + input.classList.add('is-invalid'); + input.classList.remove('is-valid'); + valid = true; + } + } + + return valid; +} + +function checkInt(input, msg) { + var valid = false; + + if (/^\d+$/.test(input.value)) { + msg.innerHTML = ''; + input.classList.add('is-valid'); + input.classList.remove('is-invalid'); + } else { + msg.innerHTML = 'поле является обязательным для заполнения'; + input.classList.add('is-invalid'); + input.classList.remove('is-valid'); + valid = true; + } + + return valid; +} + +function checkInn(input, msg) { + var valid = false; + + if (input.value === '') { + msg.innerHTML = 'поле является обязательным для заполнения'; + input.classList.add('is-invalid'); + input.classList.remove('is-valid'); + valid = true; + } else { + if (is_valid_inn(input.value)) { + msg.innerHTML = ''; + input.classList.add('is-valid'); + input.classList.remove('is-invalid'); + } else { + msg.innerHTML = 'проверьте правильность введённых данных'; + input.classList.add('is-invalid'); + input.classList.remove('is-valid'); + valid = true; + } + } + + return valid; +} + +function is_valid_inn(i) { + if ( i.match(/\D/) ) return false; + + var inn = i.match(/(\d)/g); + + if ( inn.length == 10 ) + { + return inn[9] == String((( + 2*inn[0] + 4*inn[1] + 10*inn[2] + + 3*inn[3] + 5*inn[4] + 9*inn[5] + + 4*inn[6] + 6*inn[7] + 8*inn[8] + ) % 11) % 10); + } + else if ( inn.length == 12 ) + { + return inn[10] == String((( + 7*inn[0] + 2*inn[1] + 4*inn[2] + + 10*inn[3] + 3*inn[4] + 5*inn[5] + + 9*inn[6] + 4*inn[7] + 6*inn[8] + + 8*inn[9] + ) % 11) % 10) && inn[11] == String((( + 3*inn[0] + 7*inn[1] + 2*inn[2] + + 4*inn[3] + 10*inn[4] + 3*inn[5] + + 5*inn[6] + 9*inn[7] + 4*inn[8] + + 6*inn[9] + 8*inn[10] + ) % 11) % 10); + } + + return false; +} + +function checkSmartCaptcha(token, msg) { + if (token === '') { + msg.innerHTML = 'Подтвердите, что Вы не робот'; + return true; + } + + msg.innerHTML = ''; + return false; +} + +module.exports = { + checkInt: checkInt, + checkNotEmpty: checkNotEmpty, + checkDate: checkDate, + checkInn: checkInn, + checkAccept: checkAccept, + checkSmartCaptcha: checkSmartCaptcha, + checkTextRu: checkTextRu, + checkEmail: checkEmail, + checkPhone: checkPhone +}; diff --git a/assets/controllers.json b/assets/controllers.json new file mode 100644 index 0000000..a1c6e90 --- /dev/null +++ b/assets/controllers.json @@ -0,0 +1,4 @@ +{ + "controllers": [], + "entrypoints": [] +} diff --git a/assets/controllers/alertSystem_controller.js b/assets/controllers/alertSystem_controller.js new file mode 100644 index 0000000..57bda09 --- /dev/null +++ b/assets/controllers/alertSystem_controller.js @@ -0,0 +1,103 @@ +import { Controller } from 'stimulus'; +const loader = require("./../components/loader.js"); +const helper = require("./../components/helper.js"); + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="default" attribute will cause + * this controller to be executed. The name "default" comes from the filename: + * default_controller.js -> "default" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + connect() { + const alertSystem = this.element; + + loader.loadSDK('system').then(function(webSDK) { + webSDK.on('init', function() { + runWebSDK(alertSystem, webSDK); + }); + }) + + if (location.pathname.indexOf('/case-history') != '-1') { + if (location.hash == '#error') { + var msg = 'Что то пошло не так, повторите попытку позже.'; + helper.addAlert(msg, alertSystem, 'alert-case-history-error'); + } + + if (location.hash == '#online') { + var msg = 'При отсутствии оплаты в течение 5 мин. онлайн консультация будет автоматически отменена'; + + helper.addAlert(msg, alertSystem, 'alert-case-history-online'); + } + + if (location.hash == '#pay-success') { + var msg = 'Спасибо, оплата прошла успешно.'; + + helper.addAlert(msg, alertSystem, 'alert-case-history-info', 'alert-success'); + } + if (location.hash == '#doctor-success') { + var msg = 'Спасибо, вы успешно записались на прием.'; + + helper.addAlert(msg, alertSystem, 'alert-case-history-info', 'alert-success'); + } + } + + if (window.location.href.includes("sovamed")) { + + const osa = `Онлайн консультация проводится через личный кабинет, никаких приложений устанавливать не нужно. Рекомендуем изучить инструкцию по онлайн консультированию или инструкцию по онлайн консультированию через Госуслуги
+ Возврат средств при несостоявшейся консультации производится в разделе приемы – история записей. Инструкция по возврату средств`; + + if (window.location.href.includes("onlineMode")) { + if (alertSystem.dataset.auth === "false") { + const onlineSpecialstAlert = `Для онлайн-консультации рекомендуем пройти авторизацию через Госуслуги.
+ Если Вы уже были в клинике и у Вас есть логин и пароль, при авторизации Вы можете использовать его. + Ознакомиться с инструкцией по онлайн-консультированию вы можете, пройдя по ссылке. + `; + + helper.addAlert(onlineSpecialstAlert, alertSystem, 'alert-online-mode', 'alert-info'); + } else { + helper.addAlert(osa, alertSystem, 'alert-online-mode', 'alert-info'); + } + } + + if (window.location.href.includes("/online-specialists")) { + helper.addAlert(osa, alertSystem, 'alert-online-mode', 'alert-info'); + } + } + + if (alertSystem.dataset.auth === "false") { + if (location.pathname.indexOf('/info') != '-1') { + var msg = 'Если Вы хотите, чтобы справка была направлена сразу в ФНС, в Ваш кабинет налогоплательщика, то авторизуйтесь через имеющийся логин/пароль или с помощью Госуслуг и заполните данные пациента.'; + + helper.addAlert(msg, alertSystem, 'alert-info'); + } + + if (location.pathname.indexOf('/specialist') != '-1' & !window.location.href.includes("onlineMode")) { + var msg = '* вы можете записать себя или другого человека без авторизации, но при этом запись не сохранится в личном кабинете и отменить/перенести ее в случае необходимости будет возможно только по звонку в колл-центр.'; + + helper.addAlert(msg, alertSystem, 'alert-not-auth'); + } + } + + let runWebSDK = function (alertSystem, webSDK) { + if (alertSystem.dataset.techMaintenance === 'true') { + var msg = 'Ведутся технические работы, функционал может быть доступен не полностью'; + helper.addAlert(msg, alertSystem, 'alert-warning'); + } + + if (webSDK.data.user.authenticated) { + const userAllows = webSDK.data.user.allows; + + if (userAllows.caseHistory == false || userAllows.payments == false || userAllows.reservation == false) { + + var msg = 'Ваша учетная запись имеет статус "Неподтвержденная регистрация", запись на прием через портал работает в ограниченном режиме. Вы можете записаться на прием не более одного раза к одному специалисту. Полный доступ на портал Вам будет предоставлен в регистратуре при следующем посещении клиники.'; + + helper.addAlert(msg, alertSystem, 'alert-info'); + } + } + } + } +} diff --git a/assets/controllers/bannersRegion_controller.js b/assets/controllers/bannersRegion_controller.js new file mode 100644 index 0000000..cc1dd07 --- /dev/null +++ b/assets/controllers/bannersRegion_controller.js @@ -0,0 +1,31 @@ +import { Controller } from 'stimulus'; +import Cookies from 'js-cookie'; +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="default" attribute will cause + * this controller to be executed. The name "default" comes from the filename: + * default_controller.js -> "default" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + connect() { + var bannerWrap = this.element; + + $.ajax({ + dataType: "json", + method: 'GET', + url: '/api/banner/' + Cookies.get('region'), + success(response) { + if (response.active == true) { + var a = document.createElement('a'); + a.href = response.href; + a.target = "_blank"; + a.innerHTML = 'баннер' + bannerWrap.append(a); + } + } + }); + } +} diff --git a/assets/controllers/calendar_controller.js b/assets/controllers/calendar_controller.js new file mode 100644 index 0000000..e98fce9 --- /dev/null +++ b/assets/controllers/calendar_controller.js @@ -0,0 +1,28 @@ +import { Controller } from 'stimulus'; + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="calendar" attribute will cause + * this controller to be executed. The name "hello" comes from the filename: + * calendar_controller.js -> "calendar" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + connect() { + var modal = document.getElementById('detail-specialist'); + + this.element.addEventListener('click', function (evn) { + modal.dataset.specialistid = evn.target.dataset.specialistid; + modal.dataset.filialid = evn.target.dataset.filialid; + modal.dataset.onlinemode = evn.target.dataset.onlinemode; + modal.dataset.docname = (evn.target.dataset.docName ?? evn.target.dataset.docname ?? '').toString(); + modal.dataset.address = (evn.target.dataset.address ?? '').toString(); + modal.dataset.comment = (evn.target.dataset.comment ?? '').toString(); + + $(modal).modal('show'); + }) + // this.element.textContent = 'Hello Stimulus! Edit me in assets/controllers/hello_controller.js'; + } +} diff --git a/assets/controllers/caseHistory_controller.js b/assets/controllers/caseHistory_controller.js new file mode 100644 index 0000000..aabafb1 --- /dev/null +++ b/assets/controllers/caseHistory_controller.js @@ -0,0 +1,451 @@ +import { Controller } from 'stimulus'; +import Cookies from 'js-cookie'; +const loader = require("./../components/loader.js"); +const helper = require("./../components/helper.js"); +const misSession = require("./../components/misSession.js"); + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="caseHistory" attribute will cause + * this controller to be executed. The name "caseHistory" comes from the filename: + * caseHistory_controller.js -> "caseHistory" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + async cashBack(evn) { + loader.btnLoader(evn.target, true) + + var popup = document.getElementById('popup'); + popup.querySelector('.modal-title').innerHTML = 'Возврат средств'; + popup.querySelector('.modal-dialog').classList = 'modal-dialog modal-md'; + + try { + const response = await fetch('/refund?filial=' + evn.target.dataset.filial); + + if (response.ok) { + const text = await response.text(); + + popup.querySelector('.modal-body').innerHTML = text; + loader.btnLoader(evn.target, false); + $(popup).modal('show'); + + popup.querySelector('#refund_address').value = window.webSDK.data.user.address; + popup.querySelector('#refund_phone').value = window.webSDK.data.user.phone; + popup.querySelector('#refund_email').value = window.webSDK.data.user.email; + popup.querySelector('#refund_schedident').value = evn.target.dataset.schedident; + popup.querySelector('#refund_time').value = evn.target.dataset.time; + popup.querySelector('#refund_doc_name').value = evn.target.dataset.docName; + + document.getElementById('refund-form').querySelector('button').addEventListener('click', function() { + if ($('#refund-form')[0].checkValidity()) { + $(popup).modal('hide'); + } + }) + } + + } catch (error) { + console.error('Error:', error); + loader.btnLoader(evn.target, false); + } + } + + connect() { + loader.loadSDK('caseHistory').then(function(webSDK) { + webSDK.on('init', function() { + if (this.data.user.authenticated) { + runWebSDK(webSDK); + } else { + window.location.pathname = '/logout' + } + }); + }) + + var securityRecord = this.element; + var popup = document.getElementById('popup'); + + let runWebSDK = function (webSDK) { + scheduleRecordList(webSDK, securityRecord); + } + + var securityTabs = document.getElementById('security-tabs'); + var tabsDecktop = securityTabs.querySelectorAll('a'); + var alertInfo = document.getElementById('alert-case-history-info'); + + if (tabsDecktop.length > 0) { + tabsDecktop.forEach(function (el) { + el.addEventListener('click', function (evn) { + evn.target.classList = 'tab-item tab-item--active'; + + if (evn.target.dataset.alert === 'true') alertInfo.classList.remove('d-none'); + + securityTabs.querySelectorAll('a').forEach(function (a) { + if (evn.target.dataset.allowRemove != a.dataset.allowRemove) { + a.classList.remove('tab-item--active'); + } + }); + + var history = (evn.target.dataset.allowRemove == 'true'); + filter(history, securityRecord); + }); + }); + } else { + securityTabs.addEventListener('change', function(evn) { + if (evn.target.alert === 'true') alertInfo.classList.remove('d-none'); + + var history = (evn.target.value == 'true'); + filter(history, securityRecord); + }); + } + + function filter(history, securityRecord) { + var items = securityRecord.querySelectorAll('.item-record'); + if (items) { + var count = 0; + items.forEach(function (item) { + if (history == true) { + item.classList.add('d-none'); + if (item.dataset.workDate >= window.dateFormat(new Date())) { + item.classList.remove('d-none'); + count++; + } + } else { + item.classList.add('d-none'); + + if (item.dataset.workDate < window.dateFormat(new Date())) { + if (item.dataset.onlinemode == '1') { + item.querySelector('.btn-conference').classList.remove('d-inline'); + item.querySelector('.btn-conference').classList.add('d-none'); + item.querySelector('.online-warning').classList.remove('d-none'); + console.log(item.querySelector('.online-warning')); + + var refundBtn = item.querySelector('.btn-cash-back') + refundBtn.classList.add('d-inline'); + refundBtn.dataset.schedident = item.dataset.schedident + refundBtn.dataset.filial = item.dataset.filial + refundBtn.dataset.time = item.dataset.time + refundBtn.dataset.docName = item.dataset.docName + } + + item.classList.remove('d-none'); + count++; + } + } + + notItems(count); + }); + } + } + + function notItems(count) { + var load = securityRecord.querySelector('.load'); + load.classList.add('d-none'); + + if (count == 0) { + load.innerHTML = 'Записей не найдено'; + load.classList.remove('d-none'); + } + } + + var renderItems = function(items, securityWrap) { + var count = 0; + + items.forEach(function(data, i) { + var blockItem = securityRecord.querySelector('.item-record').cloneNode(true); + blockItem.dataset.workDate = data.workDate; + blockItem.dataset.onlinemode = data.onlineType ?? 0; + blockItem.dataset.docName = data.docName; + blockItem.dataset.time = data.startTime+ ' '+ data.endTime; + blockItem.dataset.schedident = data.schedident; + blockItem.dataset.filial = data.filial; + blockItem.dataset.dcode = data.dcode; + + if (blockItem.dataset.dcode) { + helper.sendRequest({ + 'sid': blockItem.dataset.dcode, + 'onlineMode': blockItem.dataset.onlineType ?? 0 + }, "/api/doctor", "GET").then(function (response) { + if (response.data) { + blockItem.querySelector('.img-vr').style.background = 'url(' + response.data.img + ') center -5px / cover'; + blockItem.querySelector('.position').innerHTML = response.data.speciality; + + if (response.data.kinder != null) { + var kinder = blockItem.querySelector('.kinder'); + kinder.classList.remove('d-none'); + kinder.querySelector('.val').innerHTML = response.data.kinder; + } + + if (response.data.expirience != null) { + var kinder = blockItem.querySelector('.expirience'); + kinder.classList.remove('d-none'); + kinder.querySelector('.val').innerHTML = response.data.expirience; + } + + blockItem.querySelectorAll('.link-specialist').forEach(function (el) { + el.href = '/specialist/' + response.data.alias; + }); + } else { + blockItem.querySelector('.img-vr').style.background = 'url(/images/no_img.png) center -5px / cover'; + blockItem.querySelector('.position').innerHTML = ''; + } + }); + } + + var date = window.newDate(data.workDate); + + if (Cookies.get('region') == 91) { + var timeZona = '
время саратовское'; + } else { + var timeZona = '
время московское'; + } + + blockItem.querySelector('.month').innerHTML = getWeekDay(date); + blockItem.querySelector('.date').innerHTML = window.dateFormat(date, 'd-m-Y') + ' ' + + data.startTime + timeZona; + blockItem.querySelector('.address').innerHTML = (data.filialName == 'Онлайн клиника') ? 'Онлайн-консультация' : data.filialName; + blockItem.querySelector('.section-favorite').dataset.sid = data.dcode; + blockItem.querySelector('.specialist').innerHTML = data.docName; + + var btnClose = blockItem.querySelector('.btn-close'); + btnClose.classList.add('d-none'); + + if (data.allowRemove) { + btnClose.classList.remove('d-none'); + btnClose.dataset.id = data.id; + btnClose.dataset.filial = data.filial; + btnClose.addEventListener('click', function () { + if (confirm("Подтвердите удаление")) { + webSDK.scheduleRecRemove({ + 'reserveId': btnClose.dataset.id, + 'filialId': btnClose.dataset.filial + }).then(function (resolve) { + document.location.reload(); + }).catch(function (error) { + helper.sendRequest({ + data: {'error': error, method: 'scheduleRecRemove'} + }, helper.getHostname() + '/api/log', "POST", "json", true, "application/json"); + }) + } + }); + } + + var btnConfirence = blockItem.querySelector('.btn-conference'); + var accept = blockItem.querySelector('.accept'); + var licenseLink = helper.getLicenseLink(Cookies.get('region')); + var license = document.createElement('p'); + + if (data.onlineType && data?.payment?.status?.id != 0) { + license.innerHTML = `Пожалуйста, не забудете за 5 минут до начала консультации войти в личный кабинет и потом нажать кнопку «Онлайн прием» для начала консультации.`; + accept.append(license); + + var warning = document.createElement('p'); + warning.innerHTML=`Внимание! В начале онлайн-консультации вам будет необходимо подтвердить своё согласие с информированным добровольным согласием на медицинское вмешательство (ИДС).`; + accept.append(warning); + + btnConfirence.classList.remove('d-none') + btnConfirence.classList.add('d-inline') + btnConfirence.setAttribute('data-id' , data.id) + btnConfirence.setAttribute('data-filial' , data.filial) + btnConfirence.addEventListener('click', function () { + popup.querySelector('#popup-body').innerHTML = ''; + + misSession.ensureAuthenticated(webSDK).then(function() { + return webSDK.openConference({ + schedid: btnConfirence.dataset.id + }); + }).then(function () { + if (!misSession.mountConferenceInPopup(popup)) { + throw { data: { message: 'Не удалось открыть окно онлайн-консультации.' } }; + } + }).catch(function (e) { + if (misSession.handleMisSessionFailure(popup, e)) { + helper.sendRequest({ + data: {'error': e, method: 'openConference', reason: 'mis_session_expired'} + }, helper.getHostname() + '/api/log', "POST", "json", true, "application/json"); + return; + } + + if (typeof e.data?.message !== 'undefined') { + var msg = e.data.message.replace('UTC+3', 'UTC+3 (московское время)'); + popup.querySelector('#popup-body').innerHTML = msg; + popup.querySelector('.modal-dialog').classList = 'modal-dialog'; + popup.querySelector('.modal-content').classList = 'modal-content'; + popup.querySelector('.modal-title').innerHTML = 'Уведомление'; + $(popup).modal('show'); + } + + helper.sendRequest({ + data: {'error': e, method: 'openConference'} + }, helper.getHostname() + '/api/log', "POST", "json", true, "application/json"); + }); + }); + + } else { + btnConfirence.classList.add('d-none') + } + + var btnPayment = blockItem.querySelector('.btn-pay'); + + if (data.payment && data.payment.status.id == 0 && data.allowRemove) { + license.innerHTML = `Для получения возможности провести оплату консультации - подтвердите согласие на присоединение к договору оферты:`; + accept.append(license); + blockItem.querySelector('.online-warning').classList.remove('d-none'); + console.log(blockItem.querySelector('.online-warning')); + + var politics = document.createElement('div'); + politics.classList = "form-check"; + + var politicsInput = document.createElement('input'); + politicsInput.type="checkbox"; + politicsInput.classList="form-check-input"; + politicsInput.id="accept-btn"; + politics.append(politicsInput); + + var politicsLabel = document.createElement('label'); + politicsLabel.classList = "form-check-label"; + politicsLabel.htmlFor="accept-btn"; + politics.append(politicsLabel); + + politicsLabel.innerHTML = `Согласен с политикой в отношении обработки персональных данных`; + politics.addEventListener('click', function () { + console.log('politics', politicsInput.checked, personalInput.checked, ofertaInput.checked); + btnPayment.classList.add('disabled'); + btnPayment.disabled = true; + + if (politicsInput.checked && personalInput.checked && ofertaInput.checked) { + btnPayment.classList.remove('disabled'); + btnPayment.disabled = false; + } + }); + accept.append(politics); + + var personal = document.createElement('div'); + personal.classList = "form-check"; + + var personalInput = document.createElement('input'); + personalInput.type="checkbox"; + personalInput.classList="form-check-input"; + personalInput.id="accept-personal"; + personal.append(personalInput); + + var personalLabel = document.createElement('label'); + personalLabel.classList = "form-check-label"; + personalLabel.htmlFor="accept-personal"; + personal.append(personalLabel); + + personalLabel.innerHTML = `Согласен с обработкой персональных данных.`; + personal.addEventListener('click', function () { + btnPayment.classList.add('disabled'); + btnPayment.disabled = true; + + if (politicsInput.checked && personalInput.checked && ofertaInput.checked) { + btnPayment.classList.remove('disabled'); + btnPayment.disabled = false; + } + }); + accept.append(personal); + + var oferta = document.createElement('div'); + oferta.classList = "form-check"; + + var ofertaInput = document.createElement('input'); + ofertaInput.type="checkbox"; + ofertaInput.classList="form-check-input"; + ofertaInput.id="accept-oferta"; + oferta.append(ofertaInput); + + var ofertaLabel = document.createElement('label'); + ofertaLabel.classList = "form-check-label"; + ofertaLabel.htmlFor="accept-oferta"; + oferta.append(ofertaLabel); + + ofertaLabel.innerHTML = `Присоединяюсь к договору оферты`; + oferta.addEventListener('click', function () { + btnPayment.classList.add('disabled'); + btnPayment.disabled = true; + + if (politicsInput.checked && personalInput.checked && ofertaInput.checked) { + btnPayment.classList.remove('disabled'); + btnPayment.disabled = false; + } + }); + accept.append(oferta); + + btnPayment.classList.remove('d-none'); + btnPayment.classList.add('d-inline'); + btnPayment.querySelector('.amt').innerHTML = data.payment.amt; + btnPayment.setAttribute('data-id' , data.payment.id); + btnPayment.setAttribute('data-filial' , data.filial); + btnPayment.setAttribute('data-amt' , data.payment.amt); + btnPayment.setAttribute('data-payprofileid' , data.payment.magazineId); + btnPayment.addEventListener('click', function () { + var params = { + 'orderid': Number(btnPayment.dataset.id) , + 'payprofileid': btnPayment.dataset.payprofileid, + 'payamount': btnPayment.dataset.amt, + 'paymethod': 'AC', + 'filial': Number(btnPayment.dataset.filial), + 'pcode': webSDK.data.user.id, + 'successurl': document.location.origin + '/case-history#pay-success', + 'errorurl': document.location.origin + '/case-history#error', + 'containerId': 'popup-body', + }; + + misSession.ensureAuthenticated(webSDK).then(function() { + webSDK.loadPaymentView(params); + popup.querySelector('#popup-body').innerHTML = ''; + popup.querySelector('.modal-title').innerHTML = 'Оплата'; + $(popup).modal('show'); + }).catch(function(e) { + if (misSession.handleMisSessionFailure(popup, e)) { + helper.sendRequest({ + data: {'error': e, method: 'loadPaymentView', reason: 'mis_session_expired'} + }, helper.getHostname() + '/api/log', "POST", "json", true, "application/json"); + return; + } + + helper.sendRequest({ + data: {'error': e, method: 'loadPaymentView'} + }, helper.getHostname() + '/api/log', "POST", "json", true, "application/json"); + }); + }); + } else { + btnPayment.classList.add('d-none'); + } + + if (data.workDate >= window.dateFormat(new Date())) { + blockItem.classList.remove('d-none'); + count++; + } + + securityWrap.append(blockItem); + }); + + notItems(count); + } + + function scheduleRecordList(webSDK, securityRecord) { + // Если страница загружена после создания записи, добавляем небольшую задержку + // чтобы дать серверу время обработать новую запись + var delay = (location.hash === '#doctor-success' || location.hash === '#online') ? 500 : 0; + + setTimeout(function() { + webSDK.loadScheduleRecList({ + st: 20170101, + en: window.dateFormat(new Date((new Date()).getFullYear(), (new Date()).getMonth() + 6, 0)), + start: 0, + length: 500 + }).then(function (resolve) { + renderItems(resolve.data, securityRecord.querySelector('.section-wrap')); + }).catch(function(e) { + helper.sendRequest({ + data: {'error': e, method: 'loadScheduleRecList'} + }, helper.getHostname() + '/api/log', "POST", "json", true, "application/json"); + securityRecord.querySelector('.load').innerHTML = 'Записей не найдено'; + }); + }, delay); + } + } +} + diff --git a/assets/controllers/changePatient_controller.js b/assets/controllers/changePatient_controller.js new file mode 100644 index 0000000..36e46c5 --- /dev/null +++ b/assets/controllers/changePatient_controller.js @@ -0,0 +1,123 @@ +import { Controller } from 'stimulus'; +const loader = require("./../components/loader.js"); +const helper = require("./../components/helper.js"); + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="default" attribute will cause + * this controller to be executed. The name "default" comes from the filename: + * default_controller.js -> "default" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + connect() { + loader.loadSDK('system').then(function(webSDK) { + webSDK.on('init', function() { + if (this.data.user.authenticated) { + runWebSDK(webSDK); + } + }); + }) + + let runWebSDK = function (webSDK) { + this.showNamePatient(webSDK.data.user.fullName); + this.element.addEventListener('click', function () { + this.changeResponsible(webSDK); + }.bind(this)) + }.bind(this) + + $(popup).on('hidden.bs.modal', function () { + this.querySelector('.modal-title').innerHTML = ''; + this.querySelector('.modal-dialog').classList.add('modal-lg'); + this.querySelector('.modal-body').innerHTML = ''; + + if (this.querySelector('.modal-footer')) { + this.querySelector('.modal-footer').remove(); + } + }); + } + + showNamePatient(fullName) { + document.getElementById('fullName').innerHTML = fullName; + } + + changeResponsible(webSDK) { + var popup = document.getElementById('popup'); + popup.querySelector('.modal-title').innerHTML = 'Выбор пациента'; + popup.querySelector('.modal-dialog').classList.remove('modal-lg'); + + var popupBody = document.getElementById('popup-body'); + popupBody.innerHTML = ''; + + webSDK.data.user.represents.forEach(function (represent) { + var div = document.createElement('div'); + div.classList = 'form-check'; + + var input = document.createElement('input'); + input.id = represent.pcode; + input.classList = 'form-check-input'; + input.value = represent.pcode; + input.type = "radio"; + input.name = "changePatient"; + + if (webSDK.data.user.representId == represent.pcode) { + input.checked = 'true'; + } + + div.append(input); + + var label = document.createElement('label'); + label.setAttribute('for', represent.pcode); + label.classList = 'form-check-label'; + label.innerHTML = represent.fullName; + div.append(label); + + popupBody.append(div); + + }); + + var div = document.createElement('div'); + div.classList = 'modal-footer'; + + var button = document.createElement('button'); + button.classList = 'btn btn-outline-secondary'; + button.innerHTML = 'Выбрать'; + button.addEventListener('click', function() { + loader.btnLoader(this, true); + window.webSDK.selectClient({ + id: document.querySelector('input[name="changePatient"]:checked').value + }).then(function (user) { + $.ajax({ + method: "POST", + crossDomain: false, + url: "/api/authenticated", + contentType: "application/x-www-form-urlencoded", + dataType: "json", + data: { + user: user, + uid: user.id + }, + success(response) { + if (response.data.success == true) { + var parser = document.createElement('a'); + parser.href = response.data.redirect; + + window.location.replace(document.location.origin + parser.pathname + parser.search); + } + } + }); + }).catch(function (e) { + helper.sendRequest({ + data: {'error': e, method: 'selectClient'} + }, helper.getHostname() + '/api/log', "POST", "json", true, "application/json"); + }) + }); + + div.append(button); + popup.querySelector('.modal-content').append(div); + + $(popup).modal('show'); + } +} diff --git a/assets/controllers/changeRegion_controller.js b/assets/controllers/changeRegion_controller.js new file mode 100644 index 0000000..e48daa9 --- /dev/null +++ b/assets/controllers/changeRegion_controller.js @@ -0,0 +1,65 @@ +"use strict"; + +import { Controller } from 'stimulus'; +import Cookies from 'js-cookie'; + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="changeRegion" attribute will cause + * this controller to be executed. The name "changeRegion" comes from the filename: + * changeRegion_controller.js -> "changeRegion" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + connect() { + var currentUrl = new URL(window.location); + + if (currentUrl.searchParams.get('region') !== null) { + Cookies.set('region', currentUrl.searchParams.get('region'), {expires: 1}); + currentUrl.searchParams.delete('region'); + location.replace(currentUrl); + } + + var checkboxes = this.element.querySelectorAll('input'); + this.element.querySelector('button.submit').addEventListener('click', function() { + checkboxes.forEach(function(el) { + if (el.checked) { + origin = currentUrl.origin.split('.'); + origin[1] = (el.value == '94')? 'wmtmed' : 'sovamed'; + currentUrl.search = 'region=' + el.value + Cookies.set('region', el.value, {expires: 1}); + location.replace(origin.join('.') + currentUrl.pathname + currentUrl.search + location.hash); + } + }); + }); + + if (/sovamed\.ru/m.test(location.hostname)) { + if (typeof Cookies.get('region') === 'undefined' || Cookies.get('region') == '94') { + $('#chengeRegion').modal('show'); + } + } else { + Cookies.set('region', 94, {expires: 1}); + } + + if (Cookies.get('region')) { + var item = this.element.querySelector('#regionItem' + Cookies.get('region')); + item.checked = true; + + if (document.getElementById('regionName')) { + document.getElementById('regionName').innerText = item.dataset.text; + } + } + + $('#chengeRegion').on('hide.bs.modal', function (e) { + if (!Cookies.get('region')) { + if (/sovamed\.ru/m.test(location.hostname)) { + Cookies.set('region', 91, {expires: 1}); + } else { + Cookies.set('region', 94, {expires: 1}); + } + } + }) + } +} diff --git a/assets/controllers/checkScheduleBitrix_controller.js b/assets/controllers/checkScheduleBitrix_controller.js new file mode 100644 index 0000000..33369e4 --- /dev/null +++ b/assets/controllers/checkScheduleBitrix_controller.js @@ -0,0 +1,81 @@ +import { Controller } from 'stimulus'; + +const helper = require("./../components/helper.js"); +const record = require("./../components/record.js"); + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="checkScheduleBitrix" attribute will cause + * this controller to be executed. The name "hello" comes from the filename: + * calendar_checkSchedule.js -> "checkScheduleBitrix" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + connect() { + var popup = document.getElementById('popup'); + + if (! popup) { + renderModal(); + } + + function renderModal() { + var popup = document.createElement('div'); + popup.id = 'popup'; + popup.classList = 'fancybox-content'; + popup.style = "display: none; width: 700px;" + document.body.append(popup); + + var modalDialog = document.createElement('div'); + modalDialog.classList = 'modal-dialog'; + modalDialog.setAttribute('role', 'document'); + popup.append(modalDialog); + + var modalContent = document.createElement('div'); + modalContent.classList = 'modal-content'; + modalDialog.append(modalContent); + + var modalHeader = document.createElement('div'); + modalHeader.classList = 'modal-header'; + modalContent.append(modalHeader); + + var modalTitle = document.createElement('h5'); + modalTitle.classList = 'modal-title'; + modalHeader.append(modalTitle); + + var popupBody = document.createElement('div'); + popupBody.id = 'popup-body'; + popupBody.classList = 'modal-body'; + modalContent.append(popupBody); + + if (location.host == 'sovenok.sovamed.ru') { + var modalTitleDesc = document.createElement('p'); + modalTitleDesc.innerHTML = 'Внимание! Сопровождать ребенка на прием могут только законные представители пациента.'; + modalHeader.append(modalTitleDesc); + + var modalFooter = document.createElement('div'); + modalFooter.classList = 'modal-footer'; + modalFooter.innerHTML = '*При первом посещении клиники не забудьте паспорт и свидетельство о рождении ребенка.'; + modalContent.append(modalFooter); + } + + return true; + } + } + + show() { + const popup = document.getElementById('popup'); + + var date = window.newDate(this.element.dataset.workDate.replace(/\D/g, "")); + this.element.dataset.workDate = window.dateFormat(date, 'Y-m-d'); + + if (record.renderFormRecord(window.userInfo, this.element.dataset, false)) { + ym(48780536,'reachGoal','pram-zapis'); + $.fancybox.open({ + src : '#popup', + type : 'inline', + }); + } + } +} diff --git a/assets/controllers/checkSchedule_controller.js b/assets/controllers/checkSchedule_controller.js new file mode 100644 index 0000000..1c2e4bb --- /dev/null +++ b/assets/controllers/checkSchedule_controller.js @@ -0,0 +1,345 @@ +import { Controller } from 'stimulus'; +const helper = require("./../components/helper.js"); +const record = require("./../components/record.js"); + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="checSchedule" attribute will cause + * this controller to be executed. The name "hello" comes from the filename: + * calendar_checkSchedule.js -> "checSchedule" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + getClinicPhone() { + // primary source: menu_controller.js tel() sets window.clinicPhone + if (typeof window !== 'undefined' && window.clinicPhone) { + return String(window.clinicPhone).trim(); + } + + // fallback: read what tel() puts into the header button + const el = document.getElementById('btn-callback-clinic'); + const phone = el ? (el.textContent || '').trim() : ''; + return phone || ''; + } + + setTitleWithPhone(titleEl) { + if (!titleEl) return; + + const phone = this.getClinicPhone(); + const phoneText = phone ? phone : '______'; + titleEl.textContent = `К этому специалисту возможна запись по тел.${phoneText} или через кнопку «записаться».`; + + // If phone not ready yet, retry briefly (tel() is async). + if (!phone) { + let tries = 0; + const timer = setInterval(() => { + tries++; + const p = this.getClinicPhone(); + if (p) { + titleEl.textContent = `К этому специалисту возможна запись по тел.${p} или через кнопку «записаться».`; + clearInterval(timer); + } else if (tries >= 20) { + clearInterval(timer); + } + }, 250); + } + } + + connect() { + let run = function (intervalBlock, id, filialName) { + this.show(intervalBlock, id, filialName) + }.bind(this) + + // Функция для получения intervalBlock по текущему data-id + const getIntervalBlock = () => { + return document.getElementById(this.element.dataset.id); + }; + + var intervalBlock = getIntervalBlock(); + if (!intervalBlock) { + return; // Если блок не найден, выходим + } + + var wrap = intervalBlock.querySelector('.intervals-wrap'); + + wrap.classList.remove('grid-list'); + wrap.classList.add('grid-none'); + wrap.innerHTML = "Идет загрузка..."; + + var select = this.element.querySelector('.select-schedule'); + + if (select) { + // Получаем значение department из URL параметра specialist_search[department] + const urlParams = new URLSearchParams(window.location.search); + const departmentParam = urlParams.get('specialist_search[department]') || + urlParams.get('specialist_search%5Bdepartment%5D'); + + let optionFound = false; + + // Если есть параметр department в URL, выбираем соответствующую опцию + if (departmentParam) { + const departmentId = parseInt(departmentParam); + for (let i = 0; i < select.options.length; i++) { + const optionValue = select.options[i].value; + // Формат значения: dcode:filial:department:onlineMode:infoclinica + const parts = optionValue.split(':'); + if (parts.length >= 3 && parseInt(parts[2]) === departmentId) { + select.value = optionValue; + optionFound = true; + // Обновляем data-id родительского элемента в формате dcode:onlineMode:infoclinica + if (parts.length >= 5) { + this.element.dataset.id = `${parts[0]}:${parts[3]}:${parts[4]}`; + // Обновляем ссылку на intervalBlock после изменения data-id + intervalBlock = getIntervalBlock(); + if (intervalBlock) { + wrap = intervalBlock.querySelector('.intervals-wrap'); + } + } + // Обновляем bootstrap-select если он используется + // Используем setTimeout чтобы дать время bootstrap-select инициализироваться + setTimeout(() => { + if (typeof $ !== 'undefined' && $(select).data('selectpicker')) { + $(select).selectpicker('refresh'); + } + }, 100); + // Запускаем загрузку расписания для выбранной опции + if (intervalBlock) { + run(intervalBlock, optionValue, select.options[i].text); + } + break; + } + } + } + + // Если параметра нет или опция не найдена, используем дефолтное поведение + if (!optionFound) { + run(intervalBlock, false, false); + } + + const controllerElement = this.element; // Сохраняем ссылку на элемент контроллера + select.addEventListener('change', function() { + // Обновляем data-id родительского элемента при изменении селекта + const optionValue = this.value; + const parts = optionValue.split(':'); + // Формат значения: dcode:filial:department:onlineMode:infoclinica + // Формат data-id: dcode:onlineMode:infoclinica + if (parts.length >= 5) { + controllerElement.dataset.id = `${parts[0]}:${parts[3]}:${parts[4]}`; + // Обновляем ссылку на intervalBlock после изменения data-id + const newIntervalBlock = document.getElementById(controllerElement.dataset.id); + if (newIntervalBlock) { + intervalBlock = newIntervalBlock; + wrap = intervalBlock.querySelector('.intervals-wrap'); + } + } + + wrap.classList.remove('grid-list'); + wrap.classList.add('grid-none'); + wrap.innerHTML = "Идет загрузка..."; + + run(intervalBlock, optionValue, select.options[select.selectedIndex].text) + }); + } else { + // Если селекта нет, используем дефолтное поведение + run(intervalBlock, false, false); + } + } + + show(intervalBlock, id, filialName) { + // Внутри intervalBlock должны быть: + // - .intervals-wrap (контейнер для интервалов/кнопки) + // - .show-specialist-detail (кнопка "Все даты" с dataset для /api/interval) + // Раньше при пустом расписании мы затирали intervalBlock.innerHTML, + // из-за чего при смене клиники селектом пропадал btn и падало на btn.dataset. + // Теперь никогда не удаляем intervalBlock целиком. + let wrap = intervalBlock.querySelector('.intervals-wrap'); + if (!wrap) { + wrap = document.createElement('div'); + wrap.className = 'intervals-wrap'; + intervalBlock.prepend(wrap); + } + + const btn = intervalBlock.querySelector('.show-specialist-detail'); + + // Кешируем dataset на самом intervalBlock, чтобы можно было работать даже если btn временно скрыт + // data-docName в HTML даёт dataset.docName (camelCase), читаем оба варианта + if (btn) { + intervalBlock.dataset.specialistid = btn.dataset.specialistid || intervalBlock.dataset.specialistid || ''; + intervalBlock.dataset.departmentid = btn.dataset.departmentid || intervalBlock.dataset.departmentid || ''; + intervalBlock.dataset.filialid = btn.dataset.filialid || intervalBlock.dataset.filialid || ''; + intervalBlock.dataset.onlinemode = btn.dataset.onlinemode || intervalBlock.dataset.onlinemode || ''; + intervalBlock.dataset.address = btn.dataset.address || intervalBlock.dataset.address || ''; + intervalBlock.dataset.company = btn.dataset.company || intervalBlock.dataset.company || ''; + intervalBlock.dataset.comment = btn.dataset.comment || intervalBlock.dataset.comment || ''; + intervalBlock.dataset.docname = (btn.dataset.docName || btn.dataset.docname || intervalBlock.dataset.docname || '').toString(); + } + // Ищем элементы заголовка/даты строго внутри текущей карточки, + // чтобы не зависеть от хрупких parentElement-цепочек. + const timeList = intervalBlock.closest('.time-list'); + const titleEl = timeList ? timeList.querySelector('.time-list__title') : null; + // Если в заголовке ранее был текст для "записаться", возвращаем стандартный вид перед загрузкой расписания + if (titleEl && !titleEl.querySelector('.cdate')) { + titleEl.innerHTML = 'Удобное время для записи: загружается'; + } + + const cdate = titleEl ? titleEl.querySelector('.cdate') : null; + const ds = intervalBlock.dataset || {}; + if (!ds.specialistid || !ds.departmentid || !ds.filialid) { + // Нет данных для запроса расписания (защитимся от падения) + return; + } + + var data = { + 'update' : true, + 'doctor': ds.specialistid, + 'department': ds.departmentid, + 'filial': ds.filialid, + 'startInterval': document.querySelector('.specialist-items').dataset.st, + 'endInterval': document.querySelector('.specialist-items').dataset.en, + 'onlineMode': ds.onlinemode + }; + + if (id) { + var params = id.split(":"); + data.doctor = params[0]; + data.filial = params[1]; + data.department = params[2]; + data.onlineMode = params[3]; + + if (btn) { + btn.dataset.specialistid = params[0]; + btn.dataset.filialid = params[1]; + btn.dataset.departmentid = params[2]; + btn.dataset.onlinemode = params[3]; + } + intervalBlock.dataset.specialistid = params[0]; + intervalBlock.dataset.filialid = params[1]; + intervalBlock.dataset.departmentid = params[2]; + intervalBlock.dataset.onlinemode = params[3]; + } + + if (filialName) { + if (btn) { + btn.dataset.address = filialName; + } + intervalBlock.dataset.address = filialName; + } + + helper.sendRequest(data, "/api/interval", "GET").then((resolve) => { + var isNotFree = true; + // если ранее скрывали кнопку "Все даты" — возвращаем обратно при любом новом запросе + if (btn) { + btn.classList.remove('d-none'); + } + intervalBlock.classList.remove('space-between'); + + wrap.classList.add('grid-list'); + wrap.classList.remove('grid-none'); + wrap.innerHTML = ""; + var i = 0; + + if (cdate && cdate.dataset && cdate.dataset.nearestDate) { + delete cdate.dataset.nearestDate; + } + + // Проверяем, есть ли данные в ответе + if (!resolve.data.intervalsData || resolve.data.intervalsData.length === 0) { + isNotFree = true; + } else { + resolve.data.intervalsData.forEach(function(el) { + if (el.isFree == true) { + var cDate = new Date(el.workDate).toLocaleString('ru', {year: 'numeric',month: 'numeric',day: 'numeric'}); + var nearestDate = cdate && cdate.dataset ? cdate.dataset.nearestDate : undefined; + + if (i < 6) { + isNotFree = false; + + for (let [key, item] of Object.entries(el.intervals)) { + if (item.isFree == true && i < 6 && typeof nearestDate === 'undefined') { + if (i == 0) { + var spanCurrentDate = document.createElement('span'); + spanCurrentDate.innerText = cDate; + if (cdate) { + cdate.innerText = cDate; + cdate.dataset.nearestDate = item.nearestDate; + } + } + + var spanInterval = document.createElement('span'); + spanInterval.classList = 'time available text-center'; + spanInterval.innerText = item.startTime; + spanInterval.dataset.workDate = item.workDate; + spanInterval.dataset.schedident = item.schedident; + spanInterval.dataset.time = item.time; + spanInterval.dataset.onlinemode = item.onlineMode; + spanInterval.dataset.rnum = item.rNum; + spanInterval.dataset.specialistid = intervalBlock.dataset.specialistid || btn.dataset.specialistid; + spanInterval.dataset.filialid = intervalBlock.dataset.filialid || btn.dataset.filialid; + spanInterval.dataset.department = btn.dataset.departmentid; + spanInterval.dataset.company = intervalBlock.dataset.company || btn.dataset.company || ''; + spanInterval.dataset.comment = intervalBlock.dataset.comment || btn.dataset.comment || ''; + spanInterval.dataset.address = intervalBlock.dataset.address || btn.dataset.address || ''; + spanInterval.dataset.docname = (intervalBlock.dataset.docname || btn.dataset.docName || btn.dataset.docname || '').toString(); + spanInterval.addEventListener('click', function (evn) { + var popupWrap = document.getElementById('popup'); + evn.target.dataset.onlinemode = item.onlineMode; + + if (record.renderFormRecord(resolve.data.userInfo, evn.target.dataset)) { + if (resolve.data.userInfo) { + evn.target.innerHTML = ''; + record.sendReserve(popupWrap) + } else { + $(popupWrap).modal('show'); + } + } + }) + + wrap.append(spanInterval); + i++; + } + } + } + } + }); + } + + if (isNotFree) { + // Если расписание пустое, показываем кнопку "Записаться" как при specialist.infoclinica == false + intervalBlock.classList.add('space-between'); + // прячем "Все даты", но не удаляем (нужно для переключения селекта) + if (btn) { + btn.classList.add('d-none'); + } + + var button = document.createElement('button'); + // Используем правильные имена атрибутов в camelCase для dataset + // data-docName -> dataset.docName (не docname!) + var docName = btn && btn.dataset.docName ? btn.dataset.docName : ''; + var address = btn && btn.dataset.address ? btn.dataset.address : 'null'; + var company = btn && btn.dataset.company ? btn.dataset.company : 'null'; + var comment = btn && btn.dataset.comment ? btn.dataset.comment : ''; + + button.setAttribute('data-docName', docName); + button.setAttribute('data-address', address); + button.setAttribute('data-company', company); + button.setAttribute('data-comment', comment); + button.className = 'btn-show-specialist-detail'; + button.type = 'button'; + button.setAttribute('data-controller', 'uslugi'); + button.innerText = 'Записаться'; + + // кладём кнопку в wrap, чтобы не ломать структуру блока + wrap.classList.remove('grid-list'); + wrap.classList.add('grid-none'); + wrap.innerHTML = ''; + wrap.appendChild(button); + + // И если мы показываем кнопку, то заголовок должен быть с телефоном/подсказкой + this.setTitleWithPhone(titleEl); + } + + }); + } +} diff --git a/assets/controllers/cookieNotice_controller.js b/assets/controllers/cookieNotice_controller.js new file mode 100644 index 0000000..25760fc --- /dev/null +++ b/assets/controllers/cookieNotice_controller.js @@ -0,0 +1,26 @@ +import { Controller } from 'stimulus'; + +export default class extends Controller { + connect() { + if (!this.isCookieAccepted()) { + this.element.classList = 'show'; + } else { + this.element.classList = 'd-none'; + } + } + + accept() { + // Устанавливаем куку на 1 год + const date = new Date(); + date.setFullYear(date.getFullYear() + 1); + document.cookie = `cookie_accepted=true; expires=${date.toUTCString()}; path=/`; + + this.element.classList = 'd-none'; + } + + isCookieAccepted() { + return document.cookie.split(';').some(cookie => + cookie.trim().startsWith('cookie_accepted=true') + ); + } +} \ No newline at end of file diff --git a/assets/controllers/datePicker_controller.js b/assets/controllers/datePicker_controller.js new file mode 100644 index 0000000..5539889 --- /dev/null +++ b/assets/controllers/datePicker_controller.js @@ -0,0 +1,175 @@ +import { Controller } from 'stimulus'; + +import daterangepicker from 'daterangepicker' +import 'daterangepicker/daterangepicker.css' + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="datePicker" attribute will cause + * this controller to be executed. The name "hello" comes from the filename: + * datePicker_controller.js -> "datePicker" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + connect() { + var locale = { + format: 'DD.MM.YYYY', + "applyLabel": "Ок", + "cancelLabel": "Отмена", + "fromLabel": "От", + "toLabel": "До", + "customRangeLabel": "Произвольный", + "daysOfWeek": [ + "Вс", + "Пн", + "Вт", + "Ср", + "Чт", + "Пт", + "Сб" + ], + "monthNames": [ + "Январь", + "Февраль", + "Март", + "Апрель", + "Май", + "Июнь", + "Июль", + "Август", + "Сентябрь", + "Октябрь", + "Ноябрь", + "Декабрь" + ], + firstDay: 1 + }; + + var date = new Date(); + var lastDate = new Date(date.getFullYear(), date.getMonth(), date.getDate() + 7); + + // Получаем значение даты из URL параметра или из атрибута value элемента + let currentDateValue = this.element.value || this.element.getAttribute('value'); + + // Если значение не установлено, пробуем получить из URL + if (!currentDateValue || currentDateValue.trim() === '') { + const urlParams = new URLSearchParams(window.location.search); + currentDateValue = urlParams.get('specialist_search[current_date]') || + urlParams.get('specialist_search%5Bcurrent_date%5D') || + decodeURIComponent(urlParams.get('specialist_search%5Bcurrent_date%5D') || ''); + } + + if (this.element.getAttribute('range') == 'true') { + let startDate = date; + let endDate = lastDate; + + // Если есть значение из URL или value, парсим его + if (currentDateValue && currentDateValue.trim() !== '') { + const dateRange = this.parseDateRange(currentDateValue); + if (dateRange && dateRange.start && dateRange.end) { + startDate = dateRange.start; + endDate = dateRange.end; + } + } + + // Форматируем даты для daterangepicker (формат DD.MM.YYYY) + const formattedStart = window.dateFormat(startDate, 'd.m.Y'); + const formattedEnd = window.dateFormat(endDate, 'd.m.Y'); + + const picker = jQuery(this.element).daterangepicker({ + locale: locale, + "startDate": formattedStart, + "endDate": formattedEnd + }, function(start, end, label) { + // Callback при изменении даты + }); + + // Устанавливаем значение в поле после инициализации + // daterangepicker автоматически форматирует значение согласно locale.format + // Используем setTimeout чтобы дать время daterangepicker инициализироваться + setTimeout(() => { + if (currentDateValue && currentDateValue.trim() !== '') { + this.element.value = currentDateValue.trim(); + } else { + this.element.value = `${formattedStart} - ${formattedEnd}`; + } + }, 100); + } else { + let selectedDate = date; + + // Если есть значение, парсим его + if (currentDateValue && currentDateValue.trim() !== '') { + const parsedDate = this.parseSingleDate(currentDateValue); + if (parsedDate) { + selectedDate = parsedDate; + this.element.value = currentDateValue; + } + } + + jQuery(this.element).daterangepicker({ + singleDatePicker: true, + autoApply: true, + showDropdowns: true, + minYear: 1930, + maxYear: new Date(), + locale: locale, + "startDate": window.dateFormat(selectedDate, 'd-m-Y') + }); + } + } + + parseDateRange(dateString) { + if (!dateString) return null; + + // Парсим формат "13.01.2026 - 20.01.2026" или "13.01.2026+-+20.01.2026" + const match = dateString.match(/^(.+?)\s*[-+]\s*(.+)$/); + if (!match) return null; + + const startStr = match[1].trim(); + const endStr = match[2].trim(); + + const startDate = this.parseDateString(startStr); + const endDate = this.parseDateString(endStr); + + if (!startDate || !endDate) return null; + + return { start: startDate, end: endDate }; + } + + parseSingleDate(dateString) { + if (!dateString) return null; + return this.parseDateString(dateString.trim()); + } + + parseDateString(dateStr) { + if (!dateStr) return null; + + // Формат "dd.mm.yyyy" + const match = dateStr.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/); + if (match) { + const day = parseInt(match[1], 10); + const month = parseInt(match[2], 10) - 1; // месяцы в JS начинаются с 0 + const year = parseInt(match[3], 10); + return new Date(year, month, day); + } + + // Формат "yyyy-mm-dd" + const match2 = dateStr.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/); + if (match2) { + const year = parseInt(match2[1], 10); + const month = parseInt(match2[2], 10) - 1; + const day = parseInt(match2[3], 10); + return new Date(year, month, day); + } + + // Пробуем стандартный парсинг + const parsed = new Date(dateStr); + if (!isNaN(parsed.getTime())) { + return parsed; + } + + return null; + } +} diff --git a/assets/controllers/docYuorHome_controller.js b/assets/controllers/docYuorHome_controller.js new file mode 100644 index 0000000..7c5576c --- /dev/null +++ b/assets/controllers/docYuorHome_controller.js @@ -0,0 +1,29 @@ +import { Controller } from 'stimulus'; + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="default" attribute will cause + * this controller to be executed. The name "default" comes from the filename: + * default_controller.js -> "default" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + connect() { + this.element.addEventListener('click', function () { + const popupWrap = document.getElementById('popup'); + popupWrap.querySelector('.modal-title').innerHTML = 'Вызов врача на дом'; + popupWrap.querySelector('.modal-dialog').classList.remove('modal-lg'); + const popupBody = popupWrap.querySelector('#popup-body'); + const iframe = document.createElement('iframe'); + iframe.src = '/widget/form/2'; + iframe.frameBorder = 0; + iframe.height = "500"; + iframe.width = "100%"; + popupBody.append(iframe); + + $(popupWrap).modal('show'); + }) + } +} diff --git a/assets/controllers/doc_controller.js b/assets/controllers/doc_controller.js new file mode 100644 index 0000000..b6447ff --- /dev/null +++ b/assets/controllers/doc_controller.js @@ -0,0 +1,33 @@ +import { Controller } from 'stimulus'; + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="default" attribute will cause + * this controller to be executed. The name "default" comes from the filename: + * default_controller.js -> "default" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + connect() { + var buttonReferenceModal = this.element.querySelector('button.show-reference-modal'); + + buttonReferenceModal.addEventListener('click', function () { + $.ajax({ + dataType: "HTML", + method: 'GET', + crossDomain: false, + url: '/widget/reference', + success(response) { + var popupWrap = document.getElementById('popup'); + popupWrap.querySelector('.modal-title').innerHTML = 'Налоговый вычет на лечение'; + popupWrap.querySelector('.modal-dialog').classList.remove('modal-lg'); + var popupBody = popupWrap.querySelector('#popup-body'); + popupBody.innerHTML = response; + $(popupWrap).modal('show'); + } + }); + }) + } +} diff --git a/assets/controllers/favoritesBtn_controller.js b/assets/controllers/favoritesBtn_controller.js new file mode 100644 index 0000000..6fba9b3 --- /dev/null +++ b/assets/controllers/favoritesBtn_controller.js @@ -0,0 +1,21 @@ +import { Controller } from 'stimulus'; +import Inputmask from "inputmask"; + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="hello" attribute will cause + * this controller to be executed. The name "hello" comes from the filename: + * hello_controller.js -> "hello" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + connect() { + var a = this.element + var favorites = localStorage.getItem('favorites'); + if (favorites) { + this.element.href = '/favorites/?q=' + favorites + } + } +} diff --git a/assets/controllers/favoritesNaw_controller.js b/assets/controllers/favoritesNaw_controller.js new file mode 100644 index 0000000..d89cb56 --- /dev/null +++ b/assets/controllers/favoritesNaw_controller.js @@ -0,0 +1,73 @@ +import { Controller } from 'stimulus'; +import Inputmask from "inputmask"; + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="hello" attribute will cause + * this controller to be executed. The name "hello" comes from the filename: + * hello_controller.js -> "hello" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + connect() { + var wrap = this.element + var favorites = localStorage.getItem('favorites'); + + if (typeof favorites === 'object') { + var favorites = ''; + } + + if (favorites) { + var left = 0; + + favorites.split(',').forEach(function(sid, key) { + $.ajax({ + dataType: "json", + method: "GET", + crossDomain: true, + url: "/api/doctor", + data: { + 'sid': sid, + }, + success(response) { + console.log(response, wrap.id) + + if (wrap.id == 'favorites-widget' && key < 8) { + var a = document.createElement('a'); + a.classList = 'staff-link'; + a.href= "/specialist/" + response.data.alias; + + var img = document.createElement('img'); + img.classList = "staff-block__img"; + img.style.borderRadius = '100%'; + img.stylebackgroundRepeat = "no-repeat"; + img.style.background = "url(" + response.data.img + ")"; + img.style.backgroundSize = "cover"; + img.style.backgroundPosition = "center 0px"; + + a.append(img); + wrap.append(a); + } else { + if (key < 4) { + var a = document.createElement('a'); + a.classList = 'favorites-link'; + a.href= "/specialist/" + response.data.alias; + + var div = document.createElement('img'); + div.classList = "img-vr"; + div.stylebackgroundRepeat = "no-repeat"; + div.style.background = "url(" + response.data.img + ")"; + div.style.backgroundSize = "cover"; + div.style.backgroundPosition = "center 0px"; + a.append(div); + wrap.append(a); + } + } + } + }); + }) + } + } +} diff --git a/assets/controllers/favorites_controller.js b/assets/controllers/favorites_controller.js new file mode 100644 index 0000000..3ca2e90 --- /dev/null +++ b/assets/controllers/favorites_controller.js @@ -0,0 +1,157 @@ +import { Controller } from 'stimulus'; +import Inputmask from "inputmask"; + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="hello" attribute will cause + * this controller to be executed. The name "hello" comes from the filename: + * hello_controller.js -> "hello" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + connect() { + function unic(item, colections) { + var add = true; + var del = false; + colections.forEach(function (val, key) { + if (val == item) { + add = false; + del = key; + } + }) + + if (add) { + colections.push(item) + } + + return colections + } + + let sid = this.element.dataset.sid; + let like = this.element.querySelector('.like'); + let dislike = this.element.querySelector('.dislike'); + + + var favorites = localStorage.getItem('favorites'); + + if (typeof favorites === 'object') { + var favorites = ''; + } + + if (favorites.split(',').indexOf(sid) !== -1) { + like.classList.add('d-none'); + dislike.classList.remove('d-none'); + this.update(); + } + + like.addEventListener('click', function(evn) { + var favorites = localStorage.getItem('favorites'); + var container = []; + + if (typeof favorites === 'object') { + var favorites = ''; + } + + if (favorites) { + favorites = favorites.split(',') + } else { + favorites = []; + } + + favorites.push(sid) + + favorites.forEach(function (key) { + container = unic(key, container); + }); + + localStorage.setItem('favorites', container.join()); + like.classList.add('d-none'); + dislike.classList.remove('d-none'); + this.update(); + alert('врач добавлен'); + }.bind(this)); + + dislike.addEventListener('click', function (evn) { + var favorites = localStorage.getItem('favorites'); + var favorites = favorites.replace( sid + ',', ''); + var favorites = favorites.replace( ',' + sid, ''); + var favorites = favorites.replace( sid, ''); + + localStorage.setItem('favorites', favorites); + like.classList.remove('d-none'); + dislike.classList.add('d-none'); + this.update(); + }.bind(this)) + } + + update() { + var wrap = document.getElementById('favoritesNaw'); + + if (wrap == null) { + return false; + } + + wrap.innerHTML = ""; + + var favorites = localStorage.getItem('favorites'); + var favoritesBtn = document.getElementById('favoritesBtn'); + + + if (typeof favorites === 'object') { + var favorites = ''; + } + + if (favorites) { + favoritesBtn.href = '/favorites/?q=' + favorites; + + var left = 0; + + favorites.split(',').forEach(function(sid, key) { + $.ajax({ + dataType: "json", + method: "GET", + crossDomain: true, + url: "/api/doctor", + data: { + 'sid': sid, + }, + success(response) { + if (wrap.id == 'favorites-widget' && key < 8) { + var a = document.createElement('a'); + a.classList = 'staff-link'; + a.href= "/specialist/" + response.data.alias; + + var img = document.createElement('img'); + img.classList = "staff-block__img"; + img.style.borderRadius = '100%'; + img.stylebackgroundRepeat = "no-repeat"; + img.style.background = "url(" + response.data.img + ")"; + img.style.backgroundSize = "cover"; + img.style.backgroundPosition = "center 0px"; + + a.append(img); + wrap.append(a); + } else { + if (key < 4) { + var a = document.createElement('a'); + a.classList = 'favorites-link'; + a.href= "/specialist/" + response.data.alias; + + var div = document.createElement('img'); + div.classList = "img-vr"; + div.stylebackgroundRepeat = "no-repeat"; + div.style.background = "url(" + response.data.img + ")"; + div.style.backgroundSize = "cover"; + div.style.backgroundPosition = "center 0px"; + a.append(div); + wrap.append(a); + } + } + } + }); + }) + } + } +} diff --git a/assets/controllers/filterMenu_controller.js b/assets/controllers/filterMenu_controller.js new file mode 100644 index 0000000..d162235 --- /dev/null +++ b/assets/controllers/filterMenu_controller.js @@ -0,0 +1,14 @@ +import { Controller } from 'stimulus'; + +export default class extends Controller { + connect() { + + $(this.element).on('click', function(e){ + $('.filter').addClass('active'); + }); + + $('.filter__close').on('click', function(){ + $('.filter').removeClass('active'); + }); + } +} diff --git a/assets/controllers/inputMask_controller.js b/assets/controllers/inputMask_controller.js new file mode 100644 index 0000000..a93645d --- /dev/null +++ b/assets/controllers/inputMask_controller.js @@ -0,0 +1,24 @@ +import { Controller } from 'stimulus'; +import Inputmask from "inputmask"; + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="hello" attribute will cause + * this controller to be executed. The name "hello" comes from the filename: + * hello_controller.js -> "hello" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + connect() { + var params = {mask: "+7(899)999-99-99", definitions: {'8': {validator: "[9]"}}} + + if (this.element.dataset.mask) { + params = {mask: this.element.dataset.mask} + } + + var im = new Inputmask(params); + im.mask(this.element); + } +} diff --git a/assets/controllers/jivo_controller.js b/assets/controllers/jivo_controller.js new file mode 100644 index 0000000..15b03cb --- /dev/null +++ b/assets/controllers/jivo_controller.js @@ -0,0 +1,30 @@ +import { Controller } from 'stimulus'; +import Cookies from 'js-cookie'; + +export default class extends Controller { + connect() { + try { + const region = Cookies.get('region'); + + if (!region) { + console.warn('Region cookie not found, using default Jivo widget'); + } + + const widgetUrl = this.getSource(region); + + this.element.src = widgetUrl; + } catch (error) { + console.error('Failed to load Jivo widget:', error); + } + } + + getSource(region) { + const normalizedRegion = region ? String(region).trim() : ''; + + switch (normalizedRegion) { + case '93': return 'https://code.jivo.ru/widget/EMlWlFXUZB'; // Воронеж + case '94': return 'https://code.jivo.ru/widget/adPLvIW8rT'; // Краснодар + default: return 'https://code.jivo.ru/widget/IPQcFAX6b5'; // Саратов, Волгоград + } + } +} \ No newline at end of file diff --git a/assets/controllers/kinderFilter_controller.js b/assets/controllers/kinderFilter_controller.js new file mode 100644 index 0000000..ac27bb2 --- /dev/null +++ b/assets/controllers/kinderFilter_controller.js @@ -0,0 +1,205 @@ +import { Controller } from 'stimulus'; +const helper = require("./../components/helper.js"); + +/* + * Контроллер для динамического обновления списка специализаций + * при изменении фильтра "Детский врач" / "Взрослый врач" + */ +export default class extends Controller { + connect() { + const kinderSelect = this.element.querySelector('[id*="kinder"]'); + const departmentSelect = this.element.querySelector('[id*="department"]'); + + if (!kinderSelect || !departmentSelect) { + return; + } + + // Сохраняем ссылку на departmentSelect для использования в методах + this.departmentSelect = departmentSelect; + + // Обработчик изменения поля kinder + // Используем событие change от bootstrap-select, если оно доступно + const handleKinderChange = () => { + // Небольшая задержка, чтобы bootstrap-select успел обновить значение + setTimeout(() => { + const kinderValue = kinderSelect.value || ''; + console.log('Kinder changed to:', kinderValue); + this.updateDepartments(departmentSelect, kinderValue); + }, 100); + }; + + // Подписываемся на событие change + kinderSelect.addEventListener('change', handleKinderChange); + + // Также подписываемся на событие от bootstrap-select, если оно есть + if (typeof $ !== 'undefined' && $(kinderSelect).data('selectpicker')) { + $(kinderSelect).on('changed.bs.select', handleKinderChange); + } + } + + updateDepartments(departmentSelect, kinderValue) { + const kinder = kinderValue === '1' ? 1 : null; + + // Сохраняем текущее выбранное значение перед обновлением + const selectedValue = departmentSelect.value; + + // Показываем индикатор загрузки + const originalHtml = departmentSelect.innerHTML; + departmentSelect.disabled = true; + + // Сохраняем состояние bootstrap-select + const isSelectpicker = typeof $ !== 'undefined' && $(departmentSelect).data('selectpicker'); + const hasLiveSearch = departmentSelect.hasAttribute('data-live-search'); + + // Уничтожаем selectpicker, если он инициализирован + if (isSelectpicker) { + try { + $(departmentSelect).selectpicker('destroy'); + // Удаляем data-атрибут, чтобы selectpicker_controller не пытался его инициализировать снова + $(departmentSelect).removeData('selectpicker'); + // Удаляем классы и элементы, которые мог создать selectpicker + $(departmentSelect).next('.bootstrap-select').remove(); + } catch (e) { + console.warn('Ошибка при уничтожении selectpicker:', e); + } + } + + departmentSelect.innerHTML = ''; + + // Получаем список специализаций через API + const data = { + kinder: kinder || 0 + }; + + helper.sendRequest(data, "/api/departments", "GET").then((response) => { + console.log('API Response:', response); + + // Очищаем текущие опции + departmentSelect.innerHTML = ''; + + // Добавляем placeholder + const placeholderOption = document.createElement('option'); + placeholderOption.value = ''; + placeholderOption.textContent = 'Все специализации'; + departmentSelect.appendChild(placeholderOption); + + // Добавляем новые опции + let foundSelected = false; + if (response && response.data && Array.isArray(response.data)) { + if (response.data.length > 0) { + console.log('Добавляем', response.data.length, 'специализаций'); + response.data.forEach((dept) => { + const option = document.createElement('option'); + option.value = String(dept.did); + option.textContent = dept.name; + + // Проверяем, есть ли ранее выбранное значение в новом списке + if (selectedValue && selectedValue === String(dept.did)) { + option.selected = true; + foundSelected = true; + } + + departmentSelect.appendChild(option); + }); + console.log('Опции добавлены, всего опций:', departmentSelect.options.length); + } else { + console.warn('Нет данных о специализациях - пустой массив'); + } + } else { + console.error('Неверный формат ответа от API:', response); + } + + // Если ранее выбранное значение не найдено в новом списке, сбрасываем выбор + if (selectedValue && !foundSelected) { + departmentSelect.value = ''; + } + + // Проверяем, что опции действительно добавлены + console.log('Проверка перед инициализацией selectpicker:'); + console.log('- Количество опций в select:', departmentSelect.options.length); + console.log('- HTML select:', departmentSelect.outerHTML.substring(0, 200)); + + // Восстанавливаем bootstrap-select + // Используем setTimeout для гарантии, что DOM обновлен + setTimeout(() => { + try { + if (typeof $ !== 'undefined' && $(departmentSelect).length > 0) { + // Проверяем, что опции все еще есть + if (departmentSelect.options.length === 0) { + console.error('ОШИБКА: Опции не найдены перед инициализацией selectpicker!'); + return; + } + + // Убеждаемся, что старый selectpicker полностью удален + const $select = $(departmentSelect); + if ($select.data('selectpicker')) { + console.log('Найден старый selectpicker, уничтожаем'); + try { + $select.selectpicker('destroy'); + } catch (e) { + console.warn('Ошибка при destroy:', e); + } + } + + // Удаляем все элементы bootstrap-select из DOM + $select.next('.bootstrap-select').remove(); + $select.siblings('.bootstrap-select').remove(); + + // Удаляем data-атрибуты + $select.removeData('selectpicker'); + + // Всегда инициализируем заново + const selectpickerOptions = { + noneSelectedText: 'Все специализации' + }; + + // Добавляем liveSearch, если он был изначально + if (hasLiveSearch) { + selectpickerOptions.liveSearch = true; + } + + console.log('Инициализируем selectpicker с опциями:', selectpickerOptions); + console.log('Количество опций перед инициализацией:', departmentSelect.options.length); + + // Инициализируем selectpicker + $select.selectpicker(selectpickerOptions); + + // Проверяем, что selectpicker инициализирован + if ($select.data('selectpicker')) { + console.log('Selectpicker успешно инициализирован'); + // Принудительно обновляем отображение + $select.selectpicker('render'); + console.log('Selectpicker отрендерен'); + } else { + console.error('ОШИБКА: Selectpicker не инициализирован после вызова!'); + } + } else { + console.error('jQuery не доступен или элемент не найден'); + } + } catch (e) { + console.error('Ошибка при инициализации bootstrap-select:', e); + console.error('Stack trace:', e.stack); + } + }, 200); + + departmentSelect.disabled = false; + }).catch((error) => { + console.error('Ошибка при загрузке специализаций:', error); + departmentSelect.innerHTML = originalHtml; + departmentSelect.disabled = false; + + // Восстанавливаем bootstrap-select даже при ошибке + setTimeout(() => { + if (typeof $ !== 'undefined') { + const selectpickerOptions = { + noneSelectedText: 'Все специализации' + }; + if (hasLiveSearch) { + selectpickerOptions.liveSearch = true; + } + $(departmentSelect).selectpicker(selectpickerOptions); + } + }, 150); + }); + } +} diff --git a/assets/controllers/menu_controller.js b/assets/controllers/menu_controller.js new file mode 100644 index 0000000..f56c321 --- /dev/null +++ b/assets/controllers/menu_controller.js @@ -0,0 +1,190 @@ +import { Controller } from 'stimulus'; +import Cookies from 'js-cookie'; + +const loader = require("./../components/loader.js"); +const helper = require("./../components/helper.js"); + +/* + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + connect() { + $(document.querySelector('.login-menu')).on('click', function(){ + $(this).toggleClass('active'); + }); + + $(document.querySelector('.burger')).on('click', function(){ + $(this).toggleClass('active'); + $('.left-sidebar').toggleClass('menu-active'); + }); + + loader.loadSDK('menu').then(function(webSDK) { + webSDK.on('init', function() { + if (this.data.user.authenticated) { + runWebSDK(webSDK); + } + }); + }) + + let runWebSDK = function (webSDK) { + this.bonus(webSDK); + + if (location.pathname != '/case-history') { + this.caseHistory(webSDK); + } + }.bind(this) + + this.tel().catch(error => { + console.error('Ошибка в tel():', error); + }); + } + + caseHistory(webSDK) { + let countRecord = document.getElementById('countRecord'); + + window.webSDK.loadScheduleRecList({ + st: 20170101, + en: window.dateFormat(new Date((new Date()).getFullYear(), (new Date()).getMonth() + 6, 0)), + start: 0, + length: 500 + }).then(function (resolve) { + if (countRecord) { + if (resolve.data.length > 0) { + var count = 0; + + for (var i = 0; i < resolve.data.length; i++) { + if (resolve.data[i].workDate >= window.dateFormat(new Date())) { + count++; + } + } + + countRecord.innerHTML = count; + if (count !== 0) { + countRecord.classList.remove('d-none'); + } + + if (location.pathname == '/') { + var caseHistoryWidget = document.getElementById('case-history-widget'); + caseHistoryWidget.innerHTML = ''; + + if (resolve.data.length > 0) { + var count = 0; + + for (var i = 0; i < resolve.data.length; i++) { + if (resolve.data[i].workDate >= window.dateFormat(new Date()) && count < 4) { + var date = window.newDate(resolve.data[i].workDate); + var item = document.createElement('span'); + item.innerHTML = window.getWeekDay(date) + ', ' + window.dateFormat(date, 'd-m-Y') + ' ' + resolve.data[i].startTime; + item.classList = 'line-item'; + caseHistoryWidget.append(item); + count++; + } + } + + if (count == 0) { + var item = document.createElement('span'); + item.innerHTML = 'Записей не найдено'; + item.classList = 'line-item empty-item'; + caseHistoryWidget.append(item); + } + + } else { + var item = document.createElement('span'); + item.innerHTML = 'Записей не найдено'; + item.classList = 'line-item empty-item'; + caseHistoryWidget.append(item); + } + } + } + } + }); + } + + bonus() { + var bonusWidget = document.getElementById('bonus-widget'); + var bonusMenu = document.getElementById('bonus-menu'); + + window.webSDK.loadBonusList().then(function (resolve) { + if (resolve.length > 0) { + if (typeof resolve[0]['amountrub'] !== 'undefined') { + if (bonusMenu) { + bonusMenu.innerHTML = resolve[0]['amountrub']; + bonusMenu.classList.remove('d-none'); + } + + if (bonusWidget) { + bonusWidget.innerHTML = resolve[0]['amountrub']; + } + } + } + }); + } + + signOut(event) { + if (event) { + event.preventDefault(); + } + + const pcode = String(window?.webSDK?.data?.user?.id || ''); + const body = new URLSearchParams(); + if (pcode) { + body.append('pcode', pcode); + } + + fetch('/api/usrlog/logout', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + }, + credentials: 'same-origin', + keepalive: true, + body: body.toString() + }).catch(() => { + // + }).finally(() => { + window.webSDK.logout(); + window.location.href = '/logout'; + }); + } + + async tel() { + const apiUrl = helper.getApiHostname(); + const regionId = parseInt(Cookies.get('region')); + + try { + const response = await fetch(`${apiUrl}/filial/list?regionId=${regionId}`); + if (!response.ok) { + throw new Error(`Response status: ${response.status}`); + } + + const result = await response.json(); + const btnCallbackClinic = document.getElementById('btn-callback-clinic'); + const phone = result.data?.[0]?.phone; + + if (!btnCallbackClinic || !phone) { + return; + } + + const digits = String(phone).replace(/\D/g, ''); + const normalizedDigits = digits.length === 11 && digits.startsWith('8') + ? `7${digits.slice(1)}` + : digits; + + const displayPhone = normalizedDigits.length === 11 && normalizedDigits.startsWith('7') + ? `+7(${normalizedDigits.slice(1, 4)})${normalizedDigits.slice(4, 7)}-${normalizedDigits.slice(7, 9)}-${normalizedDigits.slice(9, 11)}` + : String(phone).trim(); + + // Делаем номер доступным для других частей фронта (например, карточки специалистов) + window.clinicPhone = displayPhone; + window.clinicPhoneRaw = normalizedDigits; + + btnCallbackClinic.innerText = displayPhone; + btnCallbackClinic.href = normalizedDigits ? `tel:+${normalizedDigits}` : `tel:${displayPhone}`; + + } catch (error) { + console.error(error.message); + } + } +} + + diff --git a/assets/controllers/mobileSearchOrderByInput_controller.js b/assets/controllers/mobileSearchOrderByInput_controller.js new file mode 100644 index 0000000..0d026fb --- /dev/null +++ b/assets/controllers/mobileSearchOrderByInput_controller.js @@ -0,0 +1,43 @@ +import { Controller } from 'stimulus'; + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="searchOrderByInput" attribute will cause + * this controller to be executed. The name "searchOrderByInput" comes from the filename: + * searchOrderByInput_controller.js -> "searchOrderByInput" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + connect() { + var val = document.getElementById("specialist_search_order_by").value; + var tabs = this.element; + if (val) { + check(val, false); + } + + tabs.addEventListener('change', function(evn) { + check(evn.target.value, true); + }) + + function check(index, redirect) { + console.log(index) + var array = index.split('.'); + var id = array[0]; + var sort = array[1]; + + tabs.forEach(function(item) { + if (item.value == id) { + item.selected = 'true'; + } + }); + + document.getElementById("specialist_search_order_by").value = index + '.asc'; + + if (redirect) { + document.querySelector('.submit-filter').click(); + } + }; + } +} diff --git a/assets/controllers/modal_controller.js b/assets/controllers/modal_controller.js new file mode 100644 index 0000000..7109c39 --- /dev/null +++ b/assets/controllers/modal_controller.js @@ -0,0 +1,49 @@ +import { Controller } from 'stimulus'; + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="default" attribute will cause + * this controller to be executed. The name "default" comes from the filename: + * default_controller.js -> "default" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + connect() { + this.element.querySelector('.full-scren-modal').classList.add('d-none'); + + var popup = this.element; + popup.addEventListener('click', function (ev) { + var closeBtn = ev.target.closest && ev.target.closest('[data-qa="close-button"]'); + if (!closeBtn || !popup.contains(closeBtn)) { + return; + } + if (typeof $ !== 'undefined' && typeof $(popup).modal === 'function') { + $(popup).modal('hide'); + } + if (window.location.hash === '#pay-success') { + history.replaceState(null, '', window.location.pathname + window.location.search); + } + }, true); + } + + fullScreen() { + var btn = this.element.querySelector('.full-scren-modal') + if (btn.classList.contains('fa-window-maximize')) { + btn.classList.add('fa-window-minimize'); + btn.classList.remove('fa-window-maximize'); + this.element.style.paddingRight = 0; + this.element.querySelector('.modal-dialog').style.maxWidth = window.innerWidth + 'px'; + this.element.querySelector('.modal-dialog').style.height = window.innerHeight + 'px'; + this.element.querySelector('.modal-dialog').style.margin = 0; + } else { + btn.classList.remove('fa-window-minimize'); + btn.classList.add('fa-window-maximize'); + this.element.style.paddingRight = ''; + this.element.querySelector('.modal-dialog').style.maxWidth = ''; + this.element.querySelector('.modal-dialog').style.height = ''; + this.element.querySelector('.modal-dialog').style.margin = ''; + } + } +} diff --git a/assets/controllers/passwordShow_controller.js b/assets/controllers/passwordShow_controller.js new file mode 100644 index 0000000..907f86e --- /dev/null +++ b/assets/controllers/passwordShow_controller.js @@ -0,0 +1,38 @@ +import { Controller } from 'stimulus'; + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="default" attribute will cause + * this controller to be executed. The name "default" comes from the filename: + * default_controller.js -> "default" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + connect() { + var eye = document.createElement('i'); + eye.setAttribute('aria-hidden', true); + eye.classList = 'fa fa-eye-slash'; //fa-eye + eye.style.position = 'absolute'; + eye.dataset.action = 'click->passwordShow#toggle' + eye.style.top = '10px'; + eye.style.right = '10px'; + + this.element.style.position = 'relative'; + this.element.append(eye); + } + + toggle() { + var btn = this.element.querySelector('input'); + var eye = this.element.querySelector('i'); + + if (eye.classList.contains('fa-eye-slash')) { + eye.classList = 'fa fa-eye'; + btn.type = 'text'; + } else { + eye.classList = 'fa fa-eye-slash'; + btn.type = 'password'; + } + } +} diff --git a/assets/controllers/payment_controller.js b/assets/controllers/payment_controller.js new file mode 100644 index 0000000..6146e9c --- /dev/null +++ b/assets/controllers/payment_controller.js @@ -0,0 +1,108 @@ +import { Controller } from 'stimulus'; +const loader = require("./../components/loader.js"); +const helper = require("./../components/helper.js"); + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="payment" attribute will cause + * this controller to be executed. The name "payment" comes from the filename: + * payment_controller.js -> "payment" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + connect() { + loader.loadSDK('payment').then(function(webSDK) { + webSDK.on('init', function() { + if (this.data.user.authenticated) { + runWebSDK(); + } else { + window.location.pathname = '/logout' + } + }); + }) + + let runWebSDK = function () { + this.finance(); + }.bind(this) + } + + finance() { + var securityPayment = document.getElementById('security-payment'); + + webSDK.loadPaymentList({ + start: 0, + length: 50 + }).then(function (resolve) { + var paymentWrap = securityPayment.querySelector('.payment-wrap'); + + if (resolve.length > 0) { + resolve.forEach(function(item, index) { + var paymentShow = securityPayment.querySelector('.payment-item').cloneNode(true); + + paymentShow.querySelector('.pay-id').innerHTML = item.id; + paymentShow.querySelector('.address').innerHTML = item.filialName; + paymentShow.querySelector('.comment').innerHTML = item.comment; + paymentShow.querySelector('.specialist').innerHTML = item.doctor; + paymentShow.querySelector('.service').innerHTML = item.service; + + var date = window.newDate(item.date); + paymentShow.querySelector('.month').innerHTML = getWeekDay(date); + paymentShow.querySelector('.date').innerHTML = window.dateFormat(date, 'd-m-Y') + paymentShow.querySelector('.pay-date').innerHTML = window.dateFormat(date, 'd-m-Y') + + paymentShow.querySelectorAll('.amt').forEach(function (el) { + el.innerHTML = item.amt; + + if (item.status.id == 0) { + paymentShow.querySelector('.pay').querySelector('.button-revers').classList.remove('d-none'); + } else { + paymentShow.querySelector('.pay').querySelector('.button-revers').classList.add('d-none'); + paymentShow.querySelector('.pay').querySelector('.price').classList.remove('d-none'); + } + }); + + paymentShow.querySelector('.pay').querySelector('.button-revers').dataset.orderId = item.id; + paymentShow.querySelector('.pay').querySelector('.button-revers').dataset.filial = item.filial; + paymentShow.querySelector('.pay').querySelector('.button-revers').dataset.amt = item.amt; + paymentShow.querySelector('.pay').querySelector('.button-revers').dataset.payprofileid = item.magazineId; + + paymentShow.querySelector('.pay').querySelector('.button-revers').addEventListener('click', function (evn) { + var popup = document.getElementById('popup'); + var params = { + 'orderid': Number(this.dataset.orderId) , + 'payprofileid': this.dataset.payprofileid, + 'payamount': this.dataset.amt, + // 'paymethod': 'QW', + 'filial': Number(this.dataset.filial), + 'pcode': webSDK.data.user.representId, + 'successurl': document.location.origin + '/payment?pay=true', + 'errorurl': document.location.origin + '/payment?pay=false', + 'containerId': 'popup-body', + }; + + webSDK.loadPaymentView(params); + popup.querySelector('.modal-body').innerHTML = ''; + popup.querySelector('.modal-title').innerHTML = 'Оплата'; + + $(popup).modal('show'); + }) + + paymentShow.classList.add('mt-3') + paymentShow.classList.remove('d-none') + + if (index == 0) { + paymentWrap.innerHTML = ""; + } + + paymentWrap.appendChild(paymentShow); + }) + } + }).catch(function (e) { + helper.sendRequest({ + data: {'error': e, method: 'loadPaymentList'} + }, helper.getHostname() + '/api/log', "POST", "json", true, "application/json"); + }) + } +} \ No newline at end of file diff --git a/assets/controllers/priceList_controller.js b/assets/controllers/priceList_controller.js new file mode 100644 index 0000000..eb4f7e2 --- /dev/null +++ b/assets/controllers/priceList_controller.js @@ -0,0 +1,51 @@ +import { Controller } from 'stimulus'; +const loader = require("./../components/loader.js"); +const helper = require("./../components/helper.js"); + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="priceList" attribute will cause + * this controller to be executed. The name "priceList" comes from the filename: + * priceList_controller.js -> "priceList" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + connect() { + var wrap = this.element; + var priceListSelect = document.getElementById('price_list_admin_form_groupId'); + var select = priceListSelect.cloneNode(true); + select.id = 'price_list_update_form'; + select.name = 'groupId'; + + var priceList = this.element.querySelector('.group-update'); + priceList.appendChild(select); + + var btnUpdatePriceList = this.element.querySelector('#update-price-list'); + + btnUpdatePriceList.addEventListener('click', function() { + if (select.value == '') { + select.classList.add('is-invalid'); + } else { + select.classList.remove('is-invalid'); + var msg = wrap.querySelector('.msg'); + msg.innerHTML = "Пожалуйста ждите, идет обновление цен!"; + msg.classList.add('text-danger'); + + loader.btnLoader(btnUpdatePriceList, true); + helper.sendRequest({groupId: select.value}, '/update/price-list', "POST").then(function (response) { + loader.btnLoader(btnUpdatePriceList, false); + + if (response.status == true) { + msg.innerHTML = "Обновление успешно завершено!
Нажмите на кнопку \"Search\" для отображения в таблице."; + msg.classList.remove('text-danger'); + msg.classList.add('text-success'); + } else { + msg.innerHTML = "Упс, что то пошло не так, попробуйте позже или обратитесь к Администратору."; + } + }) + } + }); + } +} diff --git a/assets/controllers/quickDateRange_controller.js b/assets/controllers/quickDateRange_controller.js new file mode 100644 index 0000000..089884e --- /dev/null +++ b/assets/controllers/quickDateRange_controller.js @@ -0,0 +1,71 @@ +import { Controller } from 'stimulus'; + +export default class extends Controller { + static targets = ['input']; + + today(event) { + event?.preventDefault?.(); + + const today = this.startOfDay(new Date()); + this.setRange(today, today); + } + + tomorrow(event) { + event?.preventDefault?.(); + + const tomorrow = this.addDays(this.startOfDay(new Date()), 1); + this.setRange(tomorrow, tomorrow); + } + + setRange(startDate, endDate) { + const input = this.inputTarget; + const formattedStart = this.formatDate(startDate); + const formattedEnd = this.formatDate(endDate); + const value = `${formattedStart} - ${formattedEnd}`; + + // If daterangepicker is attached, keep it in sync. + if (typeof $ !== 'undefined') { + const picker = $(input).data('daterangepicker'); + if (picker) { + picker.setStartDate(formattedStart); + picker.setEndDate(formattedEnd); + + // Ensure UI is refreshed for both calendars & input. + if (typeof picker.updateCalendars === 'function') { + picker.updateCalendars(); + } + if (typeof picker.updateView === 'function') { + picker.updateView(); + } + } + } + + input.value = value; + input.dispatchEvent(new Event('change', { bubbles: true })); + + const form = this.element.closest('form'); + if (form) { + if (typeof form.requestSubmit === 'function') { + form.requestSubmit(); + } else { + form.submit(); + } + } + } + + startOfDay(date) { + return new Date(date.getFullYear(), date.getMonth(), date.getDate()); + } + + addDays(date, days) { + return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days); + } + + formatDate(date) { + const dd = String(date.getDate()).padStart(2, '0'); + const mm = String(date.getMonth() + 1).padStart(2, '0'); + const yyyy = String(date.getFullYear()); + return `${dd}.${mm}.${yyyy}`; + } +} + diff --git a/assets/controllers/reference_controller.js b/assets/controllers/reference_controller.js new file mode 100644 index 0000000..d406e9b --- /dev/null +++ b/assets/controllers/reference_controller.js @@ -0,0 +1,599 @@ +import { Controller } from 'stimulus'; +const validator = require("./../components/validator.js"); +const loader = require("./../components/loader.js"); +const helper = require("./../components/helper.js"); + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="default" attribute will cause + * this controller to be executed. The name "default" comes from the filename: + * default_controller.js -> "default" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + connect() { + var referenceWrapper = this.element; + + window.addEventListener("mousemove",(e) => { + window.parent.postMessage({ type: "user_mousemove" }, "*"); + }); + + window.addEventListener("input",(e) => { + window.parent.postMessage({ type: "user_typing" }, "*"); + }); + + let evnRender = function (responsibleUser, countFilial) { + this.renderResponsibleUser(responsibleUser, countFilial); + }.bind(this) + + var changeResponsible = function (countFilial, value) { + var responsibleFilial = referenceWrapper.querySelector('.responsible_filial_' + countFilial); + var responsibleUser = responsibleFilial.querySelector('.responsible_user'); + + if (value == 1) { + responsibleUser.innerHTML = ''; + } else { + evnRender(responsibleUser, countFilial) + } + } + + referenceWrapper.querySelector('.reference-responsible').addEventListener('change', function() { + changeResponsible(this.dataset.count, this.value); + }) + + let evnRenderSending = function (referenceSending) { + this.renderSending(referenceSending); + }.bind(this) + + const addFilial = referenceWrapper.querySelector('button.add-filial'); + const referenceFilial = referenceWrapper.querySelector('#reference_filial'); + const referenceFilialSending = referenceWrapper.querySelector('#reference_filialSending'); + const referenceSending = referenceWrapper.querySelector('#reference_sending'); + const filialSendingWrapper = referenceWrapper.querySelector('.filial-sending'); + + const syncSendingUi = () => { + if (!referenceSending || !filialSendingWrapper) return; + + // Разрешённые варианты: + // 1 = "Лично в клинике" + // 2 = "Отправка в налоговую инспекцию" + // Любое другое/невалидное значение считаем "1". + const sendingSafe = (referenceSending.value === '2' || referenceSending.value === 2) ? '2' : '1'; + + // "Лично в клинике" => 1 => показываем выбор клиники получения справки + filialSendingWrapper.classList.toggle('d-none', sendingSafe !== '1'); + + // При "Лично" синхронизируем филиал получения. + // Если filialSending ещё не выбран (пустой) — НЕ блокируем, чтобы можно было выбрать вручную. + if (sendingSafe === '1') { + if (referenceFilial.selectedIndex > 0) { + referenceFilialSending.selectedIndex = referenceFilial.selectedIndex; + } + + referenceFilialSending.disabled = Boolean(referenceFilialSending.value); + } else { + referenceFilialSending.disabled = false; + } + }; + + if (referenceSending) { + referenceSending.addEventListener('change', syncSendingUi); + } + syncSendingUi(); + + referenceFilial.addEventListener('change', function(evn) { + // Если выдача "Лично в клинике" — синхронизируем филиал получения + const sendingSafe = (referenceSending && (referenceSending.value === '2' || referenceSending.value === 2)) ? '2' : '1'; + if (sendingSafe === '1') { + if (referenceFilial.selectedIndex > 0) { + referenceFilialSending.selectedIndex = referenceFilial.selectedIndex; + } + referenceFilialSending.disabled = Boolean(referenceFilialSending.value); + } else { + referenceFilialSending.selectedIndex = 0; + referenceFilialSending.disabled = false; + } + addFilial.classList.remove('d-none'); + }); + + addFilial.addEventListener('click', function() { + var countFilial = parseInt(this.dataset.count, 10); + var responsibleFilial = referenceWrapper.querySelector('.responsible_filial_' + countFilial) + var newFilialCount = countFilial + 1 + this.dataset.count = newFilialCount; + var newFilial = referenceWrapper.querySelector('.responsible_filial_' + countFilial).cloneNode(true); + newFilial.classList = 'responsible_filials responsible_filial_' + newFilialCount; + newFilial.querySelector('.reference-filial').id = 'reference_filial_' + newFilialCount; + newFilial.querySelector('.reference-filial').classList = 'reference-filial form-control'; + newFilial.querySelector('.valid-reference_filial').innerHTML = ''; + newFilial.querySelector('.responsible_user').innerHTML = ''; + newFilial.querySelector('.reference-responsible').id = 'reference_responsible_' + newFilialCount; + + newFilial.querySelector('.reference-period-first').id = 'reference_periodFirst_' + newFilialCount; + newFilial.querySelector('.reference-period-first').classList = 'reference-period-first form-control'; + + newFilial.querySelector('.reference-period-last').id = 'reference_periodLast_' + newFilialCount; + newFilial.querySelector('.reference-period-last').classList = 'reference-period-last form-control'; + + newFilial.querySelector('.reference-responsible').addEventListener('change', function() { + changeResponsible(newFilialCount, this.value); + }) + + responsibleFilial.parentNode.insertBefore(newFilial, responsibleFilial.nextSibling); + }) + var crmFields = {}; + + helper.sendRequest( + [], + 'https://sovamed.bitrix24.ru/rest/10998/3hrv38rzo3khchj3/crm.lead.fields.json', + 'GET', + 'json', + false, + 'application/json' + ).then(function(response) { + crmFields = response; + }); + + var submit = referenceWrapper.querySelector('button.submit'); + + submit.addEventListener('click', function() { + evnClick(referenceWrapper, crmFields); + }) + + let evnClick = function (wrapper, crmFields) { + var result = this.submit(wrapper); + var params = { + 'fields' : { + 'TITLE': 'Получение документов для налогового вычета', + 'NAME': result.options.autorName, + 'PHONE': [{ + 'VALUE': result.options.phone, + 'VALUE_TYPE': 'WORK' + }], + 'ASSIGNED_BY_ID' : 506, + 'UF_CRM_1539951158': helper.getCityId(result.options.filials[0].filial), + 'UF_CRM_1565783329': result.options.inn, + 'UF_CRM_1658495790': '', + 'UF_CRM_1565783428': result.options.filials[0].periodFirst + " - " + result.options.filials[0].periodLast, + 'UF_CRM_1565783258': result.options.birthDate, + } + }; + + const normalizeFilialValue = (value) => { + if (typeof value !== 'string') return ''; + return value + .replaceAll('\u00A0', ' ') // NBSP -> space + .replaceAll('.', '') // "г.Саратов" == "г. Саратов", "д. 33" == "д 33" + .replace(/\s+/g, ' ') + .replace(/\s*,\s*/g, ', ') + .trim() + .toLowerCase(); + }; + + const filialValue = result.options.filials?.[0]?.filial ?? ''; + const filialValueNormalized = normalizeFilialValue(filialValue); + + // 1) точное совпадение после нормализации + crmFields.result.UF_CRM_1658495790.items.forEach(function (el) { + if (params.fields['UF_CRM_1658495790']) return; + if (filialValueNormalized && filialValueNormalized === normalizeFilialValue(el.VALUE)) { + params.fields['UF_CRM_1658495790'] = el.ID; + } + }); + + // 2) запасной вариант: если Bitrix хранит VALUE без доп. суффикса "(...)" и т.п. + if (!params.fields['UF_CRM_1658495790'] && filialValueNormalized) { + const filialValueNoParens = filialValueNormalized.replace(/\s*\([^)]*\)\s*/g, ' ').replace(/\s+/g, ' ').trim(); + crmFields.result.UF_CRM_1658495790.items.forEach(function (el) { + if (params.fields['UF_CRM_1658495790']) return; + const bitrixValue = normalizeFilialValue(el.VALUE); + if (!bitrixValue) return; + if (bitrixValue === filialValueNoParens || filialValueNoParens.includes(bitrixValue) || bitrixValue.includes(filialValueNoParens)) { + params.fields['UF_CRM_1658495790'] = el.ID; + } + }); + } + + if (result.options.filials[0].responsible == 0) { + params.fields['UF_CRM_1565784083'] = 400 //[{"ID": "400","VALUE": "за другого пациента"}]; + params.fields['UF_CRM_1654588779'] = result.options.filials[0].relation; + params.fields['UF_CRM_1565784170'] = result.options.filials[0].birthDate; + params.fields['UF_CRM_1565784142'] = result.options.filials[0].fio; + } else { + params.fields['UF_CRM_1565784083'] = 398 //[{"ID": "398","VALUE": "себя"}]; + params.fields['UF_CRM_1654588779'] = 'за себя'; + + params.fields['UF_CRM_1565784170'] = result.options.birthDate; + params.fields['UF_CRM_1565784142'] = result.options.autorName; + } + + const sendingSafe = (result.options.sending == 2 || result.options.sending === '2') ? 2 : 1; + + if (sendingSafe == 1) { + params.fields['UF_CRM_1658499141'] = 2494 //[{"ID": "2494","VALUE": "на руки"}] + + const filialSendingValue = result.options.filialSending ?? ''; + const filialSendingValueNormalized = normalizeFilialValue(filialSendingValue); + + // 1) точное совпадение после нормализации + crmFields.result.UF_CRM_1658497553.items.forEach(function (el) { + if (params.fields['UF_CRM_1658497553']) return; + if (filialSendingValueNormalized && filialSendingValueNormalized === normalizeFilialValue(el.VALUE)) { + params.fields['UF_CRM_1658497553'] = el.ID; + } + }); + + // 2) запасной вариант: совпадение без суффикса "(...)" и т.п. + if (!params.fields['UF_CRM_1658497553'] && filialSendingValueNormalized) { + const filialSendingValueNoParens = filialSendingValueNormalized + .replace(/\s*\([^)]*\)\s*/g, ' ') + .replace(/\s+/g, ' ') + .trim(); + + crmFields.result.UF_CRM_1658497553.items.forEach(function (el) { + if (params.fields['UF_CRM_1658497553']) return; + const bitrixValue = normalizeFilialValue(el.VALUE); + if (!bitrixValue) return; + if (bitrixValue === filialSendingValueNoParens || filialSendingValueNoParens.includes(bitrixValue) || bitrixValue.includes(filialSendingValueNoParens)) { + params.fields['UF_CRM_1658497553'] = el.ID; + } + }); + } + } else if (sendingSafe == 2) { + params.fields['UF_CRM_1658499141'] = 4680 //[{"ID": "4680","VALUE": "отправка в налоговую инспекцию"}] + } + + if (Object.keys(result.options.filials).length > 1) { + var comment = ""; + + for (var index = 0; index < Object.keys(result.options.filials).length; index++) { + if (index !== 0) { + comment += "Дополнительно:\n\t" + if (result.options.filials[index].responsible == 0) { + comment += 'Степень родства: ' + result.options.filials[index].relation + "\n\t"; + comment += 'ФИО: ' + result.options.filials[index].fio + "\n\t"; + comment += 'Дата рождения: ' + result.options.filials[index].birthDate + "\n\t"; + } else { + comment += "За себя\n\t"; + comment += 'ФИО: ' + result.options.autorName + "\n\t"; + comment += 'Дата рождения: ' + result.options.birthDate + "\n\t"; + } + + comment += 'Филиал: ' + result.options.filials[index].filial + "\n\t"; + comment += 'За период: ' + result.options.filials[index].periodFirst; + comment += ' - ' + result.options.filials[index].periodLast + "\n\t"; + } + } + + params.fields['UF_CRM_1545895397'] = comment; + } + + if (result.invalid) { + return false; + } + + loader.btnLoader(submit, true); + + helper.sendRequest( + params, + 'https://sovamed.bitrix24.ru/rest/10998/3hrv38rzo3khchj3/crm.lead.add.json', + 'POST', + 'json', + false, + 'application/json' + ).then(function(response) { + loader.btnLoader(submit, false); + + if (referenceWrapper.dataset.ref == '/') { + helper.sendRequest({'reference' : 1, 'phone' : result.options.phone}, '/api/msg'); + var successAlert = document.createElement('div'); + successAlert.classList = 'alert alert-success alert-dismissible fade show'; + successAlert.setAttribute('role', 'alert'); + + var divMsg = document.createElement('div'); + divMsg.classList = 'alert-msg'; + divMsg.innerHTML = '

Заявка успешно подана.

'; + successAlert.append(divMsg); + + var buttonClose = document.createElement('button'); + buttonClose.classList = 'close'; + buttonClose.dataset.dismiss ='alert'; + buttonClose.setAttribute('aria-label', 'Close'); + buttonClose.innerHTML = ''; + successAlert.append(buttonClose); + document.getElementById('alert-system').prepend(successAlert); + + $(popup).modal('hide'); + $('html, body').animate({scrollTop:0}, '300'); + } else { + window.parent.postMessage({'message': 'hide.modal', 'phone' : result.options.phone}, atob(referenceWrapper.dataset.ref)); + } + + }); + + }.bind(this) + } + + renderSending(referenceSending) { + var div = document.createElement('div'); + div.classList = 'form-group mb-2'; + + var label = document.createElement('label'); + label.innerHTML = 'Почтовый адрес:'; + label.classList = 'mb-0'; + label.setAttribute('for', 'reference_address') + div.append(label); + + var input = document.createElement('input'); + input.id = 'reference_address'; + input.name = 'reference_address'; + input.classList = 'form-control'; + div.append(input); + + var msg = document.createElement('div') + msg.classList = 'msg-valid valid-reference_address'; + div.append(msg); + referenceSending.append(div); + + var div = document.createElement('div'); + div.classList = 'form-group mb-2'; + + var label = document.createElement('label'); + label.innerHTML = 'Индекс:'; + label.classList = 'mb-0'; + label.setAttribute('for', 'reference_index') + div.append(label); + + var input = document.createElement('input'); + input.id = 'reference_index' + input.name = 'reference_index' + input.classList = 'form-control'; + div.append(input); + + var msg = document.createElement('div') + msg.classList = 'msg-valid valid-reference_index'; + div.append(msg); + + referenceSending.append(div); + } + + renderResponsibleUser(responsibleUser, countFilial) { + var div = document.createElement('div'); + div.classList = 'form-group mb-2'; + + var label = document.createElement('label'); + label.innerHTML = 'ФИО пациента:'; + label.classList = 'mb-0'; + label.setAttribute('for', 'reference_patientName_' + countFilial) + div.append(label); + + var input = document.createElement('input'); + input.id = 'reference_patientName_' + countFilial + input.name = 'reference_patientName[' + countFilial + ']' + input.classList = 'form-control'; + div.append(input); + + var msg = document.createElement('div') + msg.classList = 'msg-valid valid-reference_patientName'; + div.append(msg); + responsibleUser.append(div); + + var div = document.createElement('div'); + div.classList = 'form-group mb-2'; + + var label = document.createElement('label'); + label.innerHTML = 'Дата рождения пациента:'; + label.classList = 'mb-0'; + label.setAttribute('for', 'reference_patientBirthDate_' + countFilial) + div.append(label); + + var input = document.createElement('input'); + input.id = 'reference_patientBirthDate_' + countFilial; + input.name = 'reference_patientBirthDate[' + countFilial + ']' + input.classList = 'form-control'; + input.dataset.controller = 'datePicker'; + input.setAttribute('range', false); + div.append(input); + responsibleUser.append(div); + + var msg = document.createElement('div') + msg.classList = 'msg-valid valid-reference_patientBirthDate'; + div.append(msg); + + var div = document.createElement('div'); + div.classList = 'form-group mb-2'; + + var label = document.createElement('label'); + label.innerHTML = 'Степень родства:'; + label.classList = 'mb-0'; + label.setAttribute('for', 'reference_relation_' + countFilial) + div.append(label); + + var select = document.createElement('select'); + select.id = 'reference_relation_' + countFilial; + select.name = 'reference_relation_[' + countFilial + ']' + select.classList = 'form-control'; + div.append(select); + + var option = document.createElement('option'); + option.innerHTML = 'супруг'; + option.value = 'супруг'; + select.append(option); + + var option = document.createElement('option'); + option.innerHTML = 'супруга'; + option.value = 'супруга'; + select.append(option); + + var option = document.createElement('option'); + option.innerHTML = 'мать'; + option.value = 'мать'; + select.append(option); + + var option = document.createElement('option'); + option.innerHTML = 'отец'; + option.value = 'отeц'; + select.append(option); + + var option = document.createElement('option'); + option.innerHTML = 'сын'; + option.value = 'сын'; + select.append(option); + + var option = document.createElement('option'); + option.innerHTML = 'дочь'; + option.value = 'дочь'; + select.append(option); + + var msg = document.createElement('div') + msg.classList = 'msg-valid valid-reference_relation'; + div.append(msg); + + responsibleUser.append(div); + } + + submit(wrapper) { + var valid, invalid = false; + var options = {}; + options.autorName = wrapper.querySelector('#reference_autorName').value; + + valid = validator.checkTextRu( + wrapper.querySelector('#reference_autorName'), + wrapper.querySelector(`.valid-reference_autorName`) + ); + + if (valid) { + invalid = valid + } + + options.phone = wrapper.querySelector('#reference_phone').value; + + valid = validator.checkPhone( + wrapper.querySelector('#reference_phone'), + wrapper.querySelector(`.valid-reference_phone`) + ); + + if (valid) { + invalid = valid + } + + options.birthDate = wrapper.querySelector('#reference_birthDate').value; + + valid = validator.checkDate( + wrapper.querySelector('#reference_birthDate'), + wrapper.querySelector(`.valid-reference_birthDate`) + ); + + if (valid) { + invalid = valid + } + + options.inn = wrapper.querySelector('#reference_inn').value; + + valid = validator.checkInn( + wrapper.querySelector('#reference_inn'), + wrapper.querySelector(`.valid-reference_inn`) + ); + + if (valid) { + invalid = valid + } + + options.filials = {}; + + wrapper.querySelectorAll('.responsible_filials').forEach(function(el, index) { + options.filials[index] = {}; + + if (index == 0) { + options.filials[index]['filial'] = el.querySelector('#reference_filial').value; + + valid = validator.checkNotEmpty( + el.querySelector('#reference_filial'), + el.querySelector('.valid-reference_filial') + ); + + if (valid) { + invalid = valid + } + + options.filials[index]['periodFirst'] = el.querySelector('#reference_periodFirst').value; + options.filials[index]['periodLast'] = el.querySelector('#reference_periodLast').value; + + if (valid) { + invalid = valid + } + + var reference_responsible = '#reference_responsible'; + } else { + options.filials[index]['filial'] = el.querySelector('#reference_filial_'+ index).value; + options.filials[index]['periodFirst'] = document.querySelector('#reference_periodFirst_'+ index).value + options.filials[index]['periodLast'] = document.querySelector('#reference_periodLast_'+ index).value + + valid = validator.checkNotEmpty( + el.querySelector('#reference_filial_' + index), + el.querySelector('.valid-reference_filial') + ); + + if (valid) { + invalid = valid + } + + var reference_responsible = '#reference_responsible_'+ index + } + + options.filials[index]['responsible'] = el.querySelector(reference_responsible).value; + + if (el.querySelector(reference_responsible).value == 0) { + + options.filials[index]['fio'] = el.querySelector('#reference_patientName_'+ index).value; + + valid = validator.checkTextRu( + el.querySelector('#reference_patientName_'+ index), + el.querySelector(`.valid-reference_patientName`) + ); + + if (valid) { + invalid = valid + } + + options.filials[index]['birthDate'] = el.querySelector('#reference_patientBirthDate_'+ index).value; + + valid = validator.checkDate( + el.querySelector('#reference_patientBirthDate_'+ index), + el.querySelector(`.valid-reference_patientBirthDate`) + ); + + if (valid) { + invalid = valid + } + + options.filials[index]['relation'] = el.querySelector('#reference_relation_'+ index).value; + } + }) + + options.sending = wrapper.querySelector('#reference_sending')?.value ?? '1'; + + // Разрешённые варианты: 1 и 2. Всё остальное считаем "1". + const sendingInt = parseInt(options.sending, 10); + if (sendingInt !== 1 && sendingInt !== 2) { + options.sending = '1'; + } + + if (options.sending == 1 || options.sending === '1') { + options.filialSending = wrapper.querySelector('#reference_filialSending').value + + valid = validator.checkNotEmpty( + wrapper.querySelector('#reference_filialSending'), + wrapper.querySelector('.valid-reference_filialSending') + ); + + if (valid) { + invalid = valid + } + } + + return {'options': options, 'invalid': invalid}; + } +} diff --git a/assets/controllers/registration_controller.js b/assets/controllers/registration_controller.js new file mode 100644 index 0000000..548bfe7 --- /dev/null +++ b/assets/controllers/registration_controller.js @@ -0,0 +1,438 @@ +import { Controller } from 'stimulus'; +import Cookies from 'js-cookie'; +const loader = require("./../components/loader.js"); +const validator = require("./../components/validator.js"); +const helper = require("./../components/helper.js"); + +export default class extends Controller { + connect() { + loader.loadSDK('registration') + var registerWrap = this.element; + var licenseLink = helper.getLicenseLink(Cookies.get('region')); + helper.renderCapcha(registerWrap.querySelector('.d-capcha-start')); + registerWrap.querySelectorAll('.license').forEach(function(el) { + /* политика */ + var div = document.createElement('div'); + div.classList = 'form-group'; + + var formCheck = document.createElement('div'); + formCheck.classList = 'form-check'; + div.append(formCheck); + + var accept = document.createElement('input'); + accept.classList = "form-check-input"; + accept.id = "accept"; + accept.checked = false; + accept.type = "checkbox"; + formCheck.append(accept); + + var license = helper.getLicenseLink(Cookies.get('region')); + var label = document.createElement('label'); + label.setAttribute('for' , 'accept'); + label.innerHTML = `ознакомлен(а) с условиями политики в отношении обработки персональных данных`; + + formCheck.append(label); + + var validAccept = document.createElement('div'); + validAccept.classList = 'msg-valid valid-accept'; + div.append(validAccept); + + el.append(div) + + /* согласие */ + + var formCheck = document.createElement('div'); + formCheck.classList = 'form-check'; + div.append(formCheck); + + var acceptPerson = document.createElement('input'); + acceptPerson.classList = "form-check-input"; + acceptPerson.id = "acceptPerson"; + acceptPerson.checked = false; + acceptPerson.type = "checkbox"; + formCheck.append(acceptPerson); + + var licensePerson = helper.getLicensePersonLink(); + var labelPerson = document.createElement('label'); + labelPerson.setAttribute('for' , 'acceptPerson'); + labelPerson.innerHTML = `даю согласие на обработку персональных данных`; + + formCheck.append(labelPerson); + + var validAcceptPerson = document.createElement('div'); + validAcceptPerson.classList = 'msg-valid valid-acceptPerson'; + div.append(validAcceptPerson); + + el.append(div) + }); + } + + btnBack() { + var registerWrap = this.element; + registerWrap.querySelector('.register-start').classList.remove('d-none'); + registerWrap.querySelector('.register-init').classList.add('d-none'); + registerWrap.querySelector('.register-complete').classList.add('d-none'); + registerWrap.querySelector('.register-complete').dataset.rToken = null; + registerWrap.querySelector('.register-complete').dataset.pwdToken = null; + + registerWrap.querySelector('.alert.msg').classList.add('d-none'); + registerWrap.querySelector('.alert.msg').innerHTML = 'Поля, обозначенные звездочкой (*), обязательны для заполнения'; + + var capchaStart = helper.renderCapcha(registerWrap.querySelector('.d-capcha-start')); + + setTimeout(() => { + window.smartCaptcha.reset(capchaStart.dataset.id) + }, 100); + } + + btnStart() { + var captchaId = this.element.querySelector('#smart-captcha')?.dataset?.id; + var valid, invalid = false; + var registerWrap = this.element; + + valid = validator.checkPhone( + registerWrap.querySelector('#phone'), + registerWrap.querySelector(`.valid-phone`) + ); + + if (valid) { + invalid = valid + } + + valid = validator.checkSmartCaptcha( + window.smartCaptcha.getResponse(captchaId), + registerWrap.querySelector('.valid-captcha') + ); + + if (valid) { + invalid = valid + } + + if (validator.checkAccept(registerWrap.querySelector('#accept'),registerWrap.querySelector('.valid-accept'))) { + invalid = true; + } + + if (validator.checkAccept(registerWrap.querySelector('#acceptPerson'),registerWrap.querySelector('.valid-acceptPerson'))) { + invalid = true; + } + + if (invalid) { + return false; + } + + window.webSDK.recoveryInit({ + 'login': registerWrap.querySelector('#phone').value.replace(/[^\w\s!?]/g,''), + 'captcha': window.smartCaptcha.getResponse(captchaId) + }).then(function (resolve) { + registerWrap.querySelector('.register-start').classList.add('d-none'); + registerWrap.querySelector('.register-complete').classList.remove('d-none'); + registerWrap.querySelector('.register-complete').dataset.pwdToken = resolve.data.pwdToken; + + if (resolve.data.message.indexOf('почтовый ящик') != '-1') { + registerWrap.querySelector('.emailCode').classList.remove('d-none'); + registerWrap.querySelector('.alertEmailView').innerHTML = resolve.data.message; + registerWrap.querySelector('.register-complete').dataset.type = 'email'; + } else { + registerWrap.querySelector('.smsCode').classList.remove('d-none'); + registerWrap.querySelector('.alertSmsCode').innerHTML = resolve.data.message; + registerWrap.querySelector('.register-complete').dataset.type = 'sms'; + } + + }).catch(function (e) { + if (e.data?.errors?.captcha) { + registerWrap.querySelector('.valid-captcha').innerText = e.data?.errors?.captcha?.[0]; + window.smartCaptcha.reset(captchaId); + } + + if (e.data?.message?.includes("не найден в базе данных")) { + registerWrap.querySelector('.register-start').classList.add('d-none'); + registerWrap.querySelector('.register-init').classList.remove('d-none'); + helper.renderCapcha(registerWrap.querySelector('.d-capcha-init')); + } + }); + } + + btnReg() { + var captchaId = this.element.querySelector('#smart-captcha')?.dataset?.id; + var valid, invalid = false; + var registerWrap = this.element; + + registerWrap.querySelectorAll('.alert.alert-danger.alert-dismissible').forEach(function(e) { + e.remove(); + }); + + var options = { + firstName: registerWrap.querySelector('#firstName').value, + lastName: registerWrap.querySelector('#lastName').value, + middleName: registerWrap.querySelector('#middleName').value + }; + + for (var key of Object.keys(options)) { + valid = validator.checkTextRu( + registerWrap.querySelector(`#${key}`), + registerWrap.querySelector(`.valid-${key}`) + ); + + if (valid) { + invalid = valid + } + } + + options.email = registerWrap.querySelector('#email').value; + + valid = validator.checkEmail( + registerWrap.querySelector('#email'), + registerWrap.querySelector(`.valid-email`) + ); + + if (valid) { + invalid = valid + } + + options.phone = registerWrap.querySelector('#phone').value; + + valid = validator.checkPhone( + registerWrap.querySelector('#phone'), + registerWrap.querySelector(`.valid-phone`) + ); + + if (valid) { + invalid = valid + } + + valid = validator.checkSmartCaptcha( + window.smartCaptcha.getResponse(captchaId), + registerWrap.querySelector('.valid-captcha') + ); + + if (valid) { + invalid = valid + } + + var validGender = false; + + registerWrap.querySelectorAll('.gender').forEach(function (el) { + if (el.checked) { + registerWrap.querySelector('.valid-gender').innerHTML = ''; + validGender = true; + options.gender = el.value; + } + }); + + if (!validGender) { + registerWrap.querySelector('.valid-gender').innerHTML = 'укажите Ваш пол'; + invalid = true; + } + + registerWrap.querySelectorAll('.msg-valid').forEach(function (el) { + el.innerHTML = ''; + }); + + window.webSDK.registerInit({ + firstName: options.firstName, + lastName: options.lastName, + middleName: options.middleName, + birthDate: registerWrap.querySelector('#birthDate').value, + email: options.email, + phone: options.phone, + gender: options.gender, + captcha: window.smartCaptcha.getResponse(captchaId) + }).then(function(resolve) { + registerWrap.querySelector('.register-init').classList.add('d-none'); + registerWrap.querySelector('.register-complete').classList.remove('d-none'); + registerWrap.querySelector('.register-complete').dataset.rToken = resolve.data.rToken; + + if (resolve.data && resolve.data.email == 'true') { + registerWrap.querySelector('.emailCode').classList.remove('d-none'); + registerWrap.querySelector('.emailView').innerHTML = options.email; + } else { + registerWrap.querySelector('.smsCode').classList.remove('d-none'); + registerWrap.querySelector('.smsView').innerHTML = options.phone; + } + + }).catch(function(error) { + window.smartCaptcha.reset(captchaId); + helper.sendRequest({ + data: {'error': error, method: 'registerInit'} + }, helper.getHostname() + '/api/log', "POST", "json", true, "application/json"); + + var registerAlert = document.getElementById('register-alert'); + + if (! registerAlert) { + var registerAlert = document.createElement('div'); + registerAlert.classList = 'alert alert-danger alert-dismissible fade'; + registerAlert.setAttribute('role', 'alert'); + + var divMsg = document.createElement('div'); + divMsg.classList = 'alert-msg'; + registerAlert.append(divMsg); + + var buttonClose = document.createElement('button'); + buttonClose.classList = 'close'; + buttonClose.dataset.dismiss ='alert'; + buttonClose.setAttribute('aria-label', 'Close'); + buttonClose.innerHTML = ''; + registerAlert.append(buttonClose); + registerWrap.prepend(registerAlert); + } + + if (error.data.message) { + registerAlert.classList = 'alert alert-danger alert-dismissible fade show'; + registerAlert.querySelector('.alert-msg').innerHTML = error.data.message; + } else { + for (var prop in error.data) { + if (error.data[prop]) { + var el = registerWrap.querySelector('.valid-' + prop) + el.innerHTML = error.data[prop]; + } + } + } + }); + } + + btnComplite() { + var registerWrap = this.element; + + if (registerWrap.querySelector('.register-complete').dataset.pwdToken !== "null") { + var type = registerWrap.querySelector('.register-complete').dataset.type; + var code = registerWrap.querySelector('#emailCode'); + + if (type == 'sms') { + var code = registerWrap.querySelector('#smsCode'); + } + + this.recoveryComplite(code, type); + } else { + this.registerComplete(); + } + } + + registerComplete() { + var registerWrap = this.element; + + window.webSDK.registerComplete({ + rToken: registerWrap.querySelector('.register-complete').dataset.rToken, + emailCode: registerWrap.querySelector('#emailCode').value, + smsCode: registerWrap.querySelector('#smsCode').value, + password: registerWrap.querySelector('#password').value + }).then(function(response) { + registerWrap.querySelector('.alert.filter').classList.add('d-none'); + + var successAlert = document.createElement('div'); + successAlert.classList = 'alert alert-success alert-dismissible fade show'; + successAlert.setAttribute('role', 'alert'); + + var divMsg = document.createElement('div'); + divMsg.classList = 'alert-msg'; + divMsg.innerHTML = '

Регистрация завершена.

' + + '

' + response.data.text + '

'; + successAlert.append(divMsg); + + var buttonClose = document.createElement('button'); + buttonClose.classList = 'close'; + buttonClose.dataset.dismiss ='alert'; + buttonClose.setAttribute('aria-label', 'Close'); + buttonClose.innerHTML = ''; + successAlert.append(buttonClose); + document.getElementById('alert-system').prepend(successAlert); + registerWrap.querySelector('.register-complete').classList.add('d-none'); + }).catch(function(error) { + helper.sendRequest({ + data: {'error': error, method: 'registerComplete'} + }, helper.getHostname() + '/api/log', "POST", "json", true, "application/json"); + for (var prop in error.data) { + if (error.data[prop]) { + var classValid = prop.split('.')[1]; + + if (classValid) { + var el = registerWrap.querySelector('.valid-' + prop.split('.')[1]); + } else { + var el = registerWrap.querySelector('.valid-' + prop); + } + + el.innerHTML = error.data[prop]; + } + } + + $('html, body').animate({scrollTop:0}, '300'); + }); + } + + recoveryComplite(inputCode, type) { + var registerWrap = this.element; + var valid, invalid = false; + + valid = validator.checkNotEmpty( + inputCode, + registerWrap.querySelector('.valid-' + type + 'Code') + ); + + if (valid) { + invalid = valid + } + + valid = validator.checkNotEmpty( + registerWrap.querySelector('#password'), + registerWrap.querySelector('.valid-password') + ); + + if (valid) { + invalid = valid + } + + if (invalid) { + return false; + } + + window.webSDK.recoveryComplete({ + 'pwdToken': registerWrap.querySelector('.register-complete').dataset.pwdToken, + 'password': registerWrap.querySelector('#password').value, + 'code': inputCode.value + }).then(function (result) { + registerWrap.querySelector('.alert.msg').classList.add('d-none'); + + var successAlert = document.createElement('div'); + successAlert.classList = 'alert alert-success alert-dismissible fade show'; + successAlert.setAttribute('role', 'alert'); + + var divMsg = document.createElement('div'); + divMsg.classList = 'alert-msg'; + divMsg.innerHTML = result.data.message; + successAlert.append(divMsg); + + var buttonClose = document.createElement('button'); + buttonClose.classList = 'close'; + buttonClose.dataset.dismiss ='alert'; + buttonClose.setAttribute('aria-label', 'Close'); + buttonClose.innerHTML = ''; + successAlert.append(buttonClose); + document.getElementById('alert-system').prepend(successAlert); + registerWrap.querySelector('.register-complete').classList.add('d-none'); + + $.ajax({ + method: "POST", + crossDomain: false, + url: "/forget", + contentType: "application/x-www-form-urlencoded", + dataType: "json", + data: { + token: popupBody.dataset.csrf, + uid: forget.data.uid, + password: forgetPasswd + } + }); + }).catch(function (e) { + helper.sendRequest({ + data: {'error': e, method: 'recoveryComplete'} + }, helper.getHostname() + '/api/log', "POST", "json", true, "application/json"); + + var msg = registerWrap.querySelector('.msg'); + msg.classList.add('alert-danger'); + msg.classList.remove('filter'); + + if (e.data && e.data.error) { + msg.innerHTML = e.data.error + } + }); + } +} \ No newline at end of file diff --git a/assets/controllers/resetPassword_controller.js b/assets/controllers/resetPassword_controller.js new file mode 100644 index 0000000..b389fbe --- /dev/null +++ b/assets/controllers/resetPassword_controller.js @@ -0,0 +1,377 @@ +import { Controller } from 'stimulus'; +import Inputmask from "inputmask"; +const loader = require("./../components/loader.js"); +const validator = require("./../components/validator.js"); +const helper = require("./../components/helper.js"); + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="resetPassword" attribute will cause + * this controller to be executed. The name "resetPassword" comes from the filename: + * resetPassword_controller.js -> "resetPassword" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + connect() { + loader.loadSDK('resetPassword') + let resetPasswordSmsCode = true; + var popup = document.getElementById('popup'); + + if (location.hash == '#recovery' && popup.dataset.recovery == 'true') { + popup.dataset.recovery = false; + renderForm(window.webSDK); + } else { + var element = this.element + element.addEventListener('click', function() { + renderForm(window.webSDK); + }); + } + + function renderForm(webSDK) { + var popup = document.getElementById('popup'); + popup.querySelector('.modal-title').innerHTML = "Восстановление пароля"; + var popupBody = popup.querySelector('#popup-body'); + popupBody.innerHTML = ""; + + if (typeof $(popup).modal !== 'undefined') { + $(popup).modal('show'); + } + + var div = document.createElement('div'); + div.classList = 'form-group'; + + if (resetPasswordSmsCode) { + var label = document.createElement('label'); + label.setAttribute('for' , 'login-forget'); + label.innerHTML = 'Телефон пользователя:'; + div.append(label); + + var input = document.createElement('input'); + input.classList = "form-control"; + input.id = "login-forget"; + input.type = "phone"; + input.autocomplete = "phone"; + div.append(input); + + var im = new Inputmask({mask: "7(899)999-99-99",definitions: {'8': {validator: "[9]"}}}); + im.mask(input); + } else { + var label = document.createElement('label'); + label.setAttribute('for' , 'login-forget'); + label.innerHTML = 'E-mail пользователя:'; + div.append(label); + + var input = document.createElement('input'); + input.classList = "form-control"; + input.id = "login-forget"; + input.autocomplete = "email"; + input.type = "email"; + div.append(input); + } + + var validUsername = document.createElement('div'); + validUsername.classList = "msg-valid valid-login-forget"; + div.append(validUsername); + + if (resetPasswordSmsCode) { + var p = document.createElement('p'); + p.innerHTML = "На Ваш номер телефона будет отправлен код для восстановления пароля." + popupBody.append(p); + + } else { + var p = document.createElement('p'); + p.innerHTML = "На Вашу электронную почту будет отправлен код для восстановления пароля." + popupBody.append(p); + } + + var msg = document.createElement('div'); + msg.classList = 'd-none msg'; + msg.innerHTML = ''; + popupBody.append(div); + popupBody.append(msg); + + var div = document.createElement('div'); + div.classList = 'form-group'; + + var recaptcha = document.createElement('div'); + recaptcha.id = "smart-captcha"; + recaptcha.dataset.controller = "smartCaptcha"; + div.append(recaptcha); + + var validRecaptcha = document.createElement('div'); + validRecaptcha.classList = "msg-valid valid-captcha"; + div.append(validRecaptcha); + popupBody.append(div); + + var button = document.createElement('button'); + button.classList = "btn btn-primary"; + button.innerHTML = "Восстановить" + button.addEventListener('click', function () { + + loader.btnLoader(button, true); + resetPassword(webSDK, popup) + }); + + var footer = document.createElement('div'); + footer.classList = 'modal-footer px-0'; + + footer.append(button); + popupBody.append(footer); + }; + + function resetPassword(webSDK, popup) { + var popupBody = popup.querySelector('#popup-body'); + + var valid, invalid = false; + if (resetPasswordSmsCode) { + valid = validator.checkPhone( + document.getElementById('login-forget'), + document.querySelector(`.valid-login-forget`) + ); + + var login = document.getElementById('login-forget').value.replace(/[^\w\s!?]/g,''); + } else { + valid = validator.checkEmail( + document.getElementById('login-forget'), + document.querySelector(`.valid-login-forget`) + ); + + var login = document.getElementById('login-forget').value; + } + + + if (valid) { + invalid = valid + } + + valid = validator.checkSmartCaptcha( + window.smartCaptcha.getResponse(), + document.querySelector('.valid-captcha') + ); + + if (valid) { + invalid = valid + } + + if (invalid) { + loader.btnLoader(popupBody.querySelector('button'), false); + return false; + } + + $.ajax({ + async : false, + method: "POST", + crossDomain: true, + url: helper.getHostname() + "/forget", + contentType: "application/x-www-form-urlencoded", + dataType: "json", + data: { + token: popupBody.dataset.csrf, + login: login + }, + success(response) { + window.webSDK.recoveryInit({ + 'login': login, + 'captcha': window.smartCaptcha.getResponse() + }).then(function (resolve) { + renderFormConfirm(webSDK, popup, resolve, response); + }).catch(function (e) { + helper.sendRequest({ + data: {'error': e, method: 'recoveryInit'} + }, helper.getHostname() + '/api/log', "POST"); + + window.smartCaptcha.reset(); + + if (e.data?.errors?.captcha) { + document.querySelector('.valid-captcha').innerText = e.data?.errors?.captcha?.[0]; + } + + loader.btnLoader(popupBody.querySelector('button'), false); + + if (e.data?.message) { + var msg = popupBody.querySelector('.msg'); + msg.classList = 'alert alert-danger'; + msg.innerHTML = e.data.message; + } + }); + } + }); + }; + + function renderFormConfirm(webSDK, popup, resolve, forget) { + var popupBody = popup.querySelector('#popup-body'); + popupBody.innerHTML = ""; + + var message = document.createElement('p'); + message.innerHTML = resolve.data.message; + popupBody.append(message); + + var div = document.createElement('div'); + div.classList = 'form-group'; + + var label = document.createElement('label'); + label.setAttribute('for' , 'forget-code'); + label.innerHTML = 'Код подтверждения:'; + div.append(label); + + var input = document.createElement('input'); + input.classList = "form-control"; + input.id = "forget-code"; + input.autocomplete = "one-time-code"; + input.autocorrect = "off"; + input.spellcheck = "off"; + input.type = "text"; + div.append(input); + + var divValidCode = document.createElement('div'); + divValidCode.classList = "msg-valid valid-code"; + div.append(divValidCode); + popupBody.append(div); + + var div = document.createElement('div'); + div.classList = 'form-group'; + + var label = document.createElement('label'); + label.setAttribute('for' , 'forget-passwd'); + label.innerHTML = 'Новый пароль:'; + div.append(label); + + var input = document.createElement('input'); + input.classList = "form-control"; + input.id = "forget-passwd"; + input.autocomplete = "new-password"; + input.type = "password"; + div.append(input); + + var divValidPass = document.createElement('div'); + divValidPass.classList = "msg-valid valid-password"; + divValidPass.innerHTML = "не менее 7 символов"; + div.append(divValidPass); + popupBody.append(div); + + var msg = document.createElement('div'); + msg.classList = 'd-none alert alert-danger'; + msg.innerHTML = ''; + popupBody.append(msg); + + var button = document.createElement('button'); + button.classList = "btn btn-primary"; + button.innerHTML = " Сохранить изменения"; + button.addEventListener('click', function () { + popupBody.querySelectorAll('.msg-valid').forEach(function(el) { + el.innerHTML = ''; + }); + + var invalid = false; + var forgetCode = document.getElementById('forget-code').value; + + if (forgetCode === '') { + popupBody.querySelector('.valid-code') + .innerHTML = 'поле не может быть пустым'; + popupBody.querySelector('#forget-code').classList.add('is-invalid'); + popupBody.querySelector('#forget-code').classList.remove('is-valid'); + invalid = true; + } else { + popupBody.querySelector('#forget-code').classList.add('is-valid'); + popupBody.querySelector('#forget-code').classList.remove('is-invalid'); + } + + var forgetPasswd = document.getElementById('forget-passwd').value; + + if (forgetPasswd.length < 6 || /^[\u0400-\u04FF]+$/.test(forgetPasswd)) { + invalid = true; + + if (/^[\u0400-\u04FF]+$/.test(forgetPasswd)) { + popupBody.querySelector('.valid-password') + .innerHTML = 'поле не должно содержать кириллицы'; + popupBody.querySelector('#forget-passwd').classList.add('is-invalid'); + popupBody.querySelector('#forget-passwd').classList.remove('is-valid'); + } else { + popupBody.querySelector('.valid-password') + .innerHTML = 'не менее 7 символов'; + popupBody.querySelector('#forget-passwd').classList.add('is-invalid'); + popupBody.querySelector('#forget-passwd').classList.remove('is-valid'); + } + } else { + popupBody.querySelector('#forget-passwd').classList.add('is-valid'); + popupBody.querySelector('#forget-passwd').classList.remove('is-invalid'); + } + + if (invalid) { + return false; + } + + window.webSDK.recoveryComplete({ + 'pwdToken': resolve.data.pwdToken, + 'password': forgetPasswd, + 'code': document.getElementById('forget-code').value + }).then(function (result) { + if (result.data.errors) { + if (result.data.errors.code) { + popupBody.querySelector('.valid-code').innerHTML = ` + ${result.data.errors.code}`; + } + + return false; + } + + popupBody.innerHTML = ''; + var msg = document.createElement('div'); + msg.classList = 'alert alert-success'; + msg.innerHTML = result.data.message; + popupBody.append(msg); + + $.ajax({ + method: "POST", + crossDomain: true, + url: helper.getHostname() + "/forget", + contentType: "application/x-www-form-urlencoded", + dataType: "json", + data: { + token: popupBody.dataset.csrf, + uid: forget.data.uid, + password: forgetPasswd + }, + success(response) { + if (response.data.success == true) { + var parser = document.createElement('a'); + parser.href = response.data.redirect; + window.location.replace(document.location.origin + parser.pathname + parser.search); + } + } + }); + }).catch(function (e) { + helper.sendRequest({ + data: {'error': e, method: 'recoveryComplete'} + }, helper.getHostname() + '/api/log', "POST"); + + if (e.data && e.data.error) { + msg.innerHTML = e.data.error + msg.classList.remove('d-none'); + } + }); + }); + + var footer = document.createElement('div'); + footer.classList = 'modal-footer'; + footer.append(button); + popupBody.append(footer); + }; + } +} + + + + + + + + + + + + + + diff --git a/assets/controllers/scrollTop_controller.js b/assets/controllers/scrollTop_controller.js new file mode 100644 index 0000000..80c1221 --- /dev/null +++ b/assets/controllers/scrollTop_controller.js @@ -0,0 +1,28 @@ +import { Controller } from 'stimulus'; + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="default" attribute will cause + * this controller to be executed. The name "default" comes from the filename: + * default_controller.js -> "default" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + connect() { + var btn = this.element; + + $(window).scroll(function() { + if ($(window).scrollTop() > 300) { + btn.classList.remove('d-none') + } else { + btn.classList.add('d-none') + } + }); + + btn.addEventListener('click', function(e) { + $('html, body').animate({scrollTop:0}, '300'); + }); + } +} diff --git a/assets/controllers/searchButton_controller.js b/assets/controllers/searchButton_controller.js new file mode 100644 index 0000000..8da4aaa --- /dev/null +++ b/assets/controllers/searchButton_controller.js @@ -0,0 +1,57 @@ +import { Controller } from 'stimulus'; + +export default class extends Controller { + connect() { + } + + cancelForm() { + const mainSearchInput = document.querySelector('[data-controller="searchNameInput"]'); + if (mainSearchInput) { + mainSearchInput.value = ''; + } + + const showContent = document.querySelector('.show-content'); + if (showContent) { + showContent.classList.add('d-none'); + showContent.innerHTML = ''; + } + } + + searchForm() { + // Вызываем глобальную функцию из searchNameInput контроллера + if (typeof window.performSearch === 'function') { + window.performSearch(); + } else { + // Fallback: делаем редирект вручную + const input = document.querySelector('[data-controller="searchNameInput"]'); + const selectSearch = document.getElementById("select-search").value; + const searchValue = input ? input.value.trim() : ''; + + if (searchValue) { + const encodedValue = encodeURIComponent(searchValue); + // Извлекаем alias из текущего URL, если он есть + const currentPath = window.location.pathname; + const aliasMatch = currentPath.match(/\/specialists\/([^\/]+)/); + const alias = aliasMatch ? aliasMatch[1] : null; + + let url; + + if (selectSearch === 'name') { + const baseUrl = alias ? `/specialists/${alias}` : '/specialists'; + url = `${baseUrl}?specialist_search%5Bname%5D=${encodedValue}`; + } else if (selectSearch === 'pl') { + url = `/stoimost-uslug?price_list_form%5Bschname%5D=${encodedValue}`; + } else if (selectSearch === 'department') { + const baseUrl = alias ? `/specialists/${alias}` : '/specialists'; + url = `${baseUrl}?specialist_search%5Bdepartment%5D=${encodedValue}`; + } + + if (url) { + window.location.href = url; + } + } + } + + console.log('searchForm вызван'); + } +} \ No newline at end of file diff --git a/assets/controllers/searchNameInput_controller.js b/assets/controllers/searchNameInput_controller.js new file mode 100644 index 0000000..59c514f --- /dev/null +++ b/assets/controllers/searchNameInput_controller.js @@ -0,0 +1,164 @@ +import { Controller } from 'stimulus'; +const helper = require("./../components/helper.js"); + +export default class extends Controller { + connect() { + let showContent = this.element.parentElement.querySelector('.show-content'); + const input = this.element; + + const searchConfig = { + 'name': { + url: '/specialists', + paramName: 'specialist_search[name]', + encodedParam: 'specialist_search%5Bname%5D' + }, + 'pl': { + url: '/stoimost-uslug', + paramName: 'price_list_form[schname]', + encodedParam: 'price_list_form%5Bschname%5D' + }, + 'dep': { + url: '/specialists', + paramName: 'specialist_search[depname]', + encodedParam: 'specialist_search%5Bdepname%5D' + } + }; + + const performSearch = () => { + const selectSearch = document.getElementById("select-search").value; + const searchValue = input.value.trim(); + + if (!searchValue) return; + + const config = searchConfig[selectSearch]; + if (!config) return; + + // Извлекаем alias из текущего URL, если он есть + const currentPath = window.location.pathname; + const aliasMatch = currentPath.match(/\/specialists\/([^\/]+)/); + const alias = aliasMatch ? aliasMatch[1] : null; + + // Создаем URL с параметром поиска, сохраняя alias если он есть + const encodedValue = encodeURIComponent(searchValue); + let baseUrl = config.url; + if (alias && (selectSearch === 'name' || selectSearch === 'dep')) { + baseUrl = `/specialists/${alias}`; + } + const searchUrl = `${baseUrl}?${config.encodedParam}=${encodedValue}`; + + console.log('Редирект на:', searchUrl); + window.location.href = searchUrl; + }; + + // Функция для автодополнения (при вводе 3+ символов) + const handleAutocomplete = (searchValue, selectSearch) => { + const onlineMode = location.pathname.includes('/online-specialists') ? 1 : 0; + + if (searchValue.length >= 3 && selectSearch !== 'pl') { + let data = { + q: searchValue, + type: selectSearch, + onlineMode: onlineMode + }; + + showContent.innerText = ''; + showContent.classList.add('d-none'); + + helper.sendRequest(data, "/api/search", "POST").then(function (response) { + showContent.innerHTML = ''; + const ul = document.createElement('ul'); + const { data: searchData } = response; + + const listItemsHTML = searchData.map(el => { + const { alias, name, speciality } = el; + const config = searchConfig[selectSearch]; + + let url; + // Извлекаем alias из текущего URL, если он есть + const currentPath = window.location.pathname; + const currentAliasMatch = currentPath.match(/\/specialists\/([^\/]+)/); + const currentAlias = currentAliasMatch ? currentAliasMatch[1] : null; + + if (selectSearch === 'name') { + url = `/specialist/${alias}?specialist_search[onlineMode]=${onlineMode}`; + } else if (selectSearch === 'pl') { + url = `/stoimost-uslug?${config.encodedParam}=${encodeURIComponent(name)}`; + } else if (selectSearch === 'dep') { + // Сохраняем текущий alias при поиске по отделению + const depUrl = currentAlias ? `/specialists/${currentAlias}` : '/specialists'; + url = `${depUrl}?${config.encodedParam}=${encodeURIComponent(name)}`; + } + + return ` +
  • + + ${name} + ${speciality ? `, ${speciality}` : ''} + +
  • + `; + }).join(''); + + ul.innerHTML = listItemsHTML; + showContent.append(ul); + showContent.classList.remove('d-none'); + }); + } else { + showContent.classList.add('d-none'); + } + }; + + // Обработчик ввода в поле поиска + input.oninput = function(e) { + const selectSearch = document.getElementById("select-search").value; + const searchValue = e.target.value; + + handleAutocomplete(searchValue, selectSearch); + }; + + // Обработчик нажатия Enter + input.addEventListener('keyup', function (evn) { + if (evn.keyCode == 13) { // Enter + evn.preventDefault(); + performSearch(); + } + }); + + // Делаем функцию доступной глобально для другого контроллера + window.performSearch = performSearch; + window.handleAutocomplete = handleAutocomplete; + + // Инициализация значения из текущего параметра URL + this.initializeFromUrl(); + } + + // Метод для инициализации значения поля из параметров URL + initializeFromUrl() { + const input = this.element; + const urlParams = new URLSearchParams(window.location.search); + const selectSearch = document.getElementById("select-search").value; + + const searchConfig = { + 'name': ['specialist_search[name]', 'specialist_search%5Bname%5D'], + 'pl': ['price_list_form[schname]', 'price_list_form%5Bschname%5D'], + 'department': ['specialist_search[department]', 'specialist_search%5Bdepartment%5D'] + }; + + const config = searchConfig[selectSearch]; + if (config) { + // Пробуем получить значение из разных вариантов параметра + let searchValue = urlParams.get(config[0]) || + urlParams.get(config[1]) || + urlParams.get(decodeURIComponent(config[1])); + + if (searchValue) { + input.value = searchValue; + + // Если есть значение, показываем автодополнение + setTimeout(() => { + window.handleAutocomplete && window.handleAutocomplete(searchValue, selectSearch); + }, 100); + } + } + } +} \ No newline at end of file diff --git a/assets/controllers/searchOrderByInput_controller.js b/assets/controllers/searchOrderByInput_controller.js new file mode 100644 index 0000000..d23ef7f --- /dev/null +++ b/assets/controllers/searchOrderByInput_controller.js @@ -0,0 +1,73 @@ +import { Controller } from 'stimulus'; + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="searchOrderByInput" attribute will cause + * this controller to be executed. The name "searchOrderByInput" comes from the filename: + * searchOrderByInput_controller.js -> "searchOrderByInput" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + connect() { + var val = document.getElementById("specialist_search_order_by").value; + var items = this.element.querySelectorAll('span'); + + if (val) { + check(val, false); + } + + items.forEach(function(el) { + el.addEventListener('click', function(evn) { + check(evn.target.id, true); + }); + }) + + function check(index, redirect) { + console.log(index) + var array = index.split('.'); + var id = array[0]; + var sort = array[1]; + + items.forEach(function(item) { + var iconASC = item.querySelector('.fa-sort-amount-asc'); + var iconDESC = item.querySelector('.fa-sort-amount-desc'); + + item.classList.remove('sort-line__item--active'); + + if (id == item.id) { + item.classList.add('sort-line__item--active'); + + if (sort == 'desc') { + item.classList.remove('asc'); + } + + if (item.classList.contains('asc')) { + iconASC.classList.remove('d-none'); + iconDESC.classList.add('d-none'); + item.classList.remove('asc'); + document.getElementById("specialist_search_order_by").value = item.id + '.asc'; + + if (redirect) { + document.querySelector('.submit-filter').click(); + } + } else { + iconDESC.classList.remove('d-none'); + iconASC.classList.add('d-none'); + item.classList.add('asc'); + document.getElementById("specialist_search_order_by").value = item.id + '.desc'; + + if (redirect) { + document.querySelector('.submit-filter').click(); + } + } + } else { + iconDESC.classList.add('d-none'); + iconASC.classList.add('d-none'); + item.classList.add('asc'); + } + }); + }; + } +} diff --git a/assets/controllers/securityCard_controller.js b/assets/controllers/securityCard_controller.js new file mode 100644 index 0000000..b3fc62f --- /dev/null +++ b/assets/controllers/securityCard_controller.js @@ -0,0 +1,377 @@ +import { Controller } from 'stimulus'; + +const loader = require("./../components/loader.js"); +const helper = require("./../components/helper.js"); + +export default class extends Controller { + connect() { + loader.loadSDK('securityCard').then(function(webSDK) { + webSDK.on('init', function() { + if (this.data.user.authenticated) { + runWebSDK(webSDK); + } else { + window.location.pathname = '/logout' + } + }); + }) + + var securityCard = this.element; + var date = new Date(); + var lastDate = new Date(date.getFullYear(), date.getMonth() + 1, 0); + + let runWebSDK = function (webSDK) { + if (document.location.search == '?tab=allTest') { + securityCard.querySelector('.load').innerHTML = 'Загрузка данных'; + + securityTabs.querySelectorAll('a').forEach(function (el) { + if ('allTest' == el.dataset.allowTarget) { + el.classList.add('tab-item--active'); + } else { + el.classList.remove('tab-item--active'); + } + }); + // allTest + treatPlaceList(webSDK, lastDate, securityCard, 'all'); + } else { + treatPlaceList(webSDK, lastDate, securityCard, 'all'); + } + }.bind(this) + + var securityTabs = document.getElementById('security-tabs'); + + if (securityTabs) { + var tabsDecktop = securityTabs.querySelectorAll('a'); + + if (tabsDecktop.length > 0) { + tabsDecktop.forEach(function (el) { + el.addEventListener('click', function (evn) { + evn.target.classList = 'tab-item tab-item--active'; + securityCard.querySelector('.section-wrap').innerHTML = ''; + securityCard.querySelector('.load').innerHTML = 'Загрузка данных'; + + securityTabs.querySelectorAll('a').forEach(function (a) { + if (evn.target.dataset.allowTarget != a.dataset.allowTarget) { + a.classList.remove('tab-item--active'); + } + }); + + if (evn.target.dataset.allowTarget == 'allTest') { + document.title = 'Результаты анализов'; + document.getElementById('page-title').innerHTML = 'Результаты анализов'; + } else { + document.title = 'Медицинская карта'; + document.getElementById('page-title').innerHTML = 'Медицинская карта'; + } + + treatPlaceList(window.webSDK, lastDate, securityCard, evn.target.dataset.allowTarget); + }); + }); + } else { + securityTabs.addEventListener('change', function(evn) { + securityCard.querySelector('.section-wrap').innerHTML = ''; + treatPlaceList(window.webSDK, lastDate, securityCard, evn.target.value); + }); + } + } + + function treatPlaceList(webSDK, lastDate, securityCard, allowTarget) { + var sectionWrap = securityCard.querySelector('.section-wrap'); + var load = securityCard.querySelector('.load'); + + if (allowTarget == 'referrals') { + webSDK.loadReferralList({ + start: 0, + length: 50 + }).then(function (refferals) { + + if (refferals.length == 0) { + securityCard.querySelector('.load').innerHTML = 'Записей не найдено'; + } + + refferals.forEach(function (item) { + var row = document.createElement('div'); + row.classList = 'row'; + + var col6 = document.createElement('div'); + col6.classList = 'col-md-6'; + + var dateTag = document.createElement('p'); + dateTag.innerHTML = '

    ,

    '; + col6.append(dateTag); + row.append(col6); + + var col6 = document.createElement('div'); + col6.classList = 'col-md-6'; + col6.innerHTML = '

    Специалист:

    '; + row.append(col6); + + var refferalsItem = document.createElement('div'); + refferalsItem.classList = 'block-item p-2 mt-1'; + refferalsItem.append(row); + + refferalsItem.classList.remove('d-none'); + refferalsItem.querySelector('.refname').innerHTML = item.refname; + refferalsItem.querySelector('.refcomment').innerHTML = item.refcomment; + refferalsItem.querySelector('.fromfname').innerHTML = item.fromfname; + refferalsItem.querySelector('.fromdname').innerHTML = item.fromdname; + refferalsItem.querySelector('.refstatusname').innerHTML = item.refstatusname; + + var date = window.newDate(item.refdate); + refferalsItem.querySelector('.month').innerHTML = getWeekDay(date); + refferalsItem.querySelector('.date').innerHTML = window.dateFormat(date, 'd-m-Y') + + sectionWrap.append(refferalsItem); + }); + }).catch(function (e) { + helper.sendRequest({ + data: {'error': e, method: 'loadReferralList'} + }, helper.getHostname() + '/api/log', "POST", "json", true, "application/json"); + }); + } else { + webSDK.loadTreatPlaceList({ + st: 20150101, + en: window.dateFormat(lastDate), + start: 0, + length: 100 + }).then(function (resolve) { + console.log("loadTreatPlaceList") + + if (resolve.data.length > 0) { + var count = 0; + load.classList.remove('d-none'); + + for (var i = 0; i < resolve.data.length; i++) { + if (allowTarget == 'allTest') { + if (analysis(resolve.data[i].protocolName)) { + count++; + sectionWrap.classList.remove('d-none'); + + addItem(resolve.data[i], sectionWrap, webSDK); + } + } else if (allowTarget == 'all') { + count = resolve.data.length + sectionWrap.classList.remove('d-none'); + + addItem(resolve.data[i], sectionWrap, webSDK); + } + } + + load.classList.add('d-none'); + + if (count == 0) { + load.classList.remove('d-none'); + } + + } else { + securityCard.querySelector('.load').innerHTML = 'Записей не найдено'; + load.classList.remove('d-none'); + } + }).catch(function (e) { + helper.sendRequest({ + data: {'error': e, method: 'loadTreatPlaceList'} + }, helper.getHostname() + '/api/log', "POST", "json", true, "application/json"); + }); + } + } + + function analysis(protocolName) { + const regex = /анализ/gm; + let m; + var result = false; + + while ((m = regex.exec(protocolName)) !== null) { + if (m.index === regex.lastIndex) { + regex.lastIndex++; + } + + m.some(function(match, groupIndex) { + result = true; + }); + + } + + return result; + } + + function addItem(item, wrap, webSDK) { + wrap.insertAdjacentHTML('beforeend', ` +
    +
    ${getWeekDay(window.newDate(item.date))} ${window.dateFormat(window.newDate(item.date), 'd-m-Y')}
    +
    +
    +
    + ${item.protocolName} +

    Специалист:
    ${item.doctorName}

    +
    +
    + +
    +
    + ${item.attachments.length > 0 ? ` +
    +

    Список изображений (файлов):

    + ${item.attachments.map(attachment => ` +
    +
    + ${attachment.attachmentName} +
    + +
    +
    +
    + +
    +
    +
    +
    + `).join('')} +
    + ` : ''} +
    +
    +
    +
    + `); + + const cardItem = wrap.lastElementChild; + + if (item.attachments.length > 0) { + const attachmentLinks = cardItem.querySelectorAll('button[data-attachment-id]'); + attachmentLinks.forEach(link => { + link.addEventListener('click', function(e) { + e.preventDefault(); + const popup = document.getElementById('popup'); + const modalBody = popup.querySelector('.modal-body'); + modalBody.dataset.attachmentType = this.dataset.attachmentType; + modalBody.innerHTML = ` +
    +
    + Загрузка... +
    +

    Загрузка файла...

    +
    + `; + + webSDK.downloadTreatPlaceAttachment({ + protocolId: this.dataset.protocolId, + treatcode: this.dataset.treatcode, + attachmentId: this.dataset.attachmentId, + download: this.dataset.attachmentType + }).then(function(blob) { + if (modalBody.dataset.attachmentType === 'download') { + return false; + } + + modalBody.innerHTML = ''; + + const objectURL = URL.createObjectURL(blob.result); + + if (blob.result.type === 'application/pdf' || + blob.result.type === 'application/x-pdf') { + + // Для PDF создаем iframe + const iframe = document.createElement('iframe'); + iframe.src = objectURL; + iframe.style.width = "100%"; + iframe.style.height = "500px"; + iframe.style.border = "none"; + iframe.title = "PDF документ"; + iframe.type = 'application/pdf'; + + popup.querySelector('.modal-body').appendChild(iframe); + + } else if (blob.result.type.startsWith('image/')) { + // Для изображений создаем img + const image = document.createElement('img'); + image.src = objectURL; + image.style.width = "100%"; + image.style.maxHeight = "80vh"; + image.style.objectFit = "contain"; + image.alt = "Изображение"; + + popup.querySelector('.modal-body').appendChild(image); + + } else { + // Для других типов файлов - кнопка скачивания + const downloadLink = document.createElement('a'); + downloadLink.href = objectURL; + downloadLink.download = 'document'; + downloadLink.className = 'btn btn-primary'; + downloadLink.textContent = 'Скачать файл'; + downloadLink.style.margin = '20px auto'; + downloadLink.style.display = 'block'; + downloadLink.style.width = '200px'; + + popup.querySelector('.modal-body').appendChild(downloadLink); + + // Также показываем информацию о типе файла + const info = document.createElement('p'); + info.textContent = `Тип файла: ${blob.result.type}`; + info.style.textAlign = 'center'; + popup.querySelector('.modal-body').appendChild(info); + } + + $(popup).modal('show'); + + // Очистка памяти при закрытии модального окна + $(popup).on('hidden.bs.modal', function() { + URL.revokeObjectURL(objectURL); + $(popup).off('hidden.bs.modal'); + }); + + }).catch(function (e) { + console.error('Error downloading file:', e); + helper.sendRequest({ + data: {'error': e, method: 'TreatPlaceAttachment'} + }, helper.getHostname() + '/api/log', "POST", "json", true, "application/json"); + + popup.querySelector('.modal-body').innerHTML = ` +
    +
    Ошибка загрузки файла
    +

    ${e.message || 'Неизвестная ошибка'}

    + +
    + `; + $(popup).modal('show'); + }); + }); + }); + } + + const btnInfo = cardItem.querySelector('.card-item__btn'); + btnInfo.addEventListener('click', function () { + const popup = document.getElementById('popup'); + popup.querySelector('.modal-body').innerHTML = ''; + webSDK.loadTreatPlaceView({ + protocolId: this.dataset.protocolId, + treatcode: this.dataset.treatcode, + container: popup.querySelector('.modal-body') + }); + + if (!helper.isMobile()) { + $(popup).modal('show'); + } + }); + } + } + +} + + diff --git a/assets/controllers/selectpicker_controller.js b/assets/controllers/selectpicker_controller.js new file mode 100644 index 0000000..a72fe77 --- /dev/null +++ b/assets/controllers/selectpicker_controller.js @@ -0,0 +1,26 @@ +import { Controller } from 'stimulus' +import selectpicker from 'bootstrap-select' +import 'bootstrap-select/dist/css/bootstrap-select.css' + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="selectpicker" attribute will cause + * this controller to be executed. The name "selectpicker" comes from the filename: + * selectpicker_controller.js -> "selectpicker" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + connect() { + var placeholder = 'ничего не выбрано' + + if (this.element.dataset.placeholder) { + placeholder = this.element.dataset.placeholder; + } + + jQuery(this.element).selectpicker({ + noneSelectedText : placeholder + }); + } +} diff --git a/assets/controllers/serviceDesk_controller.js b/assets/controllers/serviceDesk_controller.js new file mode 100644 index 0000000..cf59bd2 --- /dev/null +++ b/assets/controllers/serviceDesk_controller.js @@ -0,0 +1,32 @@ +import { Controller } from 'stimulus'; + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="default" attribute will cause + * this controller to be executed. The name "default" comes from the filename: + * default_controller.js -> "default" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + connect() { + this.element.addEventListener('click', function () { + const popupWrap = document.getElementById('popup'); + popupWrap.querySelector('.modal-title').textContent = 'Техническая поддержка'; + popupWrap.querySelector('.modal-dialog').classList.remove('modal-lg'); + + const popupBody = popupWrap.querySelector('#popup-body'); + popupBody.innerHTML = ''; + + const iframe = document.createElement('iframe'); + iframe.src = 'https://sovaclinic.okdesk.ru/webform/issues?account_name=sovaclinic'; + iframe.frameBorder = 0; + iframe.height = "450"; + iframe.width = "100%"; + popupBody.append(iframe); + + $(popupWrap).modal('show'); + }) + } +} diff --git a/assets/controllers/setting_controller.js b/assets/controllers/setting_controller.js new file mode 100644 index 0000000..fd3f413 --- /dev/null +++ b/assets/controllers/setting_controller.js @@ -0,0 +1,74 @@ +import { Controller } from 'stimulus'; +const loader = require("./../components/loader.js"); +const helper = require("./../components/helper.js"); + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="setting" attribute will cause + * this controller to be executed. The name "setting" comes from the filename: + * setting_controller.js -> "setting" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + connect() { + loader.loadSDK('setting'); + } + + submit() { + var plainPassword = document.getElementById('setting_plainPassword'); + var settingToken = document.getElementById('setting__token'); + + window.webSDK.clientModify({ + 'password': plainPassword.value + }).then(function(resolve) { + if (resolve.success) { + $.ajax({ + method: "POST", + crossDomain: false, + url: "/setting", + contentType: "application/x-www-form-urlencoded", + dataType: "json", + data: { + 'setting': { + '_token': settingToken.value, + 'plainPassword': plainPassword.value + } + }, + success(response) { + if (response.data.success == true) { + var parser = document.createElement('a'); + + parser.href = response.data.redirect; + + window.location.replace(document.location.origin + parser.pathname + parser.search); + } + } + }); + } + }).catch(function (error) { + helper.sendRequest({ + data: {'error': error, method: 'clientModify'} + }, helper.getHostname() + '/api/log', "POST", "json", true, "application/json"); + + console.log(error) + var msg = document.getElementById('msg'); + var div = document.createElement('div'); + div.classList = 'alert alert-danger'; + msg.innerHTML = ''; + msg.append(div); + + if (error.data.password) { + var p = document.createElement('p'); + p.innerHTML = error.data.password[0]; + div.append(p); + } else if (error.data['password.password'][0]) { + var p = document.createElement('p'); + p.innerHTML = error.data['password.password'][0]; + div.append(p); + } + }); + } + +} diff --git a/assets/controllers/signin_controller.js b/assets/controllers/signin_controller.js new file mode 100644 index 0000000..2f03a22 --- /dev/null +++ b/assets/controllers/signin_controller.js @@ -0,0 +1,513 @@ +import { Controller } from 'stimulus'; +import { sendReserve } from "./../components/record"; +import Inputmask from "inputmask"; + +const validator = require('./../components/validator.js'); +const helper = require('./../components/helper.js'); +const loader = require("./../components/loader.js"); +const Cookies = require('js-cookie'); + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="signin" attribute will cause + * this controller to be executed. The name "signin" comes from the filename: + * signin_controller.js -> "signin" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + connect() { + loader.loadSDK('signin').then(function (webSDK) { + webSDK.on('init', function() { + if (location.hash == '#recovery') { + const popup = document.getElementById('popup'); + + popup.dataset.controller = 'resetPassword'; + popup.dataset.recovery = true; + + $(popup).modal('show'); + } + }) + }) + } + + esia() { + const element = this.element + + element.addEventListener('click', function() { + const currentUrl = window.location.href; + const redirectUrl = helper.addUrlParam(currentUrl, 'esia', 'true'); + + window.webSDK.loadLoginView({ + authType: "esia", + redirectUrl: redirectUrl, + newWindow: false + }); + }); + } + + login() { + function renderAuth(msg = false) { + var popup = document.getElementById('popup'); + + // Сохраняем данные записи перед очисткой popup (если они есть) + if (popup && popup.dataset.reserve === 'true') { + const reserveData = { + specialistid: popup.dataset.specialistid, + filialid: popup.dataset.filialid, + depnum: popup.dataset.depnum, + schedident: popup.dataset.schedident, + workDate: popup.dataset.workDate, + time: popup.dataset.time, + onlinemode: popup.dataset.onlinemode, + comment: popup.dataset.comment, + docname: popup.dataset.docname, + address: popup.dataset.address, + company: popup.dataset.company, + rnum: popup.dataset.rnum + }; + // Сохраняем только если есть хотя бы одно поле + if (reserveData.specialistid || sessionStorage.getItem('reserveData')) { + sessionStorage.setItem('reserveData', JSON.stringify(reserveData)); + sessionStorage.setItem('reserveFlag', 'true'); + } + } + + var popupBody = popup.querySelector('.modal-title'); + popupBody.innerText = "Авторизация"; + + popupBody = popup.querySelector('#popup-body'); + popupBody.innerHTML = ''; + popupBody.dataset.controller = 'signin'; + + var validMessage = document.createElement('div'); + validMessage.classList = "msg-valid valid-message alert alert-danger d-none"; + + if (msg) { + validMessage.innerHTML = msg; + validMessage.classList.remove('d-none'); + } + + popupBody.append(validMessage); + + var username = document.createElement('input'); + username.type = "text"; + username.classList = "form-control border-radius"; + username.required = "true"; + username.autofocus = "off"; + username.autocomplete = "off"; + username.id = "username"; + + var groupPrepend = document.createElement('div'); + groupPrepend.classList = 'input-group-prepend'; + + var select = document.createElement('select'); + select.classList = "input-group-text"; + + var optionEmail = document.createElement('option'); + optionEmail.value = "email"; + optionEmail.innerText = "Email"; + select.append(optionEmail); + + var optionPhone = document.createElement('option'); + optionPhone.value = "phone"; + optionPhone.innerText = "Телефон"; + select.append(optionPhone); + groupPrepend.append(select); + + select.addEventListener('change', function() { + const selectedValue = this.value; + + if (selectedValue === 'email') { + username?.inputmask.remove(); + username.type = "email"; + } else if (selectedValue === 'phone') { + username.type = "text"; + Inputmask({ mask: '79999999999' }).mask(username); + } + }); + + var inputGroup = document.createElement('div'); + inputGroup.classList = "input-group"; + inputGroup.append(groupPrepend); + inputGroup.append(username); + + var formGroup = document.createElement('div'); + formGroup.classList = "form-group"; + + var validUsername = document.createElement('div'); + validUsername.classList = "msg-valid valid-username"; + formGroup.append(inputGroup); + formGroup.append(validUsername); + popupBody.append(formGroup); + + var password = document.createElement('input'); + password.placeholder = "Пароль"; + password.type = "password"; + password.classList = "form-control border-radius"; + password.required = "true"; + password.id = "password"; + password.addEventListener('keydown', function(evn) { + if (evn.code == 'Enter') { + loader.btnLoader(btnSubmit, true); + submit(window.webSDK, username, password, popupBody); + } + }); + + var formGroup = document.createElement('div'); + formGroup.classList = "form-group"; + formGroup.dataset.controller = 'passwordShow'; + formGroup.append(password); + + var validPassword = document.createElement('div'); + validPassword.classList = "msg-valid valid-password"; + formGroup.append(validPassword); + popupBody.append(formGroup); + + + /* политика */ + var div = document.createElement('div'); + div.classList = 'form-group'; + + var formCheck = document.createElement('div'); + formCheck.classList = 'form-check'; + div.append(formCheck); + + var accept = document.createElement('input'); + accept.classList = "form-check-input"; + accept.id = "accept"; + accept.checked = false; + accept.type = "checkbox"; + formCheck.append(accept); + + var license = helper.getLicenseLink(Cookies.get('region')); + var label = document.createElement('label'); + label.setAttribute('for' , 'accept'); + label.innerHTML = `ознакомлен(а) с условиями политики в отношении обработки персональных данных`; + + formCheck.append(label); + + var validAccept = document.createElement('div'); + validAccept.classList = 'msg-valid valid-accept'; + div.append(validAccept); + + popupBody.append(div) + + /* согласие */ + + var formCheck = document.createElement('div'); + formCheck.classList = 'form-check'; + div.append(formCheck); + + var acceptPerson = document.createElement('input'); + acceptPerson.classList = "form-check-input"; + acceptPerson.id = "acceptPerson"; + acceptPerson.checked = false; + acceptPerson.type = "checkbox"; + formCheck.append(acceptPerson); + + var licensePerson = helper.getLicensePersonLink(); + var labelPerson = document.createElement('label'); + labelPerson.setAttribute('for' , 'acceptPerson'); + labelPerson.innerHTML = `даю согласие на обработку персональных данных`; + + formCheck.append(labelPerson); + + var validAcceptPerson = document.createElement('div'); + validAcceptPerson.classList = 'msg-valid valid-acceptPerson'; + div.append(validAcceptPerson); + + popupBody.append(div) + + var btnSubmit = document.createElement('button'); + btnSubmit.type = "button"; + btnSubmit.innerHTML = "Войти"; + btnSubmit.classList = "btn btn-outline-secondary submit"; + btnSubmit.addEventListener('click', function() { + let invalid = false; + + if (validator.checkAccept(accept, validAccept)) { + invalid = true; + } + + if (validator.checkAccept(acceptPerson, validAcceptPerson)) { + invalid = true; + } + + if (invalid == false) { + loader.btnLoader(btnSubmit, true); + submit(window.webSDK, username, password, popupBody); + } + }); + + var helpLink = document.createElement('a'); + helpLink.classList = 'btn my-0'; + helpLink.dataset.controller = 'serviceDesk'; + helpLink.style = 'font-size: 21px;'; + helpLink.innerHTML = ``; + + var guLink = document.createElement('a'); + guLink.classList = 'btn my-0'; + guLink.dataset.action = 'signin#esia'; + guLink.innerHTML = `EISA`; + + var forgetLink = document.createElement('button'); + forgetLink.classList = 'btn'; + forgetLink.dataset.controller = 'resetPassword'; + forgetLink.innerHTML = 'Забыли пароль?'; + + var footer = document.createElement('div'); + footer.classList = 'modal-footer px-0'; + + footer.append(helpLink); + footer.append(guLink); + footer.append(forgetLink); + footer.append(btnSubmit); + popupBody.append(footer); + } + + renderAuth(); + + var popup = document.getElementById('popup'); + if (popup && typeof $(popup).modal !== 'undefined') { + $(popup).modal('show'); + } + + function submit(webSDK, username, password, popupBody) { + popupBody.querySelectorAll('.msg-valid').forEach(function (e) { + e.classList.add('d-none'); + e.innerHTML = ''; + }); + + webSDK.loginClient({ + login: username.value, + pass: password.value + }).then(function(user) { + + if (user.result && user.result.checkdata && typeof user.result.checkdata.pToken !== 'undefined') { + changeTempPassword(user.result); + return false; + } + + let data = { + user: user, + uid: user.id + } + + if (window.location.pathname !== '/login') { + data = { + user: user, + uid: user.id, + redirectFrom: window.location.href + } + } + + var popup = document.getElementById('popup'); + + if (popup.dataset.reserve === "true") { + data.redirectFrom = helper.getHostname() + '/case-history#doctor-success'; + } + + $.ajax({ + method: "POST", + crossDomain: true, + url: "/api/authenticated", + contentType: "application/x-www-form-urlencoded", + dataType: "json", + data: data, + success(response) { + if (response.data.success == true) { + // Восстанавливаем данные записи из sessionStorage если они были сохранены + const reserveFlag = popup.dataset.reserve === "true" || sessionStorage.getItem('reserveFlag') === 'true'; + + if (reserveFlag) { + // Восстанавливаем данные из sessionStorage + const reserveDataStr = sessionStorage.getItem('reserveData'); + if (reserveDataStr) { + try { + const reserveData = JSON.parse(reserveDataStr); + Object.keys(reserveData).forEach(key => { + if (reserveData[key] !== undefined && reserveData[key] !== null) { + popup.dataset[key] = reserveData[key]; + } + }); + console.log('Данные записи восстановлены из sessionStorage:', reserveData); + } catch (e) { + console.error('Ошибка при восстановлении данных из sessionStorage:', e); + } + } + + // Устанавливаем флаг reserve если его нет + popup.dataset.reserve = 'true'; + + // Проверяем наличие всех необходимых полей перед вызовом sendReserve + const requiredFields = ['specialistid', 'filialid', 'depnum', 'schedident', 'workDate', 'time']; + const missingFields = requiredFields.filter(field => !popup.dataset[field]); + + if (missingFields.length > 0) { + console.error('Отсутствуют необходимые поля для записи:', missingFields); + alert('Ошибка: не удалось восстановить данные для записи. Пожалуйста, попробуйте записаться снова.'); + // Очищаем sessionStorage + sessionStorage.removeItem('reserveData'); + sessionStorage.removeItem('reserveFlag'); + // Делаем обычный редирект + var parser = document.createElement('a'); + parser.href = response.data.redirect; + window.location.replace(helper.getHostname() + parser.pathname + parser.search); + return; + } + + // Очищаем sessionStorage после успешного восстановления + sessionStorage.removeItem('reserveData'); + sessionStorage.removeItem('reserveFlag'); + } + + if (window.bitrix === true) { + if (reserveFlag) { + // Для bitrix тоже нужно вызвать sendReserve для создания записи + // Сообщение покажет сам sendReserve после успешной записи + sendReserve(popup); + } else { + // Если не было флага reserve, показываем обычное сообщение + popup.querySelector('.modal-title').innerHTML = 'Запись на прием'; + var message = '

    Спасибо, Вы успешно записались на прием.
    '; + message += popup.dataset.comment + ', '; + message += popup.dataset.workDate + ' ' + popup.dataset.time + '

    '; + message += '

    Вы всегда можете отменить или перенести запись в личном кабинете по ссылке: '; + message += 'https://cabinet.sovamed.ru

    '; + popup.querySelector('#popup-body').innerHTML = message; + } + } else { + if (reserveFlag) { + // Если нужно создать запись, вызываем sendReserve, который сам сделает редирект + sendReserve(popup); + } else { + // Иначе делаем обычный редирект + var parser = document.createElement('a'); + parser.href = response.data.redirect; + window.location.replace(helper.getHostname() + parser.pathname + parser.search); + } + } + } + } + }); + }).catch(function(error) { + + helper.sendRequest({ + data: {'error': error, login: username.value, method: 'loginClient'} + }, helper.getHostname() + '/api/log', "POST", "json", true, "application/json"); + + var btnSubmit = popupBody.querySelector('button.submit'); + loader.btnLoader(btnSubmit, false); + + for (var prop in error.data) { + if (error.data[prop]) { + var el = popupBody.querySelector('.valid-' + prop) + el.classList.remove('d-none'); + el.innerHTML = error.data[prop]; + } + } + }); + }; + + function changeTempPassword(response) { + var popup = document.getElementById('popup'); + var popupBody = popup.querySelector('.modal-title') + .innerText = "Смена временного пароля"; + + var popupBody = popup.querySelector('#popup-body'); + popupBody.innerHTML = ''; + + var validMessage = document.createElement('div'); + validMessage.classList = "msg-valid valid-message alert alert-warning"; + validMessage.innerHTML = response.checkdata.msg; + popupBody.append(validMessage); + + var password = document.createElement('input'); + password.placeholder = "Новый пароль"; + password.type = "password"; + password.classList = "form-control border-radius"; + password.required = "true"; + password.id = "password"; + + var formGroup = document.createElement('div'); + formGroup.classList = "form-group"; + formGroup.append(password); + + var validPassword = document.createElement('div'); + validPassword.classList = "msg-valid valid-password"; + formGroup.append(validPassword); + popupBody.append(formGroup); + + var confirm = document.createElement('input'); + confirm.placeholder = "Подтверждение"; + confirm.type = "password"; + confirm.classList = "form-control border-radius"; + confirm.required = "true"; + confirm.id = "confirm"; + + var formGroup = document.createElement('div'); + formGroup.classList = "form-group"; + formGroup.append(confirm); + + var validConfirm = document.createElement('div'); + validConfirm.classList = "msg-valid valid-confirm"; + formGroup.append(validConfirm); + popupBody.append(formGroup); + + var btnSubmit = document.createElement('button'); + btnSubmit.type = "button"; + btnSubmit.innerHTML = "Сменить пароль"; + btnSubmit.classList = "btn btn-outline-secondary submit"; + btnSubmit.addEventListener('click', function() { + if (/^[\u0400-\u04FF]+$/.test(password.value)) { + popupBody.querySelector('.valid-password') + .innerHTML = 'Поле не должно содержать кириллицы'; + + return false; + } else { + popupBody.querySelector('.valid-password').innerHTML = ''; + } + + if (password.value !== confirm.value) { + popupBody.querySelector('.valid-confirm') + .innerHTML = 'Подтверждение не совпадает с паролем'; + + return false; + } else { + popupBody.querySelector('.valid-confirm').innerHTML = ''; + } + + loader.btnLoader(btnSubmit, true); + + window.webSDK.changeTempPassword({ + pwdToken: response.checkdata.pToken, + password: password.value, + confirm: confirm.value + }).then(function(resolve) { + renderAuth(resolve.data.success); + }).catch(function(e) { + helper.sendRequest({ + data: {'error': e, method: 'changeTempPassword'} + }, helper.getHostname() + '/api/log', "POST", "json", true, "application/json"); + var btnSubmit = popupBody.querySelector('button.submit'); + loader.btnLoader(btnSubmit, false); + + for (var prop in e.data.errors) { + if (e.data.errors[prop]) { + var el = popupBody.querySelector('.valid-' + prop.split('.')[1]) + el.classList.remove('d-none'); + el.innerHTML = e.data.errors[prop][0]; + } + } + }) + }); + + var footer = document.createElement('div'); + footer.classList = 'modal-footer'; + footer.append(btnSubmit); + popupBody.append(footer); + } + } +} \ No newline at end of file diff --git a/assets/controllers/slideshow_controller.js b/assets/controllers/slideshow_controller.js new file mode 100644 index 0000000..25d1c68 --- /dev/null +++ b/assets/controllers/slideshow_controller.js @@ -0,0 +1,64 @@ +import { Controller } from 'stimulus'; +import 'owl.carousel2/dist/assets/owl.carousel.css'; +import 'owl.carousel2/dist/assets/owl.theme.default.css'; +import 'owl.carousel2'; + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="hello" attribute will cause + * this controller to be executed. The name "hello" comes from the filename: + * hello_controller.js -> "hello" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + static targets = ["showid"]; + + connect() { + var slideshow = this.element; + + $(slideshow).owlCarousel({ + nav: true, + margin: 15, + autoplay: false, + autoplayTimeout: 7000, + autoplayHoverPause: true, + navText : [' ',' '], + responsive: { + 0: { + items: 1, + dots: false, + }, + 600: { + items: 2, + dots: false, + }, + 1000: { + items: 2, + dots: false, + } + } + }); + + slideshow.classList.remove('hide'); + + slideshow.querySelectorAll('.show-msg').forEach(function (el) { + el.addEventListener('click', function (evn) { + var showId = evn.target.dataset.showId; + var crop = slideshow.querySelector(`#crop-${showId}`); + var full = slideshow.querySelector(`#full-${showId}`); + + if (full.classList.contains('d-none')) { + crop.classList.add('d-none'); + full.classList.remove('d-none'); + evn.target.innerHTML = 'cвернуть'; + } else { + crop.classList.remove('d-none'); + full.classList.add('d-none'); + evn.target.innerHTML = 'весь отзыв'; + } + }); + }); + } +} \ No newline at end of file diff --git a/assets/controllers/smartCaptcha_controller.js b/assets/controllers/smartCaptcha_controller.js new file mode 100644 index 0000000..fa22e72 --- /dev/null +++ b/assets/controllers/smartCaptcha_controller.js @@ -0,0 +1,69 @@ +import { Controller } from 'stimulus'; +const helper = require("./../components/helper.js"); +const loader = require("./../components/loader.js"); + +export default class extends Controller { + connect() { + const btn = this.element; + + if (window.smartCaptcha) { + const container = document.getElementById('smart-captcha'); + const widgetId = window.smartCaptcha.render(container, { + sitekey: 'ysc1_EaQp6z8UPPQAIfHLm8mlrfFGee54huOrUEgGWgRpcf5c2225', + hl: 'ru', + }); + + container.dataset.id = widgetId; + + window.smartCaptcha.subscribe( widgetId, 'success', function () { + btn.disabled = false; + }); + } + + if (btn && btn?.type === 'submit') { + btn.disabled = true; + btn.addEventListener('click', function(evn) { + evn.preventDefault(); + loader.btnLoader(btn, true); + + if ($('#' + btn.dataset.formId)[0].checkValidity()) { + helper.sendRequest({'smart-token' : window.smartCaptcha.getResponse()}, '/api/smart-captcha').then(function (response) { + if (response.status != 'ok') { + loader.btnLoader(btn, false); + window.smartCaptcha.reset(); + } else { + $('#' + btn.dataset.formId).submit(); + } + }); + } else { + window.smartCaptcha.reset(); + $('#' + btn.dataset.formId)[0].reportValidity(); + loader.btnLoader(btn, false); + } + }); + } + } + + addYandexMetrica() { + const script = document.createElement('script'); + script.type = 'text/javascript'; + script.innerHTML = ` + (function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)}; + m[i].l=1*new Date();k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)}) + (window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym"); + `; + + document.head.appendChild(script); + } + + setYm() { + const ymNum = document.getElementsByName('ymNum')[0]?.value; + const ymId = document.getElementsByName('ymId')[0]?.value; + + if (ymNum && ymId) { + this.addYandexMetrica(); + + window.ym(ymNum,'reachGoal', ymId); + } + } +} diff --git a/assets/controllers/specialistView_controller.js b/assets/controllers/specialistView_controller.js new file mode 100644 index 0000000..82206ee --- /dev/null +++ b/assets/controllers/specialistView_controller.js @@ -0,0 +1,275 @@ +import { Controller } from 'stimulus'; +import Cookies from 'js-cookie'; +import { renderFormAnonym, sendReserve, renderFormBitrix } from "./../components/record"; + +const helper = require("./../components/helper.js"); +const record = require("./../components/record.js"); + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="specialistView" attribute will cause + * this controller to be executed. The name "specialistView" comes from the filename: + * detailDoctor_controller.js -> "specialistView" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + connect() { + let currentDate = this.element.dataset.st; + let bodyModal = document.getElementById('detail-specialist'); + var btnNext = bodyModal.querySelector('.btn-next'); + var btnPrev = bodyModal.querySelector('.btn-prev'); + + btnNext.addEventListener('click', function(evn) { + if (this.dataset.currentdate) { + var date = this.dataset.currentdate; + } else { + var date = currentDate; + } + + btnPrev.dataset.currentdate = new Date(date); + btnPrev.classList.remove('d-none'); + bodyModal.querySelector('.popup-btns').classList.remove('justify-content-end'); + bodyModal.querySelector('.popup-btns').classList.add('justify-content-between'); + date = getNextWeek(date); + + this.dataset.currentdate = new Date(date); + var departmentId = bodyModal.dataset.departmentid; + var specialistId = bodyModal.dataset.specialistid; + var filialId = bodyModal.dataset.filialid; + var onlineMode = bodyModal.dataset.onlinemode; + + pullInterval(specialistId, departmentId, filialId, date, onlineMode); + }); + + + btnPrev.addEventListener('click', function(evn) { + if (this.dataset.currentdate) { + var date = this.dataset.currentdate; + } else { + var date = currentDate; + } + + var nDate = new Date(currentDate).toLocaleString('ru', {year: 'numeric',month: 'numeric',day: 'numeric'}); + var cDate = new Date(date).toLocaleString('ru', {year: 'numeric',month: 'numeric',day: 'numeric'}); + + if (nDate == cDate) { + this.classList.add('d-none'); + bodyModal.querySelector('.popup-btns').classList.add('justify-content-end'); + bodyModal.querySelector('.popup-btns').classList.remove('justify-content-between'); + } + + btnNext.dataset.currentdate = new Date(date); + + var departmentId = bodyModal.dataset.departmentid; + var specialistId = bodyModal.dataset.specialistid; + var filialId = bodyModal.dataset.filialid; + var onlineMode = bodyModal.dataset.onlinemode; + + pullInterval(specialistId, departmentId, filialId, date, onlineMode); + + this.dataset.currentdate = new Date(getPrevWeek(date)); + }); + + this.element.querySelectorAll('.show-specialist-detail').forEach(function(btn) { + btn.addEventListener('click', function(evn) { + btnPrev.classList.add('d-none'); + bodyModal.querySelector('.popup-btns').classList.remove('justify-content-between'); + bodyModal.querySelector('.popup-btns').classList.add('justify-content-end'); + + var departmentId = bodyModal.dataset.departmentid = btn.dataset.departmentid; + var specialistId = bodyModal.dataset.specialistid = btn.dataset.specialistid; + bodyModal.dataset.company = btn.dataset.company; + var filialId = bodyModal.dataset.filialid = btn.dataset.filialid; + var onlineMode = bodyModal.dataset.onlinemode = btn.dataset.onlinemode; + + pullInterval(specialistId, departmentId, filialId, currentDate, onlineMode); + pullDoctor(specialistId); + }); + }); + + function pullDoctor(doctor) { + var modal = bodyModal.querySelector('.popup__content'); + + $.ajax({ + dataType: "json", + method: "GET", + crossDomain: true, + url: "/api/doctor", + data: { + 'sid': doctor, + }, + beforeSend() { + modal.querySelector('.loading').classList.remove('d-none'); + var aboutSpecialist = modal.querySelector('.about-specialist') + aboutSpecialist.classList.add('d-none'); + + aboutSpecialist.querySelector('.popup-staff__img').remove(); + }, + success(data) { + modal.querySelector('.loading').classList.add('d-none'); + modal.querySelector('.about-specialist').classList.remove('d-none'); + var content = modal.querySelector('.about-specialist'); + + var img = document.createElement('div'); + img.src = data.data.img; + img.classList = 'img-vr popup-staff__img'; + img.style.background = 'url(' + data.data.img + ') no-repeat'; + img.style.backgroundSize = 'cover'; + img.style.backgroundPosition = 'center -5px'; + + content.prepend(img); + content.querySelector('.popup-staff__name').innerHTML = data.data.name; + content.querySelector('.popup-staff__position').innerHTML = ''; + + if (data.data.speciality) { + content.querySelector('.popup-staff__position').innerHTML = data.data.speciality; + } + + content.querySelector('.popup-staff__exp').innerHTML = ''; + + if (data.data.experience) { + content.querySelector('.popup-staff__exp').innerHTML = 'Стаж:' + data.data.experience; + } + } + }); + } + + function pullInterval(doctor, department, filial, date, onlineMode) { + let wrap = bodyModal.querySelector('.calendar-wrap'); + let calendarBody = bodyModal.querySelector('.calendar-body'); + + $.ajax({ + dataType: "json", + method: "GET", + crossDomain: true, + url: "/api/interval", + data: { + 'update' : true, + 'doctor': doctor, + 'department': department, + 'filial': filial, + 'startInterval': getMonday(date), + 'endInterval': getSunday(date), + 'onlineMode': onlineMode + }, + beforeSend() { + bodyModal.querySelector('.current-dates').innerHTML = getMonday(date) + ' - ' + getSunday(date); + wrap.innerHTML = "Идет загрузка..."; + }, + success(response) { + + var isNotFree = true; + + wrap.innerHTML = ""; + response.data.intervalsData.forEach(function(el) { + if (el.isFree == true) { + var date = new Date(el.workDate).toLocaleString('ru', { + weekday: 'long', + month: 'long', + day: 'numeric' + }); + + var calBody = document.createElement('div'); + calBody.classList = 'calendar-body popup-inteval__item'; + + var timeDiv = document.createElement('div'); + timeDiv.classList = 'popup-inteval__date'; + timeDiv.innerHTML = date + ' ' + el.startInterval + '—'+ el.endInterval + ''; + calBody.append(timeDiv); + + + var intervalTimeDiv = document.createElement('div'); + intervalTimeDiv.classList = 'time-intervals'; + + var intervalDiv = document.createElement('div'); + intervalDiv.classList = 'interval intervals-wrap'; + + intervalTimeDiv.append(intervalDiv); + + for (let [key, item] of Object.entries(el.intervals)) { + if (item.isFree == true) { + isNotFree = false; + + var spanInterval = document.createElement('span'); + + spanInterval.classList = 'time available'; + spanInterval.innerText = item.startTime; + spanInterval.dataset.workDate = item.workDate; + spanInterval.dataset.schedident = item.schedident; + spanInterval.dataset.time = item.time; + spanInterval.dataset.onlinemode = item.onlineMode; + spanInterval.dataset.rnum = item.rNum; + + spanInterval.dataset.specialistid = doctor; + spanInterval.dataset.filialid = filial; + spanInterval.dataset.department = department; + + spanInterval.addEventListener('click', function (evn) { + var popupWrap = document.getElementById('popup'); + evn.target.dataset.onlinemode = item.onlineMode; + + if (record.renderFormRecord(response.data.userInfo, evn.target.dataset)) { + if (response.data.userInfo) { + evn.target.innerHTML = ''; + record.sendReserve(popupWrap) + } else { + $(popupWrap).modal('show'); + } + } + }) + + intervalDiv.append(spanInterval); + } + } + + if (!isNotFree) { + calBody.append(intervalDiv); + wrap.append(calBody); + } + } + }); + + if (isNotFree) { + wrap.innerHTML = "Всё занято. Выберите другую неделю."; + } + } + }); + } + + function getMonday(date) { + date = new Date(date); + + return new Date(date.setDate(date.getDate() - date.getDay() + 1)) + .toLocaleString('ru', { + year: 'numeric', + month: 'numeric', + day: 'numeric', + }); + } + + function getSunday(date) { + date = new Date(date); + + return new Date(date.setDate(date.getDate() - date.getDay() + 7)) + .toLocaleString('ru', { + year: 'numeric', + month: 'numeric', + day: 'numeric', + }); + } + + function getNextWeek(date) { + date = new Date(date); + + return new Date(date.getFullYear(), date.getMonth(), date.getDate() + 7); + } + + function getPrevWeek(date) { + date = new Date(date); + + return new Date(date.getFullYear(), date.getMonth(), date.getDate() - 7); + } + } +} diff --git a/assets/controllers/swaggerUI_controller.js b/assets/controllers/swaggerUI_controller.js new file mode 100644 index 0000000..c3ce6a8 --- /dev/null +++ b/assets/controllers/swaggerUI_controller.js @@ -0,0 +1,23 @@ +import { Controller } from 'stimulus'; +import "swagger-ui-dist/swagger-ui.css" + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="default" attribute will cause + * this controller to be executed. The name "default" comes from the filename: + * default_controller.js -> "default" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + connect() { + var SwaggerUIBundle = require('swagger-ui-dist').SwaggerUIBundle; + var wrap = this.element; + + SwaggerUIBundle({ + url: "/api/swagger.json", + dom_id: '#swagger-ui' + }); + } +} diff --git a/assets/controllers/uslugi_controller.js b/assets/controllers/uslugi_controller.js new file mode 100644 index 0000000..51efbbd --- /dev/null +++ b/assets/controllers/uslugi_controller.js @@ -0,0 +1,22 @@ +import { Controller } from 'stimulus'; +import Inputmask from "inputmask"; +import { renderFormBitrix } from "./../components/record" +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="uslugi" attribute will cause + * this controller to be executed. The name "hello" comes from the filename: + * hello_controller.js -> "hello" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + connect() { + var btn = this.element + btn.addEventListener('click', function () { + renderFormBitrix(this); + $('#popup').modal('show'); + }); + } +} + diff --git a/assets/controllers/widgets_controller.js b/assets/controllers/widgets_controller.js new file mode 100644 index 0000000..cb82a6e --- /dev/null +++ b/assets/controllers/widgets_controller.js @@ -0,0 +1,118 @@ +import { Controller } from 'stimulus'; + +const loader = require("./../components/loader.js"); +const helper = require("./../components/helper.js"); + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="widgets" attribute will cause + * this controller to be executed. The name "recaptcha" comes from the filename: + * recaptcha_controller.js -> "recaptcha" + * + * Delete this file or adapt it for your use! + */ +export default class extends Controller { + connect() { + loader.loadSDK('widget').then(function(webSDK) { + webSDK.on('init', function() { + if (this.data.user.authenticated) { + runWebSDK(); + } + }); + }) + + let runWebSDK = function () { + this.securityCard(); + this.finance(); + }.bind(this) + } + + finance() { + var financeWidget = document.getElementById('finance-widget'); + + window.webSDK.loadPaymentList({ + start: 0, + length: 4 + }).then(function (resolve) { + financeWidget.innerHTML = ''; + + if (resolve.length > 0) { + resolve.forEach(function(item) { + var div = document.createElement('div'); + div.classList = 'payment-line'; + + var date = window.newDate(item.date); + var spanID = document.createElement('span'); + spanID.classList = 'payment-line__id'; + spanID.innerHTML = 'Счет № ' + item.id + ' от ' + window.dateFormat(date, 'd-m-Y'); + div.append(spanID); + + var spanPrice = document.createElement('span'); + spanPrice.classList = 'payment-line__price'; + spanPrice.innerHTML = item.amt + ' ₽'; + div.append(spanPrice); + + financeWidget.append(div); + }); + } else { + var item = document.createElement('span'); + item.innerHTML = 'Записей не найдено'; + item.classList = 'line-item'; + financeWidget.append(item); + } + }).catch(function (error) { + helper.sendRequest({ + data: {'error': error, method: 'loadPaymentList'} + }, helper.getHostname() + '/api/log', "POST", "json", true, "application/json"); + }) + } + + securityCard() { + var securityCardWidget = document.getElementById('security-card-widget'); + var date = new Date(); + var lastDate = new Date(date.getFullYear(), date.getMonth() + 1, 0); + + if (window.webSDK.data.user.allows.caseHistory == true) { + window.webSDK.loadTreatPlaceList({ + st: 20200101, + en: window.dateFormat(lastDate), + start: 0, + length: 4 + }).then(function (resolve) { + securityCardWidget.innerHTML = ''; + + if (resolve.data.length > 0) { + resolve.data.forEach(function(el) { + var item = document.createElement('span'); + item.innerHTML = el.protocolName.slice(0,37) + '...'; + item.classList = 'line-item'; + + securityCardWidget.append(item); + }); + } else { + var item = document.createElement('span'); + item.innerHTML = 'Записей не найдено'; + item.classList = 'line-item'; + securityCardWidget.append(item); + } + }).catch(function (error) { + helper.sendRequest({ + data: {'error': error, method: 'loadTreatPlaceList'} + }, helper.getHostname() + '/api/log', "POST", "json", true, "application/json"); + + securityCardWidget.innerHTML = ''; + var item = document.createElement('span'); + item.innerHTML = 'Записей не найдено'; + item.classList = 'line-item'; + securityCardWidget.append(item); + }); + } else { + securityCardWidget.innerHTML = ''; + var item = document.createElement('span'); + item.innerHTML = 'Записей не найдено'; + item.classList = 'line-item'; + securityCardWidget.append(item); + } + } +} diff --git a/assets/fonts/Circe-Bold.eot b/assets/fonts/Circe-Bold.eot new file mode 100644 index 0000000..5928353 Binary files /dev/null and b/assets/fonts/Circe-Bold.eot differ diff --git a/assets/fonts/Circe-Bold.ttf b/assets/fonts/Circe-Bold.ttf new file mode 100644 index 0000000..00ca183 Binary files /dev/null and b/assets/fonts/Circe-Bold.ttf differ diff --git a/assets/fonts/Circe-Bold.woff b/assets/fonts/Circe-Bold.woff new file mode 100644 index 0000000..c148858 Binary files /dev/null and b/assets/fonts/Circe-Bold.woff differ diff --git a/assets/fonts/Circe-ExtraBold.eot b/assets/fonts/Circe-ExtraBold.eot new file mode 100644 index 0000000..91a3afc Binary files /dev/null and b/assets/fonts/Circe-ExtraBold.eot differ diff --git a/assets/fonts/Circe-ExtraBold.ttf b/assets/fonts/Circe-ExtraBold.ttf new file mode 100644 index 0000000..b9f3c09 Binary files /dev/null and b/assets/fonts/Circe-ExtraBold.ttf differ diff --git a/assets/fonts/Circe-ExtraBold.woff b/assets/fonts/Circe-ExtraBold.woff new file mode 100644 index 0000000..668baa5 Binary files /dev/null and b/assets/fonts/Circe-ExtraBold.woff differ diff --git a/assets/fonts/Circe-ExtraLight.eot b/assets/fonts/Circe-ExtraLight.eot new file mode 100644 index 0000000..c709bb2 Binary files /dev/null and b/assets/fonts/Circe-ExtraLight.eot differ diff --git a/assets/fonts/Circe-ExtraLight.ttf b/assets/fonts/Circe-ExtraLight.ttf new file mode 100644 index 0000000..ddd3989 Binary files /dev/null and b/assets/fonts/Circe-ExtraLight.ttf differ diff --git a/assets/fonts/Circe-ExtraLight.woff b/assets/fonts/Circe-ExtraLight.woff new file mode 100644 index 0000000..134fcaf Binary files /dev/null and b/assets/fonts/Circe-ExtraLight.woff differ diff --git a/assets/fonts/Circe-Light.eot b/assets/fonts/Circe-Light.eot new file mode 100644 index 0000000..3a99187 Binary files /dev/null and b/assets/fonts/Circe-Light.eot differ diff --git a/assets/fonts/Circe-Light.ttf b/assets/fonts/Circe-Light.ttf new file mode 100644 index 0000000..13d6591 Binary files /dev/null and b/assets/fonts/Circe-Light.ttf differ diff --git a/assets/fonts/Circe-Light.woff b/assets/fonts/Circe-Light.woff new file mode 100644 index 0000000..9b1b3d6 Binary files /dev/null and b/assets/fonts/Circe-Light.woff differ diff --git a/assets/fonts/Circe-Regular.eot b/assets/fonts/Circe-Regular.eot new file mode 100644 index 0000000..4a29d62 Binary files /dev/null and b/assets/fonts/Circe-Regular.eot differ diff --git a/assets/fonts/Circe-Regular.ttf b/assets/fonts/Circe-Regular.ttf new file mode 100644 index 0000000..7bb192f Binary files /dev/null and b/assets/fonts/Circe-Regular.ttf differ diff --git a/assets/fonts/Circe-Regular.woff b/assets/fonts/Circe-Regular.woff new file mode 100644 index 0000000..db22387 Binary files /dev/null and b/assets/fonts/Circe-Regular.woff differ diff --git a/assets/fonts/Circe-Thin.eot b/assets/fonts/Circe-Thin.eot new file mode 100644 index 0000000..3a9c588 Binary files /dev/null and b/assets/fonts/Circe-Thin.eot differ diff --git a/assets/fonts/Circe-Thin.ttf b/assets/fonts/Circe-Thin.ttf new file mode 100644 index 0000000..5b9ef37 Binary files /dev/null and b/assets/fonts/Circe-Thin.ttf differ diff --git a/assets/fonts/Circe-Thin.woff b/assets/fonts/Circe-Thin.woff new file mode 100644 index 0000000..eafb4b8 Binary files /dev/null and b/assets/fonts/Circe-Thin.woff differ diff --git a/assets/fonts/demo.html b/assets/fonts/demo.html new file mode 100644 index 0000000..cc097ac --- /dev/null +++ b/assets/fonts/demo.html @@ -0,0 +1,342 @@ + + + + + + + + Transfonter demo + + + + +
    +
    +

    Circe

    +
    .your-style {
    +    font-family: 'Circe';
    +    font-weight: normal;
    +    font-style: normal;
    +}
    +
    +

    + абвгдеёжзийклмнопрстуфхцчшщъыьэюя
    + АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ
    + abcdefghijklmnopqrstuvwxyz
    + ABCDEFGHIJKLMNOPQRSTUVWXYZ
    0123456789.:,;()*!?'@#<>$%&^+-=~ +

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +
    +
    +
    +

    Circe ExtraLight

    +
    .your-style {
    +    font-family: 'Circe';
    +    font-weight: 200;
    +    font-style: normal;
    +}
    +
    +

    + абвгдеёжзийклмнопрстуфхцчшщъыьэюя
    + АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ
    + abcdefghijklmnopqrstuvwxyz
    + ABCDEFGHIJKLMNOPQRSTUVWXYZ
    0123456789.:,;()*!?'@#<>$%&^+-=~ +

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +
    +
    +
    +

    Circe Thin

    +
    .your-style {
    +    font-family: 'Circe';
    +    font-weight: 100;
    +    font-style: normal;
    +}
    +
    +

    + абвгдеёжзийклмнопрстуфхцчшщъыьэюя
    + АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ
    + abcdefghijklmnopqrstuvwxyz
    + ABCDEFGHIJKLMNOPQRSTUVWXYZ
    0123456789.:,;()*!?'@#<>$%&^+-=~ +

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +
    +
    +
    +

    Circe Light

    +
    .your-style {
    +    font-family: 'Circe';
    +    font-weight: 300;
    +    font-style: normal;
    +}
    +
    +

    + абвгдеёжзийклмнопрстуфхцчшщъыьэюя
    + АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ
    + abcdefghijklmnopqrstuvwxyz
    + ABCDEFGHIJKLMNOPQRSTUVWXYZ
    0123456789.:,;()*!?'@#<>$%&^+-=~ +

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +
    +
    +
    +

    Circe Bold

    +
    .your-style {
    +    font-family: 'Circe';
    +    font-weight: bold;
    +    font-style: normal;
    +}
    +
    +

    + абвгдеёжзийклмнопрстуфхцчшщъыьэюя
    + АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ
    + abcdefghijklmnopqrstuvwxyz
    + ABCDEFGHIJKLMNOPQRSTUVWXYZ
    0123456789.:,;()*!?'@#<>$%&^+-=~ +

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +
    +
    +
    +

    Circe ExtraBold

    +
    .your-style {
    +    font-family: 'Circe';
    +    font-weight: 800;
    +    font-style: normal;
    +}
    +
    +

    + абвгдеёжзийклмнопрстуфхцчшщъыьэюя
    + АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ
    + abcdefghijklmnopqrstuvwxyz
    + ABCDEFGHIJKLMNOPQRSTUVWXYZ
    0123456789.:,;()*!?'@#<>$%&^+-=~ +

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +

    Съешь же ещё этих мягких французских булок, да выпей чаю.

    +
    +
    +
    + + \ No newline at end of file diff --git a/assets/fonts/stylesheet.css b/assets/fonts/stylesheet.css new file mode 100644 index 0000000..ec56e00 --- /dev/null +++ b/assets/fonts/stylesheet.css @@ -0,0 +1,67 @@ +/* This stylesheet generated by Transfonter (https://transfonter.org) on June 26, 2017 11:20 AM */ + +@font-face { + font-family: 'Circe'; + src: url('Circe-Regular.eot'); + src: local('Circe'), local('Circe-Regular'), + url('Circe-Regular.eot?#iefix') format('embedded-opentype'), + url('Circe-Regular.woff') format('woff'), + url('Circe-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Circe'; + src: url('Circe-ExtraLight.eot'); + src: local('Circe ExtraLight'), local('Circe-ExtraLight'), + url('Circe-ExtraLight.eot?#iefix') format('embedded-opentype'), + url('Circe-ExtraLight.woff') format('woff'), + url('Circe-ExtraLight.ttf') format('truetype'); + font-weight: 200; + font-style: normal; +} + +@font-face { + font-family: 'Circe'; + src: url('Circe-Thin.eot'); + src: local('Circe Thin'), local('Circe-Thin'), + url('Circe-Thin.eot?#iefix') format('embedded-opentype'), + url('Circe-Thin.woff') format('woff'), + url('Circe-Thin.ttf') format('truetype'); + font-weight: 100; + font-style: normal; +} + +@font-face { + font-family: 'Circe'; + src: url('Circe-Light.eot'); + src: local('Circe Light'), local('Circe-Light'), + url('Circe-Light.eot?#iefix') format('embedded-opentype'), + url('Circe-Light.woff') format('woff'), + url('Circe-Light.ttf') format('truetype'); + font-weight: 300; + font-style: normal; +} + +@font-face { + font-family: 'Circe'; + src: url('Circe-Bold.eot'); + src: local('Circe Bold'), local('Circe-Bold'), + url('Circe-Bold.eot?#iefix') format('embedded-opentype'), + url('Circe-Bold.woff') format('woff'), + url('Circe-Bold.ttf') format('truetype'); + font-weight: bold; + font-style: normal; +} + +@font-face { + font-family: 'Circe'; + src: url('Circe-ExtraBold.eot'); + src: local('Circe ExtraBold'), local('Circe-ExtraBold'), + url('Circe-ExtraBold.eot?#iefix') format('embedded-opentype'), + url('Circe-ExtraBold.woff') format('woff'), + url('Circe-ExtraBold.ttf') format('truetype'); + font-weight: 800; + font-style: normal; +} diff --git a/assets/img/3-staff.svg b/assets/img/3-staff.svg new file mode 100644 index 0000000..a6e408b --- /dev/null +++ b/assets/img/3-staff.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/assets/img/arrow-down.svg b/assets/img/arrow-down.svg new file mode 100644 index 0000000..5ce29f0 --- /dev/null +++ b/assets/img/arrow-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/img/calendar-input.svg b/assets/img/calendar-input.svg new file mode 100644 index 0000000..88d6533 --- /dev/null +++ b/assets/img/calendar-input.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/img/calendar.svg b/assets/img/calendar.svg new file mode 100644 index 0000000..a0302ec --- /dev/null +++ b/assets/img/calendar.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/img/check.svg b/assets/img/check.svg new file mode 100644 index 0000000..5702ea3 --- /dev/null +++ b/assets/img/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/img/checkbox.svg b/assets/img/checkbox.svg new file mode 100644 index 0000000..2dc8b5d --- /dev/null +++ b/assets/img/checkbox.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/img/checkbox_active.svg b/assets/img/checkbox_active.svg new file mode 100644 index 0000000..27e4729 --- /dev/null +++ b/assets/img/checkbox_active.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/img/close.svg b/assets/img/close.svg new file mode 100644 index 0000000..399be51 --- /dev/null +++ b/assets/img/close.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/img/fake-img.jpg b/assets/img/fake-img.jpg new file mode 100644 index 0000000..152e8b5 Binary files /dev/null and b/assets/img/fake-img.jpg differ diff --git a/assets/img/favorites.svg b/assets/img/favorites.svg new file mode 100644 index 0000000..9a0f71b --- /dev/null +++ b/assets/img/favorites.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/img/filter-ico.svg b/assets/img/filter-ico.svg new file mode 100644 index 0000000..8a74212 --- /dev/null +++ b/assets/img/filter-ico.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/img/filter.svg b/assets/img/filter.svg new file mode 100644 index 0000000..903e2a3 --- /dev/null +++ b/assets/img/filter.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/img/ny_top.png b/assets/img/ny_top.png new file mode 100644 index 0000000..dc2442e Binary files /dev/null and b/assets/img/ny_top.png differ diff --git a/assets/img/place.svg b/assets/img/place.svg new file mode 100644 index 0000000..9d56121 --- /dev/null +++ b/assets/img/place.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/img/repeat-visit.svg b/assets/img/repeat-visit.svg new file mode 100644 index 0000000..39ccbbc --- /dev/null +++ b/assets/img/repeat-visit.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/img/search-ico.svg b/assets/img/search-ico.svg new file mode 100644 index 0000000..6114f38 --- /dev/null +++ b/assets/img/search-ico.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/img/select-arrow-down.svg b/assets/img/select-arrow-down.svg new file mode 100644 index 0000000..ab5641b --- /dev/null +++ b/assets/img/select-arrow-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/img/select-arrow-up.svg b/assets/img/select-arrow-up.svg new file mode 100644 index 0000000..50e9ec8 --- /dev/null +++ b/assets/img/select-arrow-up.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/img/sova-bonus-logo.svg b/assets/img/sova-bonus-logo.svg new file mode 100644 index 0000000..fe2a75d --- /dev/null +++ b/assets/img/sova-bonus-logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/img/staff-icon.png b/assets/img/staff-icon.png new file mode 100644 index 0000000..6b0ff35 Binary files /dev/null and b/assets/img/staff-icon.png differ diff --git a/assets/img/up-arrow.svg b/assets/img/up-arrow.svg new file mode 100644 index 0000000..a13136b --- /dev/null +++ b/assets/img/up-arrow.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/img/user.svg b/assets/img/user.svg new file mode 100644 index 0000000..6deaea0 --- /dev/null +++ b/assets/img/user.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/loader_bitrix.js b/assets/loader_bitrix.js new file mode 100644 index 0000000..66ffeec --- /dev/null +++ b/assets/loader_bitrix.js @@ -0,0 +1,46 @@ +console.log('loader_bitix'); +import { startStimulusApp } from '@symfony/stimulus-bridge'; +import checkScheduleBitrix from './controllers/checkScheduleBitrix_controller'; +import signin from './controllers/signin_controller'; +import resetPassword from './controllers/resetPassword_controller'; +import captcha from './controllers/smartCaptcha_controller'; +import passwordShow from './controllers/passwordShow_controller'; +import inputMask from './controllers/inputMask_controller'; + +import './sass/main_bitrix.scss'; +import "@fancyapps/fancybox"; +import './window'; +import { syncSessionIdFromUrl } from './components/helper.js'; + +syncSessionIdFromUrl(); + +const loader = require("./components/loader.js"); + +loader.loadSDK('system').then(function (data) { + webSDK.on('init', function() { + window.userInfo = this.data.user.authenticated; + }) +}) + +window.bitrix = true; + +export const app = startStimulusApp(); + +// register any custom, 3rd party controllers here +app.register('inputMask', inputMask); +app.register('checkScheduleBitrix', checkScheduleBitrix); +app.register('signin', signin); +app.register('resetPassword', resetPassword); +app.register('smartCaptcha', captcha); +app.register('passwordShow', passwordShow); + + +/* + * Welcome to your app's main JavaScript file! + * + * We recommend including the built version of this JavaScript file + * (and its CSS file) in your base layout (base.html.twig). + */ + +// any CSS you import will output into a single css file (app.css in this case) + diff --git a/assets/loader_sovamed.js b/assets/loader_sovamed.js new file mode 100644 index 0000000..6ea3a4b --- /dev/null +++ b/assets/loader_sovamed.js @@ -0,0 +1,17 @@ +/* + * Welcome to your app's main JavaScript file! + * + * We recommend including the built version of this JavaScript file + * (and its CSS file) in your base layout (base.html.twig). + */ + +// start the Stimulus application +import './bootstrap'; + +// any CSS you import will output into a single css file (app.css in this case) +import 'bootstrap'; +import './sass/main.scss'; +import './window'; +import { syncSessionIdFromUrl } from './components/helper.js'; + +syncSessionIdFromUrl(); \ No newline at end of file diff --git a/assets/loader_widget.js b/assets/loader_widget.js new file mode 100644 index 0000000..dce0ee1 --- /dev/null +++ b/assets/loader_widget.js @@ -0,0 +1,17 @@ +/* + * Welcome to your app's main JavaScript file! + * + * We recommend including the built version of this JavaScript file + * (and its CSS file) in your base layout (base.html.twig). + */ + +// start the Stimulus application +import './bootstrap'; + +// any CSS you import will output into a single css file (app.css in this case) +import 'bootstrap'; +import './sass/main_widget.scss'; +import './window'; +import { syncSessionIdFromUrl } from './components/helper.js'; + +syncSessionIdFromUrl(); \ No newline at end of file diff --git a/assets/loader_wmtmed.js b/assets/loader_wmtmed.js new file mode 100644 index 0000000..7e63f5d --- /dev/null +++ b/assets/loader_wmtmed.js @@ -0,0 +1,17 @@ +/* + * Welcome to your app's main JavaScript file! + * + * We recommend including the built version of this JavaScript file + * (and its CSS file) in your base layout (base.html.twig). + */ + +// start the Stimulus application +import './bootstrap'; + +// any CSS you import will output into a single css file (app.css in this case) +import 'bootstrap'; +import './sass/main_wmtmed.scss'; +import './window'; +import { syncSessionIdFromUrl } from './components/helper.js'; + +syncSessionIdFromUrl(); diff --git a/assets/sass/_fonts.scss b/assets/sass/_fonts.scss new file mode 100644 index 0000000..172ece2 --- /dev/null +++ b/assets/sass/_fonts.scss @@ -0,0 +1,27 @@ +// font-weight helper: + +// 100 Extra Light or Ultra Light; +// 200 Light or Thin; 300 Book or Demi; +// 400 Regular or Normal; +// 500 Medium; +// 600 Semibold or Demibold; +// 700 Bold; +// 800 Black or Extra Bold or Heavy; +// 900 Extra Black or Fat or Ultra Black + + +@mixin font($f-name, $f-style, $f-weight, $f-url){ + @font-face{ + font-family: $f-name; + font-style: $f-style; + font-weight: $f-weight; + src: url($f-url) format('woff'); + } +} + +@include font('Circe', normal, 100, '../fonts/Circe-Thin.woff'); +@include font('Circe', normal, 200, '../fonts/Circe-ExtraLight.woff'); +@include font('Circe', normal, 300, '../fonts/Circe-Light.woff'); +@include font('Circe', normal, 400, '../fonts/Circe-Regular.woff'); +@include font('Circe', normal, bold, '../fonts/Circe-Bold.woff'); +@include font('Circe', normal, 800, '../fonts/Circe-ExtraBold.woff'); \ No newline at end of file diff --git a/assets/sass/bs4.scss b/assets/sass/bs4.scss new file mode 100644 index 0000000..b8fe254 --- /dev/null +++ b/assets/sass/bs4.scss @@ -0,0 +1,44 @@ +/*! + * Bootstrap v4.6.0 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors + * Copyright 2011-2021 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ + +@import "node_modules/bootstrap/scss/functions"; +@import "node_modules/bootstrap/scss/variables"; +@import "node_modules/bootstrap/scss/mixins"; +@import "node_modules/bootstrap/scss/root"; +@import "node_modules/bootstrap/scss/reboot"; +@import "node_modules/bootstrap/scss/type"; +@import "node_modules/bootstrap/scss/images"; +@import "node_modules/bootstrap/scss/code"; +@import "node_modules/bootstrap/scss/grid"; +@import "node_modules/bootstrap/scss/tables"; +@import "node_modules/bootstrap/scss/forms"; +@import "node_modules/bootstrap/scss/buttons"; +@import "node_modules/bootstrap/scss/transitions"; +@import "node_modules/bootstrap/scss/dropdown"; +@import "node_modules/bootstrap/scss/button-group"; +@import "node_modules/bootstrap/scss/input-group"; +@import "node_modules/bootstrap/scss/custom-forms"; +@import "node_modules/bootstrap/scss/nav"; +@import "node_modules/bootstrap/scss/navbar"; +@import "node_modules/bootstrap/scss/card"; +@import "node_modules/bootstrap/scss/breadcrumb"; +@import "node_modules/bootstrap/scss/pagination"; +@import "node_modules/bootstrap/scss/badge"; +@import "node_modules/bootstrap/scss/jumbotron"; +@import "node_modules/bootstrap/scss/alert"; +@import "node_modules/bootstrap/scss/progress"; +@import "node_modules/bootstrap/scss/media"; +@import "node_modules/bootstrap/scss/list-group"; +@import "node_modules/bootstrap/scss/close"; +@import "node_modules/bootstrap/scss/toasts"; +@import "node_modules/bootstrap/scss/modal"; +@import "node_modules/bootstrap/scss/tooltip"; +@import "node_modules/bootstrap/scss/popover"; +@import "node_modules/bootstrap/scss/carousel"; +@import "node_modules/bootstrap/scss/spinners"; +@import "node_modules/bootstrap/scss/utilities"; +@import "node_modules/bootstrap/scss/print"; diff --git a/assets/sass/main.scss b/assets/sass/main.scss new file mode 100644 index 0000000..6041994 --- /dev/null +++ b/assets/sass/main.scss @@ -0,0 +1,2268 @@ +@import "font-awesome"; +@import 'fonts'; +@import 'bootstrap'; + +@function rem-calc($size) { + $remSize: $size / 16; + @return #{$remSize}rem; +} + +.full-scren-modal { + position: absolute; + right: 50px; + top: 18px; + font-size: smaller; +} + +#cookie_note { + display: none; + position: fixed; + bottom: 0; + left: 0; + right: 0; + padding: 15px 25px; + z-index: 999; + background-color: #005b33; + box-sizing: border-box; + flex-direction: column; + gap: 15px; +} + +#cookie_note.show { + display: flex !important; +} + +@media (min-width: 576px) { + #cookie_note { + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 10px; + } +} + +#cookie_note p { + margin: 0; + font-size: 15px; + line-height: 1.55; + text-align: left; + color: #fff; +} + +#cookie_note a { + color: #fff; +} + +.btn-white { + background: #fff; +} + +#iframeProtocol { + width: 100%; + min-height: 400px; +} + +.full-screen#iframeProtocol { + min-height: 600px; +} + +.rating-widget-star { + font-size: 2em; +} + +.modal-dialog.full-screen { + width: 100%; + max-width: 100%; + height: 100%; + margin: 0; + padding: 0; +} + +.modal-content.full-screen { + height: auto; + min-height: 100%; + border-radius: 0; +} + +.rating-widget-star .fa-star, +.rating-widget-star .fa-star-o, +.rating-widget-star .fa-star-half-o { + color: green; +} + +.input-castom { + border: 1px solid #005B33; + border-radius: 10px; +} + +.banner { + text-align: center; + padding: 15px; +} + +.crop-message { + min-height: 112px; +} + +.full-message { + min-height: 162px; +} + +.msg-valid { + color:red; +} + +.bg-address { + height: 40px; + background: none; + border: 1px solid #005B33; + border-radius: 10px; + background: none; + color: #005B33; + padding: 10px 10px; +} + +.space-between { + flex-direction: column; + justify-content: space-between; +} + +.owl-next { + position: absolute; + right: 0; + margin-right: -10px !important; +} +.owl-prev { + position: absolute; + left: 0; + margin-left: -10px !important; +} +.owl-nav { + position: absolute; + top: 45%; + width: 100%; +} +.owl-stage-outer { + padding-bottom: 0.5rem !important; +} +.swiper-button-next, .swiper-button-prev { + position: absolute; + width: 48px; + height: 48px; + border-radius: 50%; + color: #fff; + background: url(https://sovamed.ru/bitrix/templates/sova2020/img/arrow-next.svg) no-repeat; + background-position: center; + cursor: pointer; +} + +.swiper-button-prev { + left: -10px !important; + transform: rotate(180deg); +} + +.swiper-button-next { + right: -10px !important; +} + +.owl-theme .owl-nav { + margin-top: 0px; +} + +.owl-theme .owl-nav [class*='owl-']:hover, .owl-theme .owl-nav [class*='owl-'] { + margin: 0; + padding: 0; + background: none; +} + +.sort-block { + display: flex; + justify-content: space-between; + margin-bottom: 15px; +} + +.call-back { + display: flex; + justify-content: flex-start; +} + +.call-back a { + color: #005B33; +} + +.bootstrap-select .dropdown-menu li a span.text { + white-space: normal; +} + +.fa-star, .fa-star-o, .fa-star-half-o { + color: #e3c45a; +} + +.register-init .btn-secondary:not(:disabled):not(.disabled):active, .btn-secondary:not(:disabled):not(.disabled).active, .show > .btn-secondary.dropdown-toggle { + background-color: #005b33; +} + +.story { + margin: 0 0 20px 0; +} + +.reviews-block__link, .truncate_more_link { + color: #005b33; +} + +.count { + border-radius: 99px; + background: #8EB7A5; + color: #004D2B; + padding: 0px 9px; + margin-left: 10px; +} + +.border { + border: 1px #fff solid; + border-radius: 10px; +} + +.popup__title { + height: 50px; + margin-bottom: 10px; + font-size: 24px; + line-height: 26px; + color: #004D2B; +} + +.popup-staff{ + display: flex; + align-items: center; + margin-bottom: 36px; +} + +.popup-staff__img{ + display: block; + border-radius: 100%; + width: 100px; + height: 100px; + margin-right: 20px; + flex-shrink: 0; +} + +.popup-staff__info { + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + font-size: 16px; + line-height: 19px; +} + +.popup-staff__name{ + color: #444444; + font-weight: bold; + text-decoration: none; + margin-bottom: 10px; +} + +.popup-staff__name:hover{ + text-decoration: underline; + color: #444; +} + +.popup-staff__param { + color: #8E8C8C; +} + +.popup-staff__position{ + margin-bottom: 5px; +} + +.popup-inteval{ + font-size: 16px; + line-height: 19px; +} + +.popup-inteval__item:not(:last-child){ + margin-bottom: 30px; +} +.popup-inteval__date{ + color: #444444; + margin-bottom: 15px; +} +.popup-inteval .intervals-wrap{ + grid-template-columns: repeat(4, 70px); +} +.popup-btns{ + display: flex; +} + +.justify-content-between { + justify-content: space-between; +} + +.justify-content-end { + justify-content: end; +} +.current-dates { + font-size: 16px; + line-height: 19px; + color: #8EB7A5; +} +.popup-btn:first-child{ + margin-right: 5px; +} +.popup-btn{ + min-width: 170px; + white-space: nowrap; + padding: 0 10px; +} +/* //модальное окно */ + +.search-input { + position: relative; +} +.dislike, .like { + cursor: pointer; +} +.search-input .show-content { + position: absolute; + z-index: 10; + width: 95%; + left: 17px; +} + +.search-input input { + background-color: #f7f5f5; + border: none; + border-radius: 10px; +} + +.search-input select { + width: 133px; + height: 37px; + line-height: 37px; + text-align: center; + margin-right: -133px; + position: relative; + z-index: 1; + float: left; + color: #6b6868; + background-color: #f7f5f5; + border: none; + padding-left: 0px; + border-radius: 10px; +} + +.search-input select + input { + padding-left: 140px; +} + +.show-content { + border-radius: 10px; + background: #F7F5F5; + border: 1px solid #005B33; + color: #004D2B; +} +.show-content a { + color: #165d5c; +} + +.search-wrap ul { + list-style: none; + padding: 10px 0px; +} + +.search-wrap ul > li { + padding: 0em 1em; + display: block; +} + +.form-control:focus { + background-color: #f7f5f5; +} + +*{ + box-sizing: border-box; +} + +html { + line-height: 1.15; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +body { + margin: 0; +} + +main { + display: block; +} + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ +} + +pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +a { + background-color: transparent; +} + +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} + +b, +strong { + font-weight: bolder; +} + +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +small { + font-size: 80%; +} + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +img { + border-style: none; +} + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} + +button, +input { /* 1 */ + overflow: visible; +} + +button, +select { /* 1 */ + text-transform: none; +} + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +fieldset { + padding: 0.35em 0.75em 0.625em; +} + +legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} + +progress { + vertical-align: baseline; +} + +textarea { + overflow: auto; +} + +[type="checkbox"], +[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +[type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +details { + display: block; +} + +summary { + display: list-item; +} + +template { + display: none; +} + +[hidden] { + display: none; +} + +/* breakpoints */ +$big-desktop: 1600px; +$desktop: 1200px; +$small-desktop: 1024px; +$phone: 768px; +$small-mobile: 560px; +$height-notebook: 768px; + +body{ + font-family: Circe; +} + +.login-page{ + margin-top: 5vh; + height: 90vh; + width: 100%; + background: #005B33; + border-radius: 10px; +} + +.h-md-100 { + height: 100% !important; +} + +@media (max-width: 768px) { + .h-md-100 { + height: 80% !important; + } + .col-md-4 { + flex: 0 0 100%; + max-width: 100%; + } + + .login-page { + margin-top: 0; + height: 100vh; + width: 100%; + background: #005B33; + border-radius: 10px; + } +} + +.с-container{ + width: 100%; + max-width: 1440px; + margin: 0 auto; +} +.main-container{ + display: flex; + flex-wrap: wrap; + margin-top: 30px; + min-height: calc(100vh - 60px); + margin-bottom: 20px; + padding: 0; +} + +.left-sidebar{ + flex: 0 0 21%; + max-width: 21%; + position: relative; + background: #005b33; + border-radius: 10px; + padding: 30px 40px; +} +.left-sidebar .city{ + display: none; +} +.center-content-2 { + flex: 0 0 79%; + max-width: 79%; + padding: 0 20px; +} +.center-content { + flex: 0 0 58%; + max-width: 58%; + padding: 0 20px; +} +.login-page .center-content{ + display: flex; + justify-content: center; + align-items: center; +} +.right-sidebar{ + flex: 0 0 21%; + max-width: 21%; + +} +.login-page .right-sidebar{ + padding-top: 30px; +} + +.city{ + background: url('../img/place.svg') no-repeat left center; + padding-left: 20px; + font-size: 16px; + line-height: 19px; + color: #fff; + cursor: pointer; +} + +.auth-btns{ + margin-top: 15px; +} + +.auth-btn:hover { + text-decoration: none; +} + +.auth-btn { + display: inline-flex; + align-items: center; + height: 30px; + font-size: 16px; + line-height: 19px; + padding: 5.5px 15px; + color: #fff; + border: 1px solid #fff; + border-radius: 15px; + text-decoration: none; + margin-bottom: 15px; + white-space: nowrap; +} +.auth-btn:last-child{ + margin-bottom: 0; +} +.auth-btn--normal{ + border: none; +} + +.auth-btn:not(.auth-btn--normal):hover{ + background: #fff; + color: #005B33; +} +.auth-btn--normal:hover { + background: #fff; + text-decoration: none; + color: #005B33; +} +.hello-text{ + color: #fff; + font-size: 24px; + line-height: 26px; + margin-bottom: 32px; +} + +.login-btns:not(:last-child){ + margin-bottom: 30px; +} +.login-btn{ + display: flex; + align-items: center; + justify-content: center; + height: 40px; + border-radius: 20px; + color: #fff; + font-size: 16px; + line-height: 19px; + border: 1px solid #FFFFFF; + text-decoration: none; + margin-bottom: 10px; +} +.login-btn:last-child{ + margin-bottom: 0; +} +.login-btn:hover{ + color: #005B33; + background: #fff; +} +.login-btn--bold-border{ + border: 3px solid #FFFFFF; +} +.login-btns--full .login-btn{ + width: 280px; +} + +.logo{ + margin-bottom: 50px; +} +.mobile-logo{ + display: none; +} + +.login-menu{ + position: relative; + display: flex; + flex-wrap: wrap; + // align-items: center; + min-height: 42px; + max-height: 43px; + margin: 0 -15px 30px; + color: #fff; + border: 1px solid #8EB7A5; + padding-left: 22px; + border-radius: 10px; + cursor: pointer; + transition: all 0.125s ease-in; +} +.login-menu__name{ + display: flex; + align-items: center; + height: 40px; +} +.login-menu__name svg{ + margin-right: 8.5px; +} +.login-menu__arrow{ + width: 10px; + height: 7px; + position: absolute; + top: 16px; + right: 15px; + background: url('../img/select-arrow-down.svg') no-repeat center center; +} +.login-menu__wrap{ + display: none; + margin-bottom: 10px; +} +.login-menu__item{ + display: flex; + align-items: center; + font-size: 16px; + line-height: 19px; + color: #444; + padding-left: 26px; + text-decoration: none; + height: 30px; +} +.login-menu__item:hover{ + text-decoration: underline; +} + +.login-menu.active{ + background: #fff; + min-height: 170px; + max-height: 170px; +} +.login-menu.active .login-menu__wrap{ + display: block; +} +.login-menu.active .login-menu__name{ + color: #005B33; +} +.login-menu.active .login-menu__name svg path{ + fill: #005B33; +} +.login-menu.active .login-menu__arrow{ + background: url('../img/select-arrow-up.svg') no-repeat center center; +} + +.select-login{ + width: 100%; + background: none; + border-radius: 10px; + border: 1px solid #8EB7A5; +} +.nice-select:after{ + border-bottom: 2px solid #fff; + border-right: 2px solid #fff; +} + +.select-login > span{ + color: #fff; + padding-left: 23.5px; + background: url('../img/user.svg') no-repeat; +} +.menu{ + display: block; + width: 100%; + float: left; +} +.menu ~ .menu{ + margin-top: 30px; +} +.menu__item{ + + display: block; + color: #fff; + font-size: 16px; + line-height: 19px; + text-decoration: none; + padding: 10px 20px; + margin: 0 -20px; + margin-bottom: 1px; + border-radius: 10px; +} +.menu__item:hover, .menu__item.active { + background: #004D2B; + color: #fff; + text-decoration: none; +} + +.mobile-menu .city{ + display: none; + width: 240px; + margin-bottom: 60px; +} + +.copyright{ + display: block; + position: absolute; + bottom: 10px; + font-size: 12px; + line-height: 14px; + color: #8EB7A5; + text-transform: uppercase; +} +.top-section{ + display: flex; + align-items: center; + margin-bottom: 20px; +} +.title{ + white-space: nowrap; + text-align: center; + font-size: 24px; + line-height: 26px; + font-weight: normal; + color: #444; + padding: 0 20px; + margin: 0; +} +.search-wrap{ + width: 100%; + padding-left: 20px; +} +.search-input { + width: 100%; + height: 40px; + font-size: 16px; + line-height: 19px; + box-shadow: none; + border: none; + border-radius: 10px; +} + +.search-btn { + position: absolute; + top: 0; + right: 0; + +} + +.right-sidebar__top{ + display: flex; + align-items: center; + margin-bottom: 20px; + justify-content: space-between; +} +.right-sidebar__top .button{ + white-space: nowrap; +} + +.three-staff{ + margin-right: 20px; +} +.mt-mb-3 { + margin-top: 1rem !important; +} + +.button{ + display: flex; + align-items: center; + justify-content: center; + height: 40px; + padding: 0 12px; + border-radius: 20px; + border: 1px solid #005B33; + font-size: 16px; + line-height: 19px; + color: #005B33; + text-decoration: none; + transition: all .125s ease-in; +} + +.button:hover:not(:disabled) { + background: #005B33; + color: #fff; + text-decoration: none; +} + +.button.disabled, .button:disabled, fieldset:disabled .button { + pointer-events: none; + opacity: .65; +} + +.button-revers{ + display: inline-flex; + align-items: center; + justify-content: center; + height: 40px; + padding: 0 20px; + border-radius: 20px; + background: #005B33; + border: 1px solid #005B33; + font-size: 16px; + line-height: 19px; + color: #fff; + text-decoration: none; + transition: all .125s ease-in; +} +.button-revers:hover{ + background: #fff; + color: #005B33; +} + +.right-sidebar .banners{ + margin-top: 20px; +} +.banner-img{ + display: block; + max-width: 100%; + margin-bottom: 20px; +} +.banner-img:last-child{ + margin-bottom: 0; +} +.main-blocks{ + display: flex; + flex-wrap: wrap; + justify-content: space-between; +} +.main-block{ + width: calc(50% - 10px); + min-height: 280px; + padding: 20px; + border: 1px solid #E6F0EC; + box-shadow: 0px 4px 15px rgba(0, 37, 21, 0.15); + border-radius: 10px; + margin-bottom: 20px; +} +.main-block__title, .tab-title{ + font-size: 12px; + line-height: 14px; + font-weight: bold; + text-transform: uppercase; + color: #004D2B; +} +.main-block__tabs-title{ + display: flex; + justify-content: space-between; +} +.tab-title{ + text-decoration: none; + color: #8EB7A5; +} +.tab-title:hover{ + color: #004D2B; +} +.tab-title--active{ + color: #004D2B; +} + +.tab-content{ + display: none; +} +.tab-content.tab-content--active{ + display: block; +} + +.main-block__content, .tabs-content{ + margin-top: 20px; +} + +.line-item, .payment-line{ + display: flex; + align-items: center; + padding: 10px; + height: 40px; + font-size: 16px; + line-height: 19px; + color: #444444; + border: 1px solid #8E8C8C; + border-radius: 10px; + margin-bottom: 10px; +} +.line-item--active{ + font-weight: bold; + border: 3px solid #8EB7A5; +} + +.payment-line{ + display: flex; + justify-content: space-between; +} + +.line-item:last-child, +.payment-line:last-child{ + margin-bottom: 0; +} + +.payment-line__price{ + color: #005B33; + font-weight: bold; + padding-left: 20px; + background: url('../img/check.svg') no-repeat left center; + white-space: nowrap; +} + +.staff-icons-list{ + display: flex; + flex-wrap: wrap; + padding-left: 15px; +} +.staff-link{ + display: block; + width: calc(25% + 15px); + margin-bottom: 10px; + margin-left: -15px; +} +.staff-link img{ + width: 100%; +} + +.favorites-link { + display: block; + width: calc(25% + 15px); + margin-bottom: 10px; + margin-left: -15px; +} +.favorites-link img{ + display: block; + border-radius: 100%; + margin-right: 16px; + width: 40px; + height: 40px; +} +a.sova-bonus-line__text { + color: white; +} + +.sova-bonus-line{ + display: flex; + align-items: center; + justify-content: space-between; + height: 40px; + background: #005B33; + padding: 10px 15px 10px 10px; + border-radius: 10px; + margin-bottom: 20px; + color: #fff; +} +.sova-bonus-line__text{ + font-size: 12px; + line-height: 14px; + text-transform: uppercase; +} +.right-sidebar .sova-bonus-line{ + display: inline-flex; +} +.top-section .sova-bonus-line{ + display: none; +} +.right-sidebar .sova-bonus-line__text{ + font-size: 20px; + line-height: 22px; + font-weight: bold; + text-transform: none; +} + +.sort-line{ + display: flex; + justify-content: flex-end; + margin-bottom: 15px; +} +.sort-line__ico{ + margin-right: 20px; +} +.sort-line__ico svg{ + max-width: 20xp; +} +.sort-line__item{ + font-size: 16px; + line-height: 19px; + color: #8EB7A5; + margin-right: 20px; + text-decoration: none; + cursor: pointer; +} +.sort-line__item:hover{ + text-decoration: underline; +} +.sort-line__item--active{ + color: #005B33; +} + +.mobile-actions{ + display: none; + margin-bottom: 25px; +} + +.mobile-filter-btn{ + display: flex; + align-items: center; + justify-content: center; + height: 40px; + border: 1px solid #005B33; + border-radius: 10px; + font-size: 16px; + line-height: 19px; + color: #005B33; + padding: 0 5px 0 13px; + text-decoration: none; +} +.mobile-filter-btn svg{ + margin-right: 8px; +} +.mobile-filter-btn:hover{ + color: #fff; + background: #005B33; + text-decoration: none; +} +.mobile-filter-btn:hover svg path{ + fill: #fff; +} + +.sort-select-wrap{ + position: relative; +} +/*.sort-select{ + width: 100%; + height: 40px; + padding-left: 30px; + border: 1px solid #005B33; + border-radius: 10px; + background: #fff; + color: #005B33; +}*/ + +.sort-select.bootstrap-select > .dropdown-toggle{ + padding-left: 30px; +} +.sort-select.bootstrap-select:not([class*="col-"]):not([class*="form-control"]):not(.input-group-btn){ + width: 251px !important; +} + +.sort-select-wrap .sort-line__ico{ + position: absolute; + z-index: 1; + top: 50%; + left: 10px; + transform: translateY(-50%); +} + +.block-title{ + font-size: 12px; + line-height: 14px; + font-weight: bold; + color: #8E8C8C; + text-transform: uppercase; + padding-left: 20px; + margin-bottom: 10px; +} + +.block-content{ + background: #fff; + border: 1px solid #e6f0ec; + border-radius: 10px; + box-shadow: 0px 4px 15px rgba(0, 37, 21, 0.15); +} + +.staff-block{ + display: flex; + justify-content: space-between; + padding: 20px; + margin-bottom: 10px; +} +.staff-block:last-child{ + margin-bottom: 0; +} +.staff-block__main{ + display: flex; + align-items: center; +} +.staff-block__img{ + display: block; + border-radius: 100%; + width: 100px; + height: 100px; + margin-right: 20px; + flex-shrink: 0; +} +.staff-info{ + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + font-size: 16px; + line-height: 19px; + padding-right: 10px; + width: 280px; +} +.staff-info__name{ + color: #444444; + font-weight: bold; + text-decoration: none; +} +.staff-info__name:hover{ + text-decoration: underline; + color: #444; +} +.staff-info__review{ + color: #8E8C8C; + text-decoration: none; +} +.staff-info__review:hover{ + text-decoration: underline; +} +.staff-info__param--dms-no{ + color: #dc3545; +} + +.staff-info__time-list{ + max-width: 300px; + flex-shrink: 0; +} +.time-list{ + font-size: 16px; + line-height: 19px; + display: flex; + flex-direction: column; + justify-content: space-between; +} +.time-list__title{ + color: #444; +} + +.grid-none { + padding: 0px 10px 10px 0px; +} + +.grid-list { + display: grid; + grid-template-columns: 1fr 1fr 1fr; +} + +.intervals-wrap .time{ + display: inline-block; + font-size: 15px; + font-weight: 400; + margin-bottom: 5px; + margin-right: 10px; + padding: 6px 11px; +} +.intervals-wrap .time.available{ + border: 1px solid #005b33; + border-radius: 20px; + box-sizing: border-box; + color: #005b33; + cursor: pointer; + padding: 5px 10px; + transition: all 0.125s ease-in; +} +.intervals-wrap .time.available:hover{ + background: #005B33; + color: #fff; +} +.time-list__intervals{ + display: flex; + flex-wrap: wrap; + align-items: flex-end; + margin: 30px 0; +} + +.btn-show, .btn-show-specialist-detail{ + background: #fff; + display: block; + border: 1px solid #005b33; + border-radius: 20px; + box-sizing: border-box; + color: #005b33; + cursor: pointer; + text-decoration: none; + padding: 5px 10px; + margin-bottom: 5px; + transition: all 0.125s ease-in; +} +.btn-show:hover, .btn-show-specialist-detail:hover{ + background: #005B33; + color: #fff; + text-decoration: none; +} + +.time-list__bottom{ + display: flex; + justify-content: space-between; +} +.time-list__price{ + color: #8E8C8C; +} + +.fa-bookmark, .fa-bookmark-o { + color: #005B33; +} + +.calendar-btn{ + color: #005B33; + background: url('../img/calendar.svg') no-repeat left center; + padding-left: 23px; +} +.repeat-btn{ + color: #005B33; + background: url('../img/repeat-visit.svg') no-repeat left center; + padding-left: 23px; +} + +.filter{ + background: #E6F0EC; + padding: 18px; + border-radius: 10px; +} +.filter__close{ + display: none; +} +.filter__title{ + display: flex; + align-items: center; + justify-content: center; + height: 30px; + margin-bottom: 10px; + color: #005B33; + font-size: 12px; + line-height: 14px; + font-weight: bold; + text-align: center; + text-transform: uppercase; +} +.d-none{ + display: none; +} +.filter__input-wrap{ + margin-bottom: 10px; +} +.filter__input{ + width: 100% !important; + height: 40px; + padding: 0 20px; + background: none; + border: 1px solid #005B33; + color: #005B33; + border-radius: 10px; +} +.filter__input--date{ + background: url('../img/calendar-input.svg') no-repeat calc(100% - 17px) center; +} +.filter__select.show{ + background: #fff; + border-bottom: none; + border-radius: 10px 10px 0 0; +} +.dropdown-menu{ + width: 100%; +} +.dropdown-menu.show{ + width: 100%; + top: -1px !important; + margin-top: 0; + border: 1px solid #005B33; + border-top: 1px solid transparent; + border-radius: 0 0 10px 10px; +} + +.filter__btn{ + cursor: pointer; + display: block; + margin: 0 auto; + border: 1px solid #005b33; + border-radius: 20px; + box-sizing: border-box; + color: #005b33; + padding: 10px 20px; + font-size: 16px; + line-height: 19px; + background: none; + transition: all 0.125s ease-in; +} +.filter__btn:hover{ + background: #005B33; + color: #fff; +} +.filter__checkbox{ + display: none; +} +.filter__checkbox-label{ + position: relative; + padding-left: 25px; + background: url('../img/checkbox.svg') no-repeat left center; + color: #005B33; +} + +.filter__checkbox:checked ~ .filter__checkbox-label{ + background: url('../img/checkbox_active.svg') no-repeat left center; +} + +.staff-detail{ + padding: 34px 30px; + color: #444444; + font-size: 16px; + line-height: 19px; + margin-bottom: 10px; +} +.staff-detail h1, .staff-detail h2, .staff-detail h3{ + margin-top: 0; +} +.staff-reviews{ + display: flex; + justify-content: space-between; + flex-wrap: wrap; + +} + +.owl-item { + background: #fff; + border: 1px solid #e6f0ec; + border-radius: 10px; + box-shadow: 0px 4px 15px rgb(0 37 21 / 15%); + padding: 20px 15px; +} + +.card-item{ + margin-bottom: 20px; +} +.card-item__block{ + padding: 20px; + display: flex; + justify-content: space-between; + align-items: flex-end; + font-size: 16px; + line-height: 19px; +} +.card-item__title{ + color: #444444; + font-weight: bold; + min-height: 40px; + margin-bottom: 10px; +} +.card-item__btns{ + display: flex; +} +.card-item__btn:not(:last-child){ + margin-right: 10px; +} +.pagination{ + display: flex; + justify-content: center; +} +.pagination__item{ + display: flex; + align-items: center; + justify-content: center; + text-decoration: none; + color: #005B33; + width: 35px; + height: 35px; +} +.pagination__item:not(.pagination__item--active):hover{ + text-decoration: underline; +} +.pagination__item--active{ + background: #E6F0EC; + border: 1px solid #E6F0EC; + box-sizing: border-box; + border-radius: 100%; +} +.mobile-tabs{ + display: none; + height: 40px; + padding: 0 20px; + border: 1px solid #005B33; + border-radius: 10px; + color: #005B33; + background: #E6F0EC; + margin-bottom: 20px; +} +.tabs{ + display: flex; + margin: 0 0 25px 20px; +} +.tab-item{ + display: block; + font-size: 16px; + line-height: 19px; + color: #005B33; + box-sizing: border-box; + border-radius: 10px; + padding: 10.5px 20px; + text-decoration: none; + cursor: pointer; +} +.tab-item:not(.tab-item--active):hover, +.tab-item--active:hover { + text-decoration: none; + color: #005B33 !important; +} +.tab-item--active { + font-weight: bold; + background: #E6F0EC; + color: #004D2B; +} + +.visit{ + font-size: 16px; + line-height: 19px; + width: 340px; +} + +.visit__info{ + display: flex; + justify-content: space-between; + margin-bottom: 50px; +} +.visit__time{ + font-weight: bold; + color: #004D2B; +} +.visit__cancel{ + color: #8E8C8C; + text-decoration: none; +} +.visit__cancel:hover{ + text-decoration: underline; +} +.visit__place{ + color: #444444; + margin-bottom: 15px; +} +.visit__btns{ + display: flex; + float: right; +} +.visit__btns .calendar-btn, +.visit__btns .repeat-btn{ + margin-right: 30px; +} + +.finance{ + display: grid; + grid-template-columns: 1fr 1fr; + padding: 20px; + min-height: 180px; + font-size: 16px; + line-height: 19px; + margin-bottom: 12px; +} +.finance > *{ + margin-bottom: 10px; +} +.finance > *:nth-child(5), +.finance > *:nth-child(6){ + margin-bottom: 0; +} +/*.finance__col{ + width: 50%; + display: flex; + flex-wrap: wrap; + flex-direction: column; + justify-content: space-between; +} +.finance__row{ + width: 100%; + display: flex; + justify-content: space-between; + margin-bottom: 20px; +} +.finance__row:last-child{ + margin-bottom: 0; +} +.finance__row > *{ + display: flex; + align-items: center; + width: 50%; +}*/ +.finance__date{ + color: #004D2B; + font-weight: bold; + align-items: flex-start; +} +.finance__price { + color: #005B33; + font-weight: bold; + padding-left: 20px; + background: url("../img/check.svg") no-repeat left 3px; +} +.finance__filial{ + color: #444444; + align-items: flex-end; +} +.finance__name{ + color: #444444; +} +.finance__staff{ + color: #444444; +} +.finance__btn{ + padding: 0 15px; + height: 30px; +} + +.help-btns{ + margin-bottom: 20px; +} +.help-btn{ + display: inline-flex; + justify-content: center; + align-items: center; + height: 40px; + font-size: 16px; + line-height: 19px; + color: #005B33; + padding: 10.5px 20px; + margin-right: 20px; + border: 1px solid #005B33; + border-radius: 20px; + text-decoration: none; + transition: all 0.125s ease-in; +} +.help-btn:last-child{ + margin-right: 0; +} +.help-btn:hover{ + background: #005B33; + color: #fff; +} + +.collapses{ + font-size: 16px; + line-height: 19px; + padding: 0 20px; + margin-bottom: 10px; +} +.collapses__link{ + position: relative; + display: block; + height: 50px; + padding: 15px 0; + color: #444; + font-weight: bold; + text-decoration: none; +} +.collapses__link:hover{ + color: #444; +} +.collapses__link:before{ + width: 10px; + height: 5px; + content: ""; + position: absolute; + right: 0; + top: 50%; + transform: translateY(-50%); + background: url('../img/arrow-down.svg') no-repeat right center; + background-size: cover; +} +.collapses__link:not(.collapsed):before{ + transform: translateY(-50%) rotate(180deg); +} + +.collapse-item{ + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + padding: 5px 0; +} +.collapse-item--text{ + font-weight: 300; +} +.collapse-item__name{ + color: #444; +} +.collapse-item__btn{ + padding: 5.5px 15px; + color: #005B33; + border: 1px solid #8EB7A5; + border-radius: 15px; + background: #fff; +} +.collapse-item__btn:hover{ + color: #fff; + background: #8EB7A5; +} +.collapse{ + padding-bottom: 25px; +} +.collapse:not(.show) { + display: none; +} + +.info{ + padding: 20px; + font-size: 16px; + line-height: 19px; +} +.info__link{ + display: block; + color: #444444; + text-decoration: none; + margin-bottom: 10px; + min-height: 20px; +} +.info__link:hover{ + text-decoration: underline; +} +.info__link:last-child{ + margin-bottom: 0; +} + +.ny_top { + background: url(../img/ny_top.png) repeat-x; + width: 100%; + height: 45px; +} + +.up-arrow{ + display: none; + width: 55px; + height: 55px; + background: url('../img/up-arrow.svg') no-repeat center center; + background-size: cover; + cursor: pointer; + position: fixed; + right: 20px; + bottom: 20px; +} +.mobile-copyright{ + display: none; +} + +.btn-light, +.btn-light:hover{ + border-color: transparent; +} +.btn-light.focus, .btn-light:focus{ + box-shadow: none; + border-color: transparent; +} +.bootstrap-select > .dropdown-toggle{ + height: 40px; + background: none; + border: 1px solid #005B33; + border-radius: 10px; + background: none; + color: #005B33; + padding: 0 20px; +} + +.bootstrap-select .dropdown-toggle .filter-option{ + padding-top: 6px; +} + +.bootstrap-select > .dropdown-toggle, +.bootstrap-select > .dropdown-toggle:active, +.bootstrap-select > .dropdown-toggle:focus, +.bootstrap-select > .dropdown-toggle:hover { + color: #005B33; +} + +.bootstrap-select > .dropdown-toggle.bs-placeholder, +.bootstrap-select > .dropdown-toggle.bs-placeholder:active, +.bootstrap-select > .dropdown-toggle.bs-placeholder:focus, +.bootstrap-select > .dropdown-toggle.bs-placeholder:hover { + color: #005B33; +} + +.btn-light:not(:disabled):not(.disabled).active, +.btn-light:not(:disabled):not(.disabled):active, +.show > .btn-light.dropdown-toggle{ + border-bottom: 1px solid transparent; + border-radius: 10px 10px 0 0; +} +.btn-light:not(:disabled):not(.disabled).active:focus, +.btn-light:not(:disabled):not(.disabled):active:focus, +.show > .btn-light.dropdown-toggle:focus{ + box-shadow: none; + background: #fff; +} + +.bootstrap-select > .dropdown-toggle:active, +.bootstrap-select > .dropdown-toggle:focus, +.bootstrap-select > .dropdown-toggle:hover{ + background: none; + outline: none !important; +} + +.btn-light:not(:disabled):not(.disabled).active, +.btn-light:not(:disabled):not(.disabled):active, +.show > .btn-light.dropdown-toggle{ + background: #fff; + border-color: #005B33; + border-bottom: none; +} + +.bootstrap-select > .dropdown-toggle.bs-placeholder:active, +.bootstrap-select > .dropdown-toggle.bs-placeholder:focus, +.bootstrap-select > .dropdown-toggle.bs-placeholder:hover{ + background: none; + outline: none !important; +} +.dropdown-toggle:after{ + border: none; + width: 10px; + height: 7px; + background: url("../img/arrow-down.svg") no-repeat center center; + background-size: contain; +} + +.bootstrap-select:not([class*="col-"]):not([class*="form-control"]):not(.input-group-btn) { + width: 261px; +} + +.bootstrap-select.show-tick .dropdown-menu .selected span.check-mark{ + left: 10px; +} +.form-control:focus{ + box-shadow: none; + border-color: #005B33; +} + +/*@media screen and (max-width: 1024px) and (min-width: 561px) { + .three-staff{ + margin-right: 10px; + } + .button{ + padding: 0 10px; + white-space: nowrap; + } + .left-sidebar{ + padding: 30px 20px; + } + .login-menu{ + padding-left: 5px; + } + .main-block{ + width: 100%; + min-height: auto; + } + .filter__input--date{ + background: none; + } +}*/ + +@media screen and (max-width: 1024px) and (min-width: 561px){ + .staff-block-wrap{ + display: flex; + flex-wrap: wrap; + justify-content: space-between; + } + .staff-block{ + width: calc(50% - 5px); + } +} + +@media screen and (max-width: 1440px) and (min-width: 1024px) { + .copyright { + bottom: 10px; + } + + .menu ~ .menu { + margin-top: 15px; + } + + .center-content { + flex: 0 0 58%; + max-width: 58%; + padding: 0 10px; + } + + .right-content { + flex: 0 0 22%; + max-width: 22%; + } + + .left-sidebar { + flex: 0 0 20%; + max-width: 20%; + padding: 30px 25px; + } + + .staff-info { + width: 247px; + } + + .staff-block { + padding: 15px 10px; + margin-bottom: 10px; + } + + .staff-block__img { + margin-right: 10px; + } + + .bootstrap-select:not([class*=col-]):not([class*=form-control]):not(.input-group-btn) { + width: 100%; + } +} + +@media screen and (max-width: 1024px){ + .btn-pay { + margin-bottom: 1em; + } + .up-arrow{ + display: block; + } + .main-container{ + margin-top: 0; + margin-bottom: 0; + flex-direction: column; + } + .login-page.main-container{ + height: 100vh; + margin-bottom: 0; + } + .login-page .center-content{ + align-items: flex-start; + } + + .left-sidebar, + .center-content, + .right-sidebar{ + width: 100%; + flex: 0 0 100%; + max-width: 100%; + margin-top: 13px; // отступ для ny + } + .center-content{ + min-height: calc(100vh - 142px); + } + .right-sidebar__top{ + display: none; + } + .filter{ + display: none; + } + .filter.active{ + display: block; + height: 100%; + position: fixed; + z-index: 3; + top: 0; + right: 0; + border-radius: 10px 0 0 10px; + } + .filter__close{ + display: block; + position: absolute; + right: 20px; + top: 5px; + cursor: pointer; + } + .filter__close svg{ + width: 25px; + height: 25px; + } + + .sort-select.bootstrap-select:not([class*=col-]):not([class*=form-control]):not(.input-group-btn) { + width: 200px !important; + } + + .left-sidebar{ + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: 20px; + border-radius: 0; + height: 80px; + z-index: 2; + } + .left-sidebar .city{ + display: block; + margin-top: 10px; + } + .left-sidebar.menu-active{ + position: fixed; + top: 0; + left: 0; + justify-content: flex-end; + } + .login-menu{ + width: 250px; + } + .burger{ + position: relative; + width: 30px; + height: 30px; + cursor: pointer; + margin-top: 10px; + } + .burger.active{ + margin-top: 15px; + } + .burger span{ + display: block; + width: 100%; + height: 2px; + background: #fff; + margin-bottom: 9px; + // transition: all .25s ease-in; + } + .burger span:last-child{ + margin-bottom: 0; + } + + .burger.active span:nth-child(1){ + position: absolute; + top: 5px; + transform: rotate(45deg); + } + .burger.active span:nth-child(2){ + opacity: 0; + } + .burger.active span:nth-child(3){ + position: absolute; + top: 5px; + transform: rotate(-45deg); + } + .mobile-menu{ + display: none; + width: 100%; + height: calc(100vh - 66px); + position: absolute; + left: 0; + top: 66px; + background: #005b33; + } + .burger.active ~ .mobile-menu{ + display: flex; + flex-wrap: wrap; + flex-direction: column; + justify-content: flex-start; + align-items: center; + } + .login-menu{ + margin-bottom: 0; + } + .login-menu.active{ + position: relative; + z-index: 1; + } + .menu-active .login-menu, + .menu-active .logo-link{ + display: none; + } + .menu{ + max-width: 240px; + } + .mobile-menu .city{ + display: block; + } + .copyright{ + display: none; + } + .logo{ + display: none; + margin-bottom: 0; + } + .login-page .logo{ + display: block; + } + .login-page .right-sidebar{ + display: none; + } + .mobile-logo{ + display: block; + } + + .top-section{ + position: relative; + flex-direction: column-reverse; + align-items: flex-start; + padding-top: 10px; + } + .search-wrap{ + margin-bottom: 20px; + padding-left: 0; + } + .title{ + padding: 0; + } + .block-title{ + padding: 0; + } + .main-block{ + width: 100%; + min-height: auto; + } + .sort-line{ + display: none; + } + .mobile-actions{ + display: flex; + justify-content: space-between; + } + + .staff-block{ + flex-wrap: wrap; + } + + .staff-block__main{ + width: 100%; + margin-bottom: 25px; + } + + .time-list{ + width: 100%; + } + .time-list__intervals{ + margin: 15px 0; + } + + .staff-review{ + width: 100%; + margin-bottom: 10px; + } + + .tabs{ + display: none; + } + .mobile-tabs{ + display: block; + } + + .card-item__block{ + flex-wrap: wrap; + } + .card-item__info{ + width: 100%; + margin-bottom: 20px; + } + .card-item__title{ + min-height: auto; + } + + .visit__info{ + margin-bottom: 10px; + } + .visit{ + width: 100%; + } + + .mobile-copyright{ + display: flex; + width: 100%; + height: 60px; + justify-content: center; + align-items: center; + font-size: 12px; + line-height: 14px; + text-transform: uppercase; + color: #8EB7A5; + } + + .finance{ + grid-template-columns: 1fr; + } + .finance > *:nth-child(3){ + margin-bottom: 30px; + } + .finance > *:nth-child(5){ + margin-bottom: 10px; + } + + .top-section .sova-bonus-line{ + display: flex; + position: absolute; + right: 0; + bottom: 3px; + margin-bottom: 0; + padding: 0; + height: auto; + background: none; + color: #004D2B; + } + .top-section .sova-bonus-line__text{ + font-size: 16px; + line-height: 19px; + text-transform: none; + } + + .collapse{ + padding-bottom: 0; + } + .collapse-item__name{ + margin-bottom: 10px; + } + + .help-btns{ + display: flex; + flex-wrap: wrap; + justify-content: center; + } + .help-btn{ + margin-right: 0; + margin-bottom: 20px; + } + .help-btn:last-child{ + margin-bottom: 0; + } + .ny_top { + position: absolute; + z-index: 3; + top: 0; + left: 0; + } +} \ No newline at end of file diff --git a/assets/sass/main_bitrix.scss b/assets/sass/main_bitrix.scss new file mode 100644 index 0000000..6c40345 --- /dev/null +++ b/assets/sass/main_bitrix.scss @@ -0,0 +1,70 @@ +@import "font-awesome"; +@import 'fonts'; +// @import 'bootstrap/dist/css/bootstrap-grid.css'; + +#popup .btn.btn-primary, .btn-primary:hover, .btn.btn-primary:focus { + background-color: #3a613e; + border-color: #3a613e; + font-size: 19px; +} + +#popup .btn-primary:not(:disabled):not(.disabled):active, .btn-primary:not(:disabled):not(.disabled).active, .show > .btn-primary.dropdown-toggle { + color: #fff; + background-color: #3a613e; + border-color: #3a613e; +} + +#popup a { + color: #000; + text-decoration: none; +} + +#popup a:hover { + color: #666; + text-decoration: none; +} + +#popup .license-link { + font-size: small; +} + +#popup .w-100 { + width: 100%; +} + +#popup .d-none { + display: none; +} + +#popup .mb-3 { + margin-bottom: 15px; +} + +#popup .veretify-code-block { + align-items: flex-end; + margin-top: 15px; + display: grid; + grid-gap: 0px; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: auto; + grid-template-areas: + "m m h" + "c c c"; +} + +#popup .veretify-code-block .form-control { + grid-area: m; +} + +#popup .veretify-code-block .input-group-append { + grid-area: h; + text-align: right; +} + +#popup .veretify-code-block .valid-veretify-phone { + grid-area: c; +} + +.mt-3, .my-3 { + margin-top: 1rem !important; +} \ No newline at end of file diff --git a/assets/sass/main_widget.scss b/assets/sass/main_widget.scss new file mode 100644 index 0000000..754ab82 --- /dev/null +++ b/assets/sass/main_widget.scss @@ -0,0 +1,45 @@ +@import "font-awesome"; +@import 'fonts'; +@import 'bootstrap'; + +.btn.btn-primary, .btn-primary:hover, .btn.btn-primary:focus { + background-color: #3a613e; + border-color: #3a613e; + font-size: 19px; +} + +.btn-primary:not(:disabled):not(.disabled):active, .btn-primary:not(:disabled):not(.disabled).active, .show > .btn-primary.dropdown-toggle { + color: #fff; + background-color: #3a613e; + border-color: #3a613e; +} + +a { + color: #000; + text-decoration: none; +} + +a:hover { + color: #666; + text-decoration: none; +} + +.license-link { + font-size: small; +} + +.smart-captcha { + width: calc(100% - 10px); + padding-left: 10px; +} + +#refund-form label { + margin-bottom: 0; +} + +#refund-form .form-group { + margin-bottom: 5px; +} +.refund-wrapper .alert { + margin-bottom: 5px; +} \ No newline at end of file diff --git a/assets/sass/main_wmtmed.scss b/assets/sass/main_wmtmed.scss new file mode 100644 index 0000000..25c0ae3 --- /dev/null +++ b/assets/sass/main_wmtmed.scss @@ -0,0 +1,2237 @@ +@import "font-awesome"; +@import 'fonts'; +@import 'bootstrap'; + +@function rem-calc($size) { + $remSize: $size / 16; + @return #{$remSize}rem; +} + +.rating-widget-star { + font-size: 2em; +} + +#cookie_note { + display: none; + position: fixed; + bottom: 0; + left: 0; + right: 0; + padding: 15px 25px; + z-index: 999; + background-color: #48b1b8; + box-sizing: border-box; + flex-direction: column; + gap: 15px; +} + +#cookie_note.show { + display: flex !important; +} + +@media (min-width: 576px) { + #cookie_note { + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 10px; + } +} + +#cookie_note p { + margin: 0; + font-size: 15px; + line-height: 1.55; + text-align: left; + color: #fff; +} + +#cookie_note a { + color: #fff; +} + +.btn-white { + background: #fff; +} + +#iframeProtocol { + width: 100%; + min-height: 400px; +} + +.rating-widget-star .fa-star, +.rating-widget-star .fa-star-o, +.rating-widget-star .fa-star-half-o { + color: green; +} + +.banner { + text-align: center; + padding: 15px; +} + +.crop-message { + min-height: 112px; +} + +.full-message { + min-height: 162px; +} + +.msg-valid { + color:red; +} + +.bg-address { + height: 40px; + background: none; + border: 1px solid #267373; + border-radius: 10px; + background: none; + color: #267373; + padding: 10px 10px; +} + +.space-between { + flex-direction: column; + justify-content: space-between; +} + +.owl-next { + position: absolute; + right: 0; + margin-right: -10px !important; +} +.owl-prev { + position: absolute; + left: 0; + margin-left: -10px !important; +} +.owl-nav { + position: absolute; + top: 45%; + width: 100%; +} +.owl-stage-outer { + padding-bottom: 0.5rem !important; +} +.swiper-button-next, .swiper-button-prev { + position: absolute; + width: 48px; + height: 48px; + border-radius: 50%; + color: #fff; + background: url(https://sovamed.ru/bitrix/templates/sova2020/img/arrow-next.svg) no-repeat; + background-position: center; + cursor: pointer; +} + +.swiper-button-prev { + left: -10px !important; + transform: rotate(180deg); +} + +.swiper-button-next { + right: -10px !important; +} + +.owl-theme .owl-nav { + margin-top: 0px; +} + +.owl-theme .owl-nav [class*='owl-']:hover, .owl-theme .owl-nav [class*='owl-'] { + margin: 0; + padding: 0; + background: none; +} + +.sort-block { + display: flex; + justify-content: space-between; + margin-bottom: 15px; +} + +.call-back { + display: flex; + justify-content: flex-start; +} + +.call-back a { + color: #267373; +} + +.bootstrap-select .dropdown-menu li a span.text { + white-space: normal; +} + +.fa-star, .fa-star-o, .fa-star-half-o { + color: #e3c45a; +} + +.register-init .btn-secondary:not(:disabled):not(.disabled):active, .btn-secondary:not(:disabled):not(.disabled).active, .show > .btn-secondary.dropdown-toggle { + background-color: #267373; +} + +.story { + margin: 0 0 20px 0; +} + +.reviews-block__link, .truncate_more_link { + color: #267373; +} + +.count { + border-radius: 99px; + background: #8EB7A5; + color: #267373; + padding: 0px 9px; + margin-left: 10px; +} + +.border { + border: 1px #fff solid; + border-radius: 10px; +} + +.popup__title { + height: 50px; + margin-bottom: 10px; + font-size: 24px; + line-height: 26px; + color: #267373; +} + +.popup-staff{ + display: flex; + align-items: center; + margin-bottom: 36px; +} + +.popup-staff__img{ + display: block; + border-radius: 100%; + width: 100px; + height: 100px; + margin-right: 20px; + flex-shrink: 0; +} + +.popup-staff__info { + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + font-size: 16px; + line-height: 19px; +} + +.popup-staff__name{ + color: #444444; + font-weight: bold; + text-decoration: none; + margin-bottom: 10px; +} + +.popup-staff__name:hover{ + text-decoration: underline; + color: #444; +} + +.popup-staff__param { + color: #8E8C8C; +} + +.popup-staff__position{ + margin-bottom: 5px; +} + +.popup-inteval{ + font-size: 16px; + line-height: 19px; +} + +.popup-inteval__item:not(:last-child){ + margin-bottom: 30px; +} +.popup-inteval__date{ + color: #444444; + margin-bottom: 15px; +} +.popup-inteval .intervals-wrap{ + grid-template-columns: repeat(4, 70px); +} +.popup-btns{ + display: flex; +} + +.justify-content-between { + justify-content: space-between; +} + +.justify-content-end { + justify-content: end; +} +.current-dates { + font-size: 16px; + line-height: 19px; + color: #8EB7A5; +} +.popup-btn:first-child{ + margin-right: 5px; +} +.popup-btn{ + min-width: 170px; + white-space: nowrap; + padding: 0 10px; +} +/* //модальное окно */ + +.search-btn { + position: absolute; + top: 0; + right: 0; +} + +.search-input { + position: relative; +} +.dislike, .like { + cursor: pointer; +} +.search-input .show-content { + position: absolute; + z-index: 10; + width: 95%; + left: 17px; +} + +.search-input input { + background-color: #f7f5f5; + border: none; + border-radius: 10px; +} + +.search-input select { + width: 133px; + height: 37px; + line-height: 37px; + text-align: center; + margin-right: -133px; + position: relative; + z-index: 1; + float: left; + color: #6b6868; + background-color: #f7f5f5; + border: none; + padding-left: 5px; + border-radius: 10px; +} + +.search-input select + input { + padding-left: 140px; +} + +.show-content { + border-radius: 10px; + background: #F7F5F5; + border: 1px solid #267373; + color: #267373; +} +.show-content a { + color: #165d5c; +} + +.search-wrap ul { + list-style: none; + padding: 10px 0px; +} + +.search-wrap ul > li { + padding: 0em 1em; + display: block; +} + +.form-control:focus { + background-color: #f7f5f5; +} + +*{ + box-sizing: border-box; +} + +html { + line-height: 1.15; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +body { + margin: 0; +} + +main { + display: block; +} + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ +} + +pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +a { + background-color: transparent; +} + +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} + +b, +strong { + font-weight: bolder; +} + +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +small { + font-size: 80%; +} + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +img { + border-style: none; +} + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} + +button, +input { /* 1 */ + overflow: visible; +} + +button, +select { /* 1 */ + text-transform: none; +} + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +fieldset { + padding: 0.35em 0.75em 0.625em; +} + +legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} + +progress { + vertical-align: baseline; +} + +textarea { + overflow: auto; +} + +[type="checkbox"], +[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +[type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +details { + display: block; +} + +summary { + display: list-item; +} + +template { + display: none; +} + +[hidden] { + display: none; +} + +/* breakpoints */ +$big-desktop: 1600px; +$desktop: 1200px; +$small-desktop: 1024px; +$phone: 768px; +$small-mobile: 560px; +$height-notebook: 768px; + +body{ + font-family: Circe; +} + +.login-page{ + margin-top: 5vh; + height: 90vh; + width: 100%; + background: #48b1b8; + border-radius: 10px; +} + +.h-md-100 { + height: 100% !important; +} + +@media (max-width: 768px) { + .h-md-100 { + height: 80% !important; + } + .col-md-4 { + flex: 0 0 100%; + max-width: 100%; + } + + .login-page { + margin-top: 0; + height: 100vh; + width: 100%; + background: #48b1b8; + border-radius: 10px; + } +} + +.с-container{ + width: 100%; + max-width: 1440px; + margin: 0 auto; +} +.main-container{ + display: flex; + flex-wrap: wrap; + margin-top: 30px; + min-height: calc(100vh - 60px); + margin-bottom: 20px; + padding: 0; +} + +.left-sidebar{ + flex: 0 0 21%; + max-width: 21%; + position: relative; + background: #48b1b8; + border-radius: 10px; + padding: 30px 40px; +} +.left-sidebar .city{ + display: none; +} +.center-content-2 { + flex: 0 0 79%; + max-width: 79%; + padding: 0 20px; +} +.center-content { + flex: 0 0 58%; + max-width: 58%; + padding: 0 20px; +} +.login-page .center-content{ + display: flex; + justify-content: center; + align-items: center; +} +.right-sidebar{ + flex: 0 0 21%; + max-width: 21%; + +} +.login-page .right-sidebar{ + padding-top: 30px; +} + + +.city{ + background: url('../img/place.svg') no-repeat left center; + padding-left: 20px; + font-size: 16px; + line-height: 19px; + cursor: pointer; + color: #fff; +} + +.auth-btns{ + margin-top: 15px; +} + +.auth-btn:hover { + text-decoration: none; +} + +.auth-btn { + display: inline-flex; + align-items: center; + height: 30px; + font-size: 16px; + line-height: 19px; + padding: 5.5px 15px; + color: #fff; + border: 1px solid #fff; + border-radius: 15px; + text-decoration: none; + margin-bottom: 15px; + white-space: nowrap; +} +.auth-btn:last-child{ + margin-bottom: 0; +} +.auth-btn--normal{ + border: none; +} + +.auth-btn:not(.auth-btn--normal):hover{ + background: #fff; + color: #267373; +} +.auth-btn--normal:hover { + background: #fff; + text-decoration: none; + color: #267373; +} +.hello-text{ + color: #fff; + font-size: 24px; + line-height: 26px; + margin-bottom: 32px; +} + +.login-btns:not(:last-child){ + margin-bottom: 30px; +} +.login-btn{ + display: flex; + align-items: center; + justify-content: center; + height: 40px; + border-radius: 20px; + color: #fff; + font-size: 16px; + line-height: 19px; + border: 1px solid #FFFFFF; + text-decoration: none; + margin-bottom: 10px; +} +.login-btn:last-child{ + margin-bottom: 0; +} +.login-btn:hover{ + color: #267373; + background: #fff; +} +.login-btn--bold-border{ + border: 3px solid #FFFFFF; +} +.login-btns--full .login-btn{ + width: 280px; +} + +.logo{ + margin-bottom: 50px; +} +.mobile-logo{ + display: none; +} + +.login-menu{ + position: relative; + display: flex; + flex-wrap: wrap; + // align-items: center; + min-height: 42px; + max-height: 43px; + margin: 0 -15px 30px; + color: #fff; + border: 1px solid #fff; + padding-left: 22px; + border-radius: 10px; + cursor: pointer; + transition: all 0.125s ease-in; +} +.login-menu__name{ + display: flex; + align-items: center; + height: 40px; +} +.login-menu__name svg{ + margin-right: 8.5px; +} +.login-menu__arrow{ + width: 10px; + height: 7px; + position: absolute; + top: 16px; + right: 15px; + background: url('../img/select-arrow-down.svg') no-repeat center center; +} +.login-menu__wrap{ + display: none; + margin-bottom: 10px; +} +.login-menu__item{ + display: flex; + align-items: center; + font-size: 16px; + line-height: 19px; + color: #444; + padding-left: 26px; + text-decoration: none; + height: 30px; +} +.login-menu__item:hover{ + text-decoration: underline; +} + +.login-menu.active{ + background: #fff; + min-height: 170px; + max-height: 170px; +} +.login-menu.active .login-menu__wrap{ + display: block; +} +.login-menu.active .login-menu__name{ + color: #267373; +} +.login-menu.active .login-menu__name svg path{ + fill: #267373; +} +.login-menu.active .login-menu__arrow{ + background: url('../img/select-arrow-up.svg') no-repeat center center; +} + + + + +.select-login{ + width: 100%; + background: none; + border-radius: 10px; + border: 1px solid #8EB7A5; +} +.nice-select:after{ + border-bottom: 2px solid #fff; + border-right: 2px solid #fff; +} + +.select-login > span{ + color: #fff; + padding-left: 23.5px; + background: url('../img/user.svg') no-repeat; +} +.menu{ + display: block; + width: 100%; + float: left; +} +.menu ~ .menu{ + margin-top: 30px; +} +.menu__item{ + + display: block; + color: #fff; + font-size: 16px; + line-height: 19px; + text-decoration: none; + padding: 10px 20px; + margin: 0 -20px; + margin-bottom: 1px; + border-radius: 10px; +} +.menu__item:hover, .menu__item.active { + background: #267373; + color: #fff; + text-decoration: none; +} + +.mobile-menu .city{ + display: none; + width: 240px; + margin-bottom: 60px; +} + +.copyright{ + display: block; + position: absolute; + bottom: 10px; + font-size: 12px; + line-height: 14px; + color: #1a5858; + text-transform: uppercase; +} +.top-section{ + display: flex; + align-items: center; + margin-bottom: 20px; +} +.title{ + white-space: nowrap; + text-align: center; + font-size: 24px; + line-height: 26px; + font-weight: normal; + color: #444; + padding: 0 20px; + margin: 0; +} +.search-wrap{ + width: 100%; + padding-left: 20px; +} + +.search-input { + width: 100%; + height: 40px; + font-size: 16px; + line-height: 19px; + box-shadow: none; + border: none; + border-radius: 10px; +} + +.right-sidebar__top{ + display: flex; + align-items: center; + margin-bottom: 20px; + justify-content: space-between; +} +.right-sidebar__top .button{ + white-space: nowrap; +} + +.three-staff{ + margin-right: 20px; +} +.mt-mb-3 { + margin-top: 1rem !important; +} + +.button{ + display: flex; + align-items: center; + justify-content: center; + height: 40px; + padding: 0 12px; + border-radius: 20px; + border: 1px solid #267373; + font-size: 16px; + line-height: 19px; + color: #267373; + text-decoration: none; + transition: all .125s ease-in; +} +.button:hover{ + background: #267373; + color: #fff; + text-decoration: none; +} + +.button-revers{ + display: inline-flex; + align-items: center; + justify-content: center; + height: 40px; + padding: 0 20px; + border-radius: 20px; + background: #267373; + border: 1px solid #267373; + font-size: 16px; + line-height: 19px; + color: #fff; + text-decoration: none; + transition: all .125s ease-in; +} +.button-revers:hover{ + background: #fff; + color: #267373; +} + +.right-sidebar .banners{ + margin-top: 20px; +} +.banner-img{ + display: block; + max-width: 100%; + margin-bottom: 20px; +} +.banner-img:last-child{ + margin-bottom: 0; +} +.main-blocks{ + display: flex; + flex-wrap: wrap; + justify-content: space-between; +} +.main-block{ + width: calc(50% - 10px); + min-height: 280px; + padding: 20px; + border: 1px solid #E6F0EC; + box-shadow: 0px 4px 15px rgba(0, 37, 21, 0.15); + border-radius: 10px; + margin-bottom: 20px; +} +.main-block__title, .tab-title{ + font-size: 12px; + line-height: 14px; + font-weight: bold; + text-transform: uppercase; + color: #267373; +} +.main-block__tabs-title{ + display: flex; + justify-content: space-between; +} +.tab-title{ + text-decoration: none; + color: #8EB7A5; +} +.tab-title:hover{ + color: #267373; +} +.tab-title--active{ + color: #267373; +} + +.tab-content{ + display: none; +} +.tab-content.tab-content--active{ + display: block; +} + +.main-block__content, .tabs-content{ + margin-top: 20px; +} + +.line-item, .payment-line{ + display: flex; + align-items: center; + padding: 10px; + height: 40px; + font-size: 16px; + line-height: 19px; + color: #444444; + border: 1px solid #8E8C8C; + border-radius: 10px; + margin-bottom: 10px; +} +.line-item--active{ + font-weight: bold; + border: 3px solid #8EB7A5; +} + +.payment-line{ + display: flex; + justify-content: space-between; +} + +.line-item:last-child, +.payment-line:last-child{ + margin-bottom: 0; +} + +.payment-line__price{ + color: #267373; + font-weight: bold; + padding-left: 20px; + background: url('../img/check.svg') no-repeat left center; + white-space: nowrap; +} + +.staff-icons-list{ + display: flex; + flex-wrap: wrap; + padding-left: 15px; +} +.staff-link{ + display: block; + width: calc(25% + 15px); + margin-bottom: 10px; + margin-left: -15px; +} +.staff-link img{ + width: 100%; +} + +.favorites-link { + display: block; + width: calc(25% + 15px); + margin-bottom: 10px; + margin-left: -15px; +} +.favorites-link img{ + display: block; + border-radius: 100%; + margin-right: 16px; + width: 40px; + height: 40px; +} +a.sova-bonus-line__text { + color: white; +} + +.sova-bonus-line{ + display: flex; + align-items: center; + justify-content: space-between; + height: 40px; + background: #267373; + padding: 10px 15px 10px 10px; + border-radius: 10px; + margin-bottom: 20px; + color: #fff; +} +.sova-bonus-line__text{ + font-size: 12px; + line-height: 14px; + text-transform: uppercase; +} +.right-sidebar .sova-bonus-line{ + display: inline-flex; +} +.top-section .sova-bonus-line{ + display: none; +} +.right-sidebar .sova-bonus-line__text{ + font-size: 20px; + line-height: 22px; + font-weight: bold; + text-transform: none; +} + +.sort-line{ + display: flex; + justify-content: flex-end; + margin-bottom: 15px; +} +.sort-line__ico{ + margin-right: 20px; +} +.sort-line__ico svg{ + max-width: 20xp; +} +.sort-line__item{ + font-size: 16px; + line-height: 19px; + color: #8EB7A5; + margin-right: 20px; + text-decoration: none; + cursor: pointer; +} +.sort-line__item:hover{ + text-decoration: underline; +} +.sort-line__item--active{ + color: #267373; +} + +.mobile-actions{ + display: none; + margin-bottom: 25px; +} + +.mobile-filter-btn{ + display: flex; + align-items: center; + justify-content: center; + height: 40px; + border: 1px solid #267373; + border-radius: 10px; + font-size: 16px; + line-height: 19px; + color: #267373; + padding: 0 5px 0 13px; + text-decoration: none; +} +.mobile-filter-btn svg{ + margin-right: 8px; +} +.mobile-filter-btn:hover{ + color: #fff; + background: #267373; + text-decoration: none; +} +.mobile-filter-btn:hover svg path{ + fill: #fff; +} + +.sort-select-wrap{ + position: relative; +} +/*.sort-select{ + width: 100%; + height: 40px; + padding-left: 30px; + border: 1px solid #267373; + border-radius: 10px; + background: #fff; + color: #267373; +}*/ + +.sort-select.bootstrap-select > .dropdown-toggle{ + padding-left: 30px; +} +.sort-select.bootstrap-select:not([class*="col-"]):not([class*="form-control"]):not(.input-group-btn){ + width: 251px !important; +} + +.sort-select-wrap .sort-line__ico{ + position: absolute; + z-index: 1; + top: 50%; + left: 10px; + transform: translateY(-50%); +} + +.block-title{ + font-size: 12px; + line-height: 14px; + font-weight: bold; + color: #8E8C8C; + text-transform: uppercase; + padding-left: 20px; + margin-bottom: 10px; +} + +.block-content{ + background: #fff; + border: 1px solid #e6f0ec; + border-radius: 10px; + box-shadow: 0px 4px 15px rgba(0, 37, 21, 0.15); +} + +.staff-block{ + display: flex; + justify-content: space-between; + padding: 20px; + margin-bottom: 10px; +} +.staff-block:last-child{ + margin-bottom: 0; +} +.staff-block__main{ + display: flex; + align-items: center; +} +.staff-block__img{ + display: block; + border-radius: 100%; + width: 100px; + height: 100px; + margin-right: 20px; + flex-shrink: 0; +} +.staff-info{ + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + font-size: 16px; + line-height: 19px; + padding-right: 10px; + width: 280px; +} +.staff-info__name{ + color: #444444; + font-weight: bold; + text-decoration: none; +} +.staff-info__name:hover{ + text-decoration: underline; + color: #444; +} +.staff-info__review{ + color: #8E8C8C; + text-decoration: none; +} +.staff-info__review:hover{ + text-decoration: underline; +} +.staff-info__param--dms-no{ + text-transform: uppercase; + color: #dc3545; +} + +.staff-info__time-list{ + max-width: 300px; + flex-shrink: 0; +} +.time-list{ + font-size: 16px; + line-height: 19px; + display: flex; + flex-direction: column; + justify-content: space-between; +} +.time-list__title{ + color: #444; +} + +.grid-none { + padding: 0px 10px 10px 0px; +} + +.grid-list { + display: grid; + grid-template-columns: 1fr 1fr 1fr; +} + +.intervals-wrap .time{ + display: inline-block; + font-size: 15px; + font-weight: 400; + margin-bottom: 5px; + margin-right: 10px; + padding: 6px 11px; +} +.intervals-wrap .time.available{ + border: 1px solid #267373; + border-radius: 20px; + box-sizing: border-box; + color: #267373; + cursor: pointer; + padding: 5px 10px; + transition: all 0.125s ease-in; +} +.intervals-wrap .time.available:hover{ + background: #267373; + color: #fff; +} +.time-list__intervals{ + display: flex; + flex-wrap: wrap; + align-items: flex-end; + margin: 30px 0; +} + +.btn-show, .btn-show-specialist-detail{ + background: #fff; + display: block; + border: 1px solid #267373; + border-radius: 20px; + box-sizing: border-box; + color: #267373; + cursor: pointer; + text-decoration: none; + padding: 5px 10px; + margin-bottom: 5px; + transition: all 0.125s ease-in; +} +.btn-show:hover, .btn-show-specialist-detail:hover{ + background: #267373; + color: #fff; + text-decoration: none; +} + +.time-list__bottom{ + display: flex; + justify-content: space-between; +} +.time-list__price{ + color: #8E8C8C; +} + +.fa-bookmark, .fa-bookmark-o { + color: #267373; +} + +.calendar-btn{ + color: #267373; + background: url('../img/calendar.svg') no-repeat left center; + padding-left: 23px; +} +.repeat-btn{ + color: #267373; + background: url('../img/repeat-visit.svg') no-repeat left center; + padding-left: 23px; +} + +.filter{ + background: #DCF3F3; + padding: 18px; + border-radius: 10px; +} +.filter__close{ + display: none; +} +.filter__title{ + display: flex; + align-items: center; + justify-content: center; + height: 30px; + margin-bottom: 10px; + color: #267373; + font-size: 12px; + line-height: 14px; + font-weight: bold; + text-align: center; + text-transform: uppercase; +} +.d-none{ + display: none; +} +.filter__input-wrap{ + margin-bottom: 10px; +} +.filter__input{ + width: 100% !important; + height: 40px; + padding: 0 20px; + background: none; + border: 1px solid #267373; + color: #267373; + border-radius: 10px; +} +.filter__input--date{ + background: url('../img/calendar-input.svg') no-repeat calc(100% - 17px) center; +} +.filter__select.show{ + background: #fff; + border-bottom: none; + border-radius: 10px 10px 0 0; +} +.dropdown-menu{ + width: 100%; +} +.dropdown-menu.show{ + width: 100%; + top: -1px !important; + margin-top: 0; + border: 1px solid #267373; + border-top: 1px solid transparent; + border-radius: 0 0 10px 10px; +} + +.filter__btn{ + cursor: pointer; + display: block; + margin: 0 auto; + border: 1px solid #267373; + border-radius: 20px; + box-sizing: border-box; + color: #267373; + padding: 10px 20px; + font-size: 16px; + line-height: 19px; + background: none; + transition: all 0.125s ease-in; +} +.filter__btn:hover{ + background: #267373; + color: #fff; +} +.filter__checkbox{ + display: none; +} +.filter__checkbox-label{ + position: relative; + padding-left: 25px; + background: url('../img/checkbox.svg') no-repeat left center; + color: #267373; +} + +.filter__checkbox:checked ~ .filter__checkbox-label{ + background: url('../img/checkbox_active.svg') no-repeat left center; +} + +.staff-detail{ + padding: 34px 30px; + color: #444444; + font-size: 16px; + line-height: 19px; + margin-bottom: 10px; +} +.staff-detail h1, .staff-detail h2, .staff-detail h3{ + margin-top: 0; +} +.staff-reviews{ + display: flex; + justify-content: space-between; + flex-wrap: wrap; + +} + +.owl-item { + background: #fff; + border: 1px solid #e6f0ec; + border-radius: 10px; + box-shadow: 0px 4px 15px rgb(0 37 21 / 15%); + padding: 20px 15px; +} + +.card-item{ + margin-bottom: 20px; +} +.card-item__block{ + padding: 20px; + display: flex; + justify-content: space-between; + align-items: flex-end; + font-size: 16px; + line-height: 19px; +} +.card-item__title{ + color: #444444; + font-weight: bold; + min-height: 40px; + margin-bottom: 10px; +} +.card-item__btns{ + display: flex; +} +.card-item__btn:not(:last-child){ + margin-right: 10px; +} +.pagination{ + display: flex; + justify-content: center; +} +.pagination__item{ + display: flex; + align-items: center; + justify-content: center; + text-decoration: none; + color: #267373; + width: 35px; + height: 35px; +} +.pagination__item:not(.pagination__item--active):hover{ + text-decoration: underline; +} +.pagination__item--active{ + background: #E6F0EC; + border: 1px solid #E6F0EC; + box-sizing: border-box; + border-radius: 100%; +} +.mobile-tabs{ + display: none; + height: 40px; + padding: 0 20px; + border: 1px solid #267373; + border-radius: 10px; + color: #267373; + background: #E6F0EC; + margin-bottom: 20px; +} +.tabs{ + display: flex; + margin: 0 0 25px 20px; +} +.tab-item{ + display: block; + font-size: 16px; + line-height: 19px; + color: #267373; + box-sizing: border-box; + border-radius: 10px; + padding: 10.5px 20px; + text-decoration: none; + cursor: pointer; +} +.tab-item:not(.tab-item--active):hover, +.tab-item--active:hover { + text-decoration: none; + color: #267373 !important; +} +.tab-item--active { + font-weight: bold; + background: #DCF3F3; + color: #267373; +} + +.visit{ + font-size: 16px; + line-height: 19px; + width: 340px; +} + +.visit__info{ + display: flex; + justify-content: space-between; + margin-bottom: 50px; +} +.visit__time{ + font-weight: bold; + color: #267373; +} +.visit__cancel{ + color: #8E8C8C; + text-decoration: none; +} +.visit__cancel:hover{ + text-decoration: underline; +} +.visit__place{ + color: #444444; + margin-bottom: 15px; +} +.visit__btns{ + display: flex; + float: right; +} +.visit__btns .calendar-btn, +.visit__btns .repeat-btn{ + margin-right: 30px; +} + +.finance{ + display: grid; + grid-template-columns: 1fr 1fr; + padding: 20px; + min-height: 180px; + font-size: 16px; + line-height: 19px; + margin-bottom: 12px; +} +.finance > *{ + margin-bottom: 10px; +} +.finance > *:nth-child(5), +.finance > *:nth-child(6){ + margin-bottom: 0; +} +/*.finance__col{ + width: 50%; + display: flex; + flex-wrap: wrap; + flex-direction: column; + justify-content: space-between; +} +.finance__row{ + width: 100%; + display: flex; + justify-content: space-between; + margin-bottom: 20px; +} +.finance__row:last-child{ + margin-bottom: 0; +} +.finance__row > *{ + display: flex; + align-items: center; + width: 50%; +}*/ +.finance__date{ + color: #267373; + font-weight: bold; + align-items: flex-start; +} +.finance__price { + color: #267373; + font-weight: bold; + padding-left: 20px; + background: url("../img/check.svg") no-repeat left 3px; +} +.finance__filial{ + color: #444444; + align-items: flex-end; +} +.finance__name{ + color: #444444; +} +.finance__staff{ + color: #444444; +} +.finance__btn{ + padding: 0 15px; + height: 30px; +} + +.help-btns{ + margin-bottom: 20px; +} +.help-btn{ + display: inline-flex; + justify-content: center; + align-items: center; + height: 40px; + font-size: 16px; + line-height: 19px; + color: #267373; + padding: 10.5px 20px; + margin-right: 20px; + border: 1px solid #267373; + border-radius: 20px; + text-decoration: none; + transition: all 0.125s ease-in; +} +.help-btn:last-child{ + margin-right: 0; +} +.help-btn:hover{ + background: #267373; + color: #fff; +} + +.collapses{ + font-size: 16px; + line-height: 19px; + padding: 0 20px; + margin-bottom: 10px; +} +.collapses__link{ + position: relative; + display: block; + height: 50px; + padding: 15px 0; + color: #444; + font-weight: bold; + text-decoration: none; +} +.collapses__link:hover{ + color: #444; +} +.collapses__link:before{ + width: 10px; + height: 5px; + content: ""; + position: absolute; + right: 0; + top: 50%; + transform: translateY(-50%); + background: url('../img/arrow-down.svg') no-repeat right center; + background-size: cover; +} +.collapses__link:not(.collapsed):before{ + transform: translateY(-50%) rotate(180deg); +} + +.collapse-item{ + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + padding: 5px 0; +} +.collapse-item--text{ + font-weight: 300; +} +.collapse-item__name{ + color: #444; +} +.collapse-item__btn{ + padding: 5.5px 15px; + color: #267373; + border: 1px solid #8EB7A5; + border-radius: 15px; + background: #fff; +} +.collapse-item__btn:hover{ + color: #fff; + background: #8EB7A5; +} +.collapse{ + padding-bottom: 25px; +} +.collapse:not(.show) { + display: none; +} + +.info{ + padding: 20px; + font-size: 16px; + line-height: 19px; +} +.info__link{ + display: block; + color: #444444; + text-decoration: none; + margin-bottom: 10px; + min-height: 20px; +} +.info__link:hover{ + text-decoration: underline; +} +.info__link:last-child{ + margin-bottom: 0; +} + +.up-arrow{ + display: none; + width: 55px; + height: 55px; + background: url('../img/up-arrow.svg') no-repeat center center; + background-size: cover; + cursor: pointer; + position: fixed; + right: 20px; + bottom: 20px; +} +.mobile-copyright{ + display: none; +} + +.btn-light, +.btn-light:hover{ + border-color: transparent; +} +.btn-light.focus, .btn-light:focus{ + box-shadow: none; + border-color: transparent; +} +.bootstrap-select > .dropdown-toggle{ + height: 40px; + background: none; + border: 1px solid #267373; + border-radius: 10px; + background: none; + color: #267373; + padding: 0 20px; +} + +.bootstrap-select .dropdown-toggle .filter-option{ + padding-top: 6px; +} + +.bootstrap-select > .dropdown-toggle, +.bootstrap-select > .dropdown-toggle:active, +.bootstrap-select > .dropdown-toggle:focus, +.bootstrap-select > .dropdown-toggle:hover { + color: #267373; +} + +.bootstrap-select > .dropdown-toggle.bs-placeholder, +.bootstrap-select > .dropdown-toggle.bs-placeholder:active, +.bootstrap-select > .dropdown-toggle.bs-placeholder:focus, +.bootstrap-select > .dropdown-toggle.bs-placeholder:hover { + color: #267373; +} + +.btn-light:not(:disabled):not(.disabled).active, +.btn-light:not(:disabled):not(.disabled):active, +.show > .btn-light.dropdown-toggle{ + border-bottom: 1px solid transparent; + border-radius: 10px 10px 0 0; +} +.btn-light:not(:disabled):not(.disabled).active:focus, +.btn-light:not(:disabled):not(.disabled):active:focus, +.show > .btn-light.dropdown-toggle:focus{ + box-shadow: none; + background: #fff; +} + +.bootstrap-select > .dropdown-toggle:active, +.bootstrap-select > .dropdown-toggle:focus, +.bootstrap-select > .dropdown-toggle:hover{ + background: none; + outline: none !important; +} + +.btn-light:not(:disabled):not(.disabled).active, +.btn-light:not(:disabled):not(.disabled):active, +.show > .btn-light.dropdown-toggle{ + background: #fff; + border-color: #267373; + border-bottom: none; +} + +.bootstrap-select > .dropdown-toggle.bs-placeholder:active, +.bootstrap-select > .dropdown-toggle.bs-placeholder:focus, +.bootstrap-select > .dropdown-toggle.bs-placeholder:hover{ + background: none; + outline: none !important; +} +.dropdown-toggle:after{ + border: none; + width: 10px; + height: 7px; + background: url("../img/arrow-down.svg") no-repeat center center; + background-size: contain; +} + +.bootstrap-select:not([class*="col-"]):not([class*="form-control"]):not(.input-group-btn) { + width: 261px; +} + +.bootstrap-select.show-tick .dropdown-menu .selected span.check-mark{ + left: 10px; +} +.form-control:focus{ + box-shadow: none; + border-color: #267373; +} + +.ny_top { + background: url(../img/ny_top.png) repeat-x; + width: 100%; + height: 45px; +} + +/*@media screen and (max-width: 1024px) and (min-width: 561px) { + .three-staff{ + margin-right: 10px; + } + .button{ + padding: 0 10px; + white-space: nowrap; + } + .left-sidebar{ + padding: 30px 20px; + } + .login-menu{ + padding-left: 5px; + } + .main-block{ + width: 100%; + min-height: auto; + } + .filter__input--date{ + background: none; + } +}*/ + +@media screen and (max-width: 1024px) and (min-width: 561px){ + .staff-block-wrap{ + display: flex; + flex-wrap: wrap; + justify-content: space-between; + } + .staff-block{ + width: calc(50% - 5px); + } +} + +@media screen and (max-width: 1440px) and (min-width: 1024px) { + .copyright { + bottom: 10px; + } + + .menu ~ .menu { + margin-top: 15px; + } + + .center-content { + flex: 0 0 58%; + max-width: 58%; + padding: 0 10px; + } + + .right-content { + flex: 0 0 22%; + max-width: 22%; + } + + .left-sidebar { + flex: 0 0 20%; + max-width: 20%; + padding: 30px 25px; + } + + .staff-info { + width: 247px; + } + + .staff-block { + padding: 15px 10px; + margin-bottom: 10px; + } + + .staff-block__img { + margin-right: 10px; + } + + .bootstrap-select:not([class*=col-]):not([class*=form-control]):not(.input-group-btn) { + width: 100%; + } +} + +@media screen and (max-width: 1024px){ + .btn-pay { + margin-bottom: 1em; + } + .up-arrow{ + display: block; + } + .main-container{ + margin-top: 0; + margin-bottom: 0; + flex-direction: column; + } + .login-page.main-container{ + height: 100vh; + margin-bottom: 0; + } + .login-page .center-content{ + align-items: flex-start; + } + + .left-sidebar, + .center-content, + .right-sidebar{ + width: 100%; + flex: 0 0 100%; + max-width: 100%; + margin-top: 13px; // отступ для ny + } + .center-content{ + min-height: calc(100vh - 142px); + } + .right-sidebar__top{ + display: none; + } + .filter{ + display: none; + } + .filter.active{ + display: block; + height: 100%; + position: fixed; + z-index: 3; + top: 0; + right: 0; + border-radius: 10px 0 0 10px; + } + .filter__close{ + display: block; + position: absolute; + right: 20px; + top: 5px; + cursor: pointer; + } + .filter__close svg{ + width: 25px; + height: 25px; + } + + .sort-select.bootstrap-select:not([class*=col-]):not([class*=form-control]):not(.input-group-btn) { + width: 200px !important; + } + + .left-sidebar{ + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: 20px; + border-radius: 0; + height: 80px; + z-index: 2; + } + .left-sidebar .city{ + display: block; + margin-top: 10px; + } + .left-sidebar.menu-active{ + position: fixed; + top: 0; + left: 0; + justify-content: flex-end; + } + .login-menu{ + width: 250px; + } + .burger{ + position: relative; + width: 30px; + height: 30px; + cursor: pointer; + margin-top: 10px; + } + .burger.active{ + margin-top: 15px; + } + .burger span{ + display: block; + width: 100%; + height: 2px; + background: #fff; + margin-bottom: 9px; + // transition: all .25s ease-in; + } + .burger span:last-child{ + margin-bottom: 0; + } + + .burger.active span:nth-child(1){ + position: absolute; + top: 5px; + transform: rotate(45deg); + } + .burger.active span:nth-child(2){ + opacity: 0; + } + .burger.active span:nth-child(3){ + position: absolute; + top: 5px; + transform: rotate(-45deg); + } + .mobile-menu{ + display: none; + width: 100%; + height: calc(100vh - 66px); + position: absolute; + left: 0; + top: 66px; + background: #267373; + } + .burger.active ~ .mobile-menu{ + display: flex; + flex-wrap: wrap; + flex-direction: column; + justify-content: flex-start; + align-items: center; + } + .login-menu{ + margin-bottom: 0; + } + .login-menu.active{ + position: relative; + z-index: 1; + } + .menu-active .login-menu, + .menu-active .logo-link{ + display: none; + } + .menu{ + max-width: 240px; + } + .mobile-menu .city{ + display: block; + } + .copyright{ + display: none; + } + .logo{ + display: none; + margin-bottom: 0; + } + .login-page .logo{ + display: block; + } + .login-page .right-sidebar{ + display: none; + } + .mobile-logo{ + display: block; + } + + .top-section{ + position: relative; + flex-direction: column-reverse; + align-items: flex-start; + padding-top: 10px; + } + .search-wrap{ + margin-bottom: 20px; + padding-left: 0; + } + .title{ + padding: 0; + } + .block-title{ + padding: 0; + } + .main-block{ + width: 100%; + min-height: auto; + } + .sort-line{ + display: none; + } + .mobile-actions{ + display: flex; + justify-content: space-between; + } + + .staff-block{ + flex-wrap: wrap; + } + + .staff-block__main{ + width: 100%; + margin-bottom: 25px; + } + + .time-list{ + width: 100%; + } + .time-list__intervals{ + margin: 15px 0; + } + + .staff-review{ + width: 100%; + margin-bottom: 10px; + } + + .tabs{ + display: none; + } + .mobile-tabs{ + display: block; + } + + .card-item__block{ + flex-wrap: wrap; + } + .card-item__info{ + width: 100%; + margin-bottom: 20px; + } + .card-item__title{ + min-height: auto; + } + + .visit__info{ + margin-bottom: 10px; + } + .visit{ + width: 100%; + } + + .mobile-copyright{ + display: flex; + width: 100%; + height: 60px; + justify-content: center; + align-items: center; + font-size: 12px; + line-height: 14px; + text-transform: uppercase; + color: #8EB7A5; + } + + .finance{ + grid-template-columns: 1fr; + } + .finance > *:nth-child(3){ + margin-bottom: 30px; + } + .finance > *:nth-child(5){ + margin-bottom: 10px; + } + + .top-section .sova-bonus-line{ + display: flex; + position: absolute; + right: 0; + bottom: 3px; + margin-bottom: 0; + padding: 0; + height: auto; + background: none; + color: #267373; + } + .top-section .sova-bonus-line__text{ + font-size: 16px; + line-height: 19px; + text-transform: none; + } + + .collapse{ + padding-bottom: 0; + } + .collapse-item__name{ + margin-bottom: 10px; + } + + .help-btns{ + display: flex; + flex-wrap: wrap; + justify-content: center; + } + .help-btn{ + margin-right: 0; + margin-bottom: 20px; + } + .help-btn:last-child{ + margin-bottom: 0; + } + .ny_top { + position: absolute; + z-index: 3; + top: 0; + left: 0; + } +} \ No newline at end of file diff --git a/assets/window.js b/assets/window.js new file mode 100644 index 0000000..f999f3a --- /dev/null +++ b/assets/window.js @@ -0,0 +1,68 @@ +window.validateEmail = function(email) { + var re = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/; + return re.test(String(email).toLowerCase()); +}; + +window.validatePhone = function(phone) { + var re = /^[\+\d]{0,2}[\d\(\)\s\-]{10,16}\d$/; + return re.test(String(phone).toLowerCase()); +}; + +window.dateFormat = function(date, format = 'Ymd') { + if (format == 'Ymd') { + return date.getFullYear() + + ('0' + (date.getMonth()+1)).slice(-2) + + ('0' + date.getDate()).slice(-2); + } else if (format == 'd-m-Y') { + return ('0' + date.getDate()).slice(-2) + + '-' + ('0' + (date.getMonth()+1)).slice(-2) + + '-' + date.getFullYear(); + } else if (format == 'Y-m-d') { + return date.getFullYear() + + '-' + ('0' + (date.getMonth()+1)).slice(-2) + + '-' + ('0' + date.getDate()).slice(-2); + } else if (format == 'd.m.Y') { + return ('0' + date.getDate()).slice(-2) + + '.' + ('0' + (date.getMonth()+1)).slice(-2) + + '.' + date.getFullYear(); + } +}; + +window.getWeekDay = function(date) { + let days = [ + 'Воскресенье', + 'Понедельник', + 'Вторник', + 'Среда', + 'Четверг', + 'Пятница', + 'Суббота' + ]; + + return days[date.getDay()]; +} + +window.newDate = function(date) { + const regex = /([0-9]{4})([0-9]{2})([0-9]{2})/gm; + let m; + + while ((m = regex.exec(date)) !== null) { + if (m.index === regex.lastIndex) { + regex.lastIndex++; + } + + var dateArray = {'year': null, 'month': null, 'day': null}; + + m.forEach((match, groupIndex) => { + if (groupIndex == 1) { + dateArray.year = match; + } else if (groupIndex == 2) { + dateArray.month = match; + } else if (groupIndex == 3) { + dateArray.day = match; + } + }); + + return new Date(dateArray.year + '-' + dateArray.month + '-' + dateArray.day); + } +}; diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..8fe9d49 --- /dev/null +++ b/bin/console @@ -0,0 +1,43 @@ +#!/usr/bin/env php +getParameterOption(['--env', '-e'], null, true)) { + putenv('APP_ENV='.$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = $env); +} + +if ($input->hasParameterOption('--no-debug', true)) { + putenv('APP_DEBUG='.$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0'); +} + +(new Dotenv())->bootEnv(dirname(__DIR__).'/.env'); + +if ($_SERVER['APP_DEBUG']) { + umask(0000); + + if (class_exists(Debug::class)) { + Debug::enable(); + } +} + +$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); +$application = new Application($kernel); +$application->run($input); diff --git a/bin/phpunit b/bin/phpunit new file mode 100755 index 0000000..4d1ed05 --- /dev/null +++ b/bin/phpunit @@ -0,0 +1,13 @@ +#!/usr/bin/env php +=7.2.5", + "ext-ctype": "*", + "ext-iconv": "*", + "beberlei/doctrineextensions": "^1.3", + "composer/package-versions-deprecated": "1.11.99.1", + "doctrine/annotations": "^1.0", + "doctrine/doctrine-bundle": "^2.3", + "doctrine/doctrine-migrations-bundle": "^3.1", + "doctrine/orm": "^2.8", + "friendsofsymfony/ckeditor-bundle": "^2.6", + "guzzlehttp/guzzle": "^7.0", + "knplabs/knp-paginator-bundle": "^5.5", + "mpdf/mpdf": "^8.2", + "nelmio/cors-bundle": "*", + "phpdocumentor/reflection-docblock": "^5.2", + "predis/predis": "*", + "sensio/framework-extra-bundle": "^5.1", + "symfony/asset": "5.4.*", + "symfony/cache": "^5.4", + "symfony/console": "5.4.*", + "symfony/dotenv": "5.4.*", + "symfony/expression-language": "5.4.*", + "symfony/flex": "^1.3.1", + "symfony/form": "5.4.*", + "symfony/framework-bundle": "5.4.*", + "symfony/http-client": "^5.4", + "symfony/intl": "5.4.*", + "symfony/mailer": "5.4.*", + "symfony/mime": "5.4.*", + "symfony/monolog-bundle": "^3.1", + "symfony/notifier": "5.4.*", + "symfony/process": "5.4.*", + "symfony/property-access": "5.4.*", + "symfony/property-info": "5.4.*", + "symfony/proxy-manager-bridge": "5.4.*", + "symfony/security-bundle": "5.4.*", + "symfony/serializer": "5.4.*", + "symfony/string": "5.4.*", + "symfony/translation": "5.4.*", + "symfony/twig-bundle": "^5.4", + "symfony/validator": "5.4.*", + "symfony/web-link": "5.4.*", + "symfony/webpack-encore-bundle": "^1.11", + "symfony/yaml": "5.4.*", + "twig/extra-bundle": "^2.12|^3.0", + "twig/twig": "^2.12|^3.0", + "zircote/swagger-php": "^4.9" + }, + "require-dev": { + "symfony/browser-kit": "^5.4", + "symfony/css-selector": "^5.4", + "symfony/debug-bundle": "^5.4", + "symfony/maker-bundle": "^1.0", + "symfony/phpunit-bridge": "^5.4", + "symfony/stopwatch": "^5.4", + "symfony/var-dumper": "^5.4", + "symfony/web-profiler-bundle": "^5.4" + }, + "config": { + "optimize-autoloader": true, + "preferred-install": { + "*": "dist" + }, + "sort-packages": true, + "allow-plugins": { + "symfony/flex": true + } + }, + "autoload": { + "psr-4": { + "App\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "App\\Tests\\": "tests/" + } + }, + "replace": { + "symfony/polyfill-ctype": "*", + "symfony/polyfill-iconv": "*", + "symfony/polyfill-php72": "*", + "symfony/polyfill-php73": "*", + "symfony/polyfill-php74": "*", + "symfony/polyfill-php80": "*", + "symfony/polyfill-php81": "*", + "symfony/polyfill-php82": "*" + }, + "scripts": { + "auto-scripts": { + "cache:clear": "symfony-cmd", + "assets:install %PUBLIC_DIR%": "symfony-cmd" + }, + "post-install-cmd": [ + "@auto-scripts" + ], + "post-update-cmd": [ + "@auto-scripts" + ] + }, + "conflict": { + "symfony/symfony": "*" + }, + "extra": { + "symfony": { + "allow-contrib": false, + "require": "^5.4" + } + } +} diff --git a/config/bundles.php b/config/bundles.php new file mode 100644 index 0000000..2e60e90 --- /dev/null +++ b/config/bundles.php @@ -0,0 +1,19 @@ + ['all' => true], + Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true], + Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], + Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], + Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], + Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true], + Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], + Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], + Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], + Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], + Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], + Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], + Knp\Bundle\PaginatorBundle\KnpPaginatorBundle::class => ['all' => true], + FOS\CKEditorBundle\FOSCKEditorBundle::class => ['all' => true], + Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true], +]; diff --git a/config/packages/assets.yaml b/config/packages/assets.yaml new file mode 100644 index 0000000..051d36d --- /dev/null +++ b/config/packages/assets.yaml @@ -0,0 +1,3 @@ +framework: + assets: + json_manifest_path: '%kernel.project_dir%/public/build/manifest.json' diff --git a/config/packages/cache.yaml b/config/packages/cache.yaml new file mode 100644 index 0000000..0a30fb3 --- /dev/null +++ b/config/packages/cache.yaml @@ -0,0 +1,10 @@ +framework: + cache: + default_redis_provider: '%env(resolve:REDIS_URL)%' # or 'redis://localhost' + + # app: cache.adapter.redis + # system: cache.adapter.redis + + pools: + db_redis_cache_pool: + adapter: cache.adapter.redis \ No newline at end of file diff --git a/config/packages/dev/debug.yaml b/config/packages/dev/debug.yaml new file mode 100644 index 0000000..26d4e53 --- /dev/null +++ b/config/packages/dev/debug.yaml @@ -0,0 +1,4 @@ +debug: + # Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser. + # See the "server:dump" command to start a new server. + dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%" diff --git a/config/packages/dev/monolog.yaml b/config/packages/dev/monolog.yaml new file mode 100644 index 0000000..b1998da --- /dev/null +++ b/config/packages/dev/monolog.yaml @@ -0,0 +1,19 @@ +monolog: + handlers: + main: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: debug + channels: ["!event"] + # uncomment to get logging in your browser + # you may have to allow bigger header sizes in your Web server configuration + #firephp: + # type: firephp + # level: info + #chromephp: + # type: chromephp + # level: info + console: + type: console + process_psr_3_messages: false + channels: ["!event", "!doctrine", "!console"] diff --git a/config/packages/dev/web_profiler.yaml b/config/packages/dev/web_profiler.yaml new file mode 100644 index 0000000..e92166a --- /dev/null +++ b/config/packages/dev/web_profiler.yaml @@ -0,0 +1,6 @@ +web_profiler: + toolbar: true + intercept_redirects: false + +framework: + profiler: { only_exceptions: false } diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml new file mode 100644 index 0000000..1002df1 --- /dev/null +++ b/config/packages/doctrine.yaml @@ -0,0 +1,26 @@ +doctrine: + dbal: + default_connection: default + connections: + default: + schema_filter: ~^(?!.*_view$)~ + url: '%env(resolve:DATABASE_URL)%' + # IMPORTANT: You MUST configure your server version, + # either here or in the DATABASE_URL env var (see .env file) + #server_version: '13' + bitrix: + url: '%env(resolve:DATABASE_BITRIX_URL)%' + orm: + result_cache_driver: + type: pool + pool: db_redis_cache_pool + auto_generate_proxy_classes: true + naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware + auto_mapping: true + mappings: + App: + is_bundle: false + type: annotation + dir: '%kernel.project_dir%/src/Entity' + prefix: 'App\Entity' + alias: App diff --git a/config/packages/doctrine_migrations.yaml b/config/packages/doctrine_migrations.yaml new file mode 100644 index 0000000..a0a17a0 --- /dev/null +++ b/config/packages/doctrine_migrations.yaml @@ -0,0 +1,6 @@ +doctrine_migrations: + migrations_paths: + # namespace is arbitrary but should be different from App\Migrations + # as migrations classes should NOT be autoloaded + 'DoctrineMigrations': '%kernel.project_dir%/migrations' + enable_profiler: '%kernel.debug%' diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml new file mode 100644 index 0000000..6fe7ffa --- /dev/null +++ b/config/packages/framework.yaml @@ -0,0 +1,25 @@ +# see https://symfony.com/doc/current/reference/configuration/framework.html + +framework: + secret: '%env(APP_SECRET)%' + trusted_proxies: '%env(TRUSTED_PROXIES)%' + trusted_headers: ['x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'x-forwarded-port', 'x-forwarded-prefix'] + #csrf_protection: true + #http_method_override: true + http_cache: true + http_client: + default_options: + http_version: '2.0' + + # Enables session support. Note that the session will ONLY be started if you read or write from it. + # Remove or comment this section to explicitly disable session support. + session: + enabled: true + handler_id: '%env(resolve:REDIS_URL)%' + cookie_secure: auto + cookie_samesite: lax + + #esi: true + #fragments: true + php_errors: + log: true diff --git a/config/packages/knp_paginator.yaml b/config/packages/knp_paginator.yaml new file mode 100644 index 0000000..e59e707 --- /dev/null +++ b/config/packages/knp_paginator.yaml @@ -0,0 +1,5 @@ +knp_paginator: + page_range: 3 + template: + pagination: 'base/paginator.html.twig' + \ No newline at end of file diff --git a/config/packages/mailer.yaml b/config/packages/mailer.yaml new file mode 100644 index 0000000..56a650d --- /dev/null +++ b/config/packages/mailer.yaml @@ -0,0 +1,3 @@ +framework: + mailer: + dsn: '%env(MAILER_DSN)%' diff --git a/config/packages/nelmio_cors.yaml b/config/packages/nelmio_cors.yaml new file mode 100644 index 0000000..db0a72b --- /dev/null +++ b/config/packages/nelmio_cors.yaml @@ -0,0 +1,17 @@ +nelmio_cors: + defaults: + origin_regex: true + allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] + allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE'] + allow_headers: ['Content-Type', 'Authorization', 'X-Requested-With'] + expose_headers: ['Link'] + max_age: 3600 + allow_credentials: true + paths: + '^/api/': + origin_regex: true + allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] + allow_headers: ['Content-Type', 'Authorization', 'X-Requested-With'] + allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE'] + max_age: 3600 + allow_credentials: true \ No newline at end of file diff --git a/config/packages/notifier.yaml b/config/packages/notifier.yaml new file mode 100644 index 0000000..3984a48 --- /dev/null +++ b/config/packages/notifier.yaml @@ -0,0 +1,16 @@ +framework: + notifier: + #chatter_transports: + # slack: '%env(SLACK_DSN)%' + # telegram: '%env(TELEGRAM_DSN)%' + #texter_transports: + # twilio: '%env(TWILIO_DSN)%' + # nexmo: '%env(NEXMO_DSN)%' + channel_policy: + # use chat/slack, chat/telegram, sms/twilio or sms/nexmo + urgent: ['email'] + high: ['email'] + medium: ['email'] + low: ['email'] + admin_recipients: + - { email: admin@example.com } diff --git a/config/packages/prod/deprecations.yaml b/config/packages/prod/deprecations.yaml new file mode 100644 index 0000000..60026a1 --- /dev/null +++ b/config/packages/prod/deprecations.yaml @@ -0,0 +1,8 @@ +# As of Symfony 5.1, deprecations are logged in the dedicated "deprecation" channel when it exists +#monolog: +# channels: [deprecation] +# handlers: +# deprecation: +# type: stream +# channels: [deprecation] +# path: php://stderr diff --git a/config/packages/prod/doctrine.yaml b/config/packages/prod/doctrine.yaml new file mode 100644 index 0000000..084f59a --- /dev/null +++ b/config/packages/prod/doctrine.yaml @@ -0,0 +1,20 @@ +doctrine: + orm: + auto_generate_proxy_classes: false + metadata_cache_driver: + type: pool + pool: doctrine.system_cache_pool + query_cache_driver: + type: pool + pool: doctrine.system_cache_pool + result_cache_driver: + type: pool + pool: doctrine.result_cache_pool + +framework: + cache: + pools: + doctrine.result_cache_pool: + adapter: cache.app + doctrine.system_cache_pool: + adapter: cache.system diff --git a/config/packages/prod/monolog.yaml b/config/packages/prod/monolog.yaml new file mode 100644 index 0000000..6ef73fe --- /dev/null +++ b/config/packages/prod/monolog.yaml @@ -0,0 +1,18 @@ +monolog: + handlers: + main: + type: fingers_crossed + action_level: error + handler: nested + excluded_http_codes: [404, 405, 500] + buffer_size: 50 # How many messages should be saved? Prevent memory leaks + channels: ["!event", "!doctrine"] + nested: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: debug + formatter: monolog.formatter.json + console: + type: console + process_psr_3_messages: false + channels: ["!event", "!doctrine"] diff --git a/config/packages/prod/routing.yaml b/config/packages/prod/routing.yaml new file mode 100644 index 0000000..b3e6a0a --- /dev/null +++ b/config/packages/prod/routing.yaml @@ -0,0 +1,3 @@ +framework: + router: + strict_requirements: null diff --git a/config/packages/prod/webpack_encore.yaml b/config/packages/prod/webpack_encore.yaml new file mode 100644 index 0000000..d0b3ba8 --- /dev/null +++ b/config/packages/prod/webpack_encore.yaml @@ -0,0 +1,4 @@ +#webpack_encore: + # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes) + # Available in version 1.2 + #cache: true diff --git a/config/packages/routing.yaml b/config/packages/routing.yaml new file mode 100644 index 0000000..b45c1ce --- /dev/null +++ b/config/packages/routing.yaml @@ -0,0 +1,7 @@ +framework: + router: + utf8: true + + # Configure how to generate URLs in non-HTTP contexts, such as CLI commands. + # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands + #default_uri: http://localhost diff --git a/config/packages/security.yaml b/config/packages/security.yaml new file mode 100644 index 0000000..c545a23 --- /dev/null +++ b/config/packages/security.yaml @@ -0,0 +1,43 @@ +security: + enable_authenticator_manager: true + encoders: + App\Entity\User: + algorithm: bcrypt + cost: 12 + + # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers + providers: + # used to reload user from session & other features (e.g. switch_user) + app_user_provider: + entity: + class: App\Entity\User + property: email + firewalls: + dev: + pattern: ^/(_(profiler|wdt)|css|images|js)/ + security: false + main: + anonymous: false + lazy: true + provider: app_user_provider + guard: + authenticators: + - App\Security\LoginFormAuthenticator + logout: + path: security_logout + target: / + delete_cookies: ['CABINET_SESSION', 'WR_SESSION', 'WR_FLASH', 'PLAY_SESSION', 'WR_DETAIL', 'region'] + # where to redirect after logout + # target: app_any_route + + # activate different ways to authenticate + # https://symfony.com/doc/current/security.html#firewalls-authentication + + # https://symfony.com/doc/current/security/impersonating_user.html + # switch_user: true + + # Easy way to control access for large sections of your site + # Note: Only the *first* access control that matches will be used + access_control: + # - { path: ^/admin, roles: ROLE_ADMIN } + # - { path: ^/profile, roles: ROLE_USER } diff --git a/config/packages/sensio_framework_extra.yaml b/config/packages/sensio_framework_extra.yaml new file mode 100644 index 0000000..1821ccc --- /dev/null +++ b/config/packages/sensio_framework_extra.yaml @@ -0,0 +1,3 @@ +sensio_framework_extra: + router: + annotations: false diff --git a/config/packages/test/doctrine.yaml b/config/packages/test/doctrine.yaml new file mode 100644 index 0000000..2ace640 --- /dev/null +++ b/config/packages/test/doctrine.yaml @@ -0,0 +1,4 @@ +doctrine: + dbal: + # "TEST_TOKEN" is typically set by ParaTest + dbname: 'main_test%env(default::TEST_TOKEN)%' diff --git a/config/packages/test/framework.yaml b/config/packages/test/framework.yaml new file mode 100644 index 0000000..d051c84 --- /dev/null +++ b/config/packages/test/framework.yaml @@ -0,0 +1,4 @@ +framework: + test: true + session: + storage_id: session.storage.mock_file diff --git a/config/packages/test/monolog.yaml b/config/packages/test/monolog.yaml new file mode 100644 index 0000000..fc40641 --- /dev/null +++ b/config/packages/test/monolog.yaml @@ -0,0 +1,12 @@ +monolog: + handlers: + main: + type: fingers_crossed + action_level: error + handler: nested + excluded_http_codes: [404, 405] + channels: ["!event"] + nested: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: debug diff --git a/config/packages/test/twig.yaml b/config/packages/test/twig.yaml new file mode 100644 index 0000000..8c6e0b4 --- /dev/null +++ b/config/packages/test/twig.yaml @@ -0,0 +1,2 @@ +twig: + strict_variables: true diff --git a/config/packages/test/validator.yaml b/config/packages/test/validator.yaml new file mode 100644 index 0000000..1e5ab78 --- /dev/null +++ b/config/packages/test/validator.yaml @@ -0,0 +1,3 @@ +framework: + validation: + not_compromised_password: false diff --git a/config/packages/test/web_profiler.yaml b/config/packages/test/web_profiler.yaml new file mode 100644 index 0000000..03752de --- /dev/null +++ b/config/packages/test/web_profiler.yaml @@ -0,0 +1,6 @@ +web_profiler: + toolbar: false + intercept_redirects: false + +framework: + profiler: { collect: false } diff --git a/config/packages/test/webpack_encore.yaml b/config/packages/test/webpack_encore.yaml new file mode 100644 index 0000000..02a7651 --- /dev/null +++ b/config/packages/test/webpack_encore.yaml @@ -0,0 +1,2 @@ +#webpack_encore: +# strict_mode: false diff --git a/config/packages/translation.yaml b/config/packages/translation.yaml new file mode 100644 index 0000000..25291a0 --- /dev/null +++ b/config/packages/translation.yaml @@ -0,0 +1,6 @@ +framework: + default_locale: ru + translator: + default_path: '%kernel.project_dir%/translations' + fallbacks: + - ru diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml new file mode 100644 index 0000000..9170751 --- /dev/null +++ b/config/packages/twig.yaml @@ -0,0 +1,6 @@ +twig: + globals: + tech_maintenance: '%tech_maintenance%' + default_path: '%kernel.project_dir%/templates' + form_themes: + - '@FOSCKEditor/Form/ckeditor_widget.html.twig' diff --git a/config/packages/validator.yaml b/config/packages/validator.yaml new file mode 100644 index 0000000..350786a --- /dev/null +++ b/config/packages/validator.yaml @@ -0,0 +1,8 @@ +framework: + validation: + email_validation_mode: html5 + + # Enables validator auto-mapping support. + # For instance, basic validation constraints will be inferred from Doctrine's metadata. + #auto_mapping: + # App\Entity\: [] diff --git a/config/packages/webpack_encore.yaml b/config/packages/webpack_encore.yaml new file mode 100644 index 0000000..90f1a1d --- /dev/null +++ b/config/packages/webpack_encore.yaml @@ -0,0 +1,30 @@ +webpack_encore: + # The path where Encore is building the assets - i.e. Encore.setOutputPath() + output_path: '%kernel.project_dir%/public/build' + # If multiple builds are defined (as shown below), you can disable the default build: + # output_path: false + + # Set attributes that will be rendered on all script and link tags + script_attributes: + defer: true + # link_attributes: + + # If using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials') + # crossorigin: 'anonymous' + + # Preload all rendered script and link tags automatically via the HTTP/2 Link header + # preload: true + + # Throw an exception if the entrypoints.json file is missing or an entry is missing from the data + # strict_mode: false + + # If you have multiple builds: + # builds: + # pass "frontend" as the 3rg arg to the Twig functions + # {{ encore_entry_script_tags('entry1', null, 'frontend') }} + + # frontend: '%kernel.project_dir%/public/frontend/build' + + # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes) + # Put in config/packages/prod/webpack_encore.yaml + # cache: true diff --git a/config/preload.php b/config/preload.php new file mode 100644 index 0000000..5ebcdb2 --- /dev/null +++ b/config/preload.php @@ -0,0 +1,5 @@ + + + + + Diff to HTML by rtfpessoa + + + + + + + + + + + + + +

    Diff to HTML by rtfpessoa

    + +
    +
    +
    + Files changed (2) + hide + show +
    +
      +
    1. + + assets/components/misSession.js + + +118 + -0 + + +
    2. +
    3. + + assets/controllers/caseHistory_controller.js + + +36 + -21 + + +
    4. +
    +
    +
    +
    + + assets/components/misSession.js + ADDED + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    @@ -0,0 +1,118 @@
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
     
    +
    + 1 + +
    + + + /** +
    +
    + 2 + +
    + + + * MIS (webSDK / widget.sovamed.ru) session helpers. +
    +
    + 3 + +
    + + + * Symfony login (ROLE_USER) and MIS session are independent. +
    +
    + 4 + +
    + + + */ +
    +
    + 5 + +
    + + +
    +
    +
    + 6 + +
    + + + function MisSessionError() { +
    +
    + 7 + +
    + + + this.name = 'MisSessionError'; +
    +
    + 8 + +
    + + + this.message = 'MIS session is not authenticated'; +
    +
    + 9 + +
    + + + } +
    +
    + 10 + +
    + + +
    +
    +
    + 11 + +
    + + + function isAuthenticated(webSDK) { +
    +
    + 12 + +
    + + + return Boolean(webSDK?.data?.user?.authenticated); +
    +
    + 13 + +
    + + + } +
    +
    + 14 + +
    + + +
    +
    +
    + 15 + +
    + + + function ensureAuthenticated(webSDK) { +
    +
    + 16 + +
    + + + return new Promise(function(resolve, reject) { +
    +
    + 17 + +
    + + + if (isAuthenticated(webSDK)) { +
    +
    + 18 + +
    + + + resolve(webSDK); +
    +
    + 19 + +
    + + + return; +
    +
    + 20 + +
    + + + } +
    +
    + 21 + +
    + + +
    +
    +
    + 22 + +
    + + + if (typeof webSDK?.isLoggedIn === 'function') { +
    +
    + 23 + +
    + + + webSDK.isLoggedIn(webSDK.sdkOrigin).then(function(result) { +
    +
    + 24 + +
    + + + if (result?.authenticated) { +
    +
    + 25 + +
    + + + resolve(webSDK); +
    +
    + 26 + +
    + + + return; +
    +
    + 27 + +
    + + + } +
    +
    + 28 + +
    + + + reject(new MisSessionError()); +
    +
    + 29 + +
    + + + }).catch(function() { +
    +
    + 30 + +
    + + + reject(new MisSessionError()); +
    +
    + 31 + +
    + + + }); +
    +
    + 32 + +
    + + + return; +
    +
    + 33 + +
    + + + } +
    +
    + 34 + +
    + + +
    +
    +
    + 35 + +
    + + + reject(new MisSessionError()); +
    +
    + 36 + +
    + + + }); +
    +
    + 37 + +
    + + + } +
    +
    + 38 + +
    + + +
    +
    +
    + 39 + +
    + + + function removeSdkOverlayModals() { +
    +
    + 40 + +
    + + + document.querySelectorAll('.wr-sdk-widget-modal').forEach(function(modal) { +
    +
    + 41 + +
    + + + modal.remove(); +
    +
    + 42 + +
    + + + }); +
    +
    + 43 + +
    + + + } +
    +
    + 44 + +
    + + +
    +
    +
    + 45 + +
    + + + /** +
    +
    + 46 + +
    + + + * Move #iframeProtocol from SDK overlay into cabinet bootstrap popup. +
    +
    + 47 + +
    + + + * openConference() must be called without container (SDK applies Guest URL fix). +
    +
    + 48 + +
    + + + */ +
    +
    + 49 + +
    + + + function mountConferenceInPopup(popup) { +
    +
    + 50 + +
    + + + var iframe = document.getElementById('iframeProtocol'); +
    +
    + 51 + +
    + + + var popupBody = popup?.querySelector('#popup-body'); +
    +
    + 52 + +
    + + +
    +
    +
    + 53 + +
    + + + if (!iframe || !popupBody) { +
    +
    + 54 + +
    + + + return false; +
    +
    + 55 + +
    + + + } +
    +
    + 56 + +
    + + +
    +
    +
    + 57 + +
    + + + removeSdkOverlayModals(); +
    +
    + 58 + +
    + + + popupBody.innerHTML = ''; +
    +
    + 59 + +
    + + + popupBody.appendChild(iframe); +
    +
    + 60 + +
    + + + iframe.style.width = '100%'; +
    +
    + 61 + +
    + + + iframe.style.border = 'none'; +
    +
    + 62 + +
    + + + iframe.style.height = (window.innerHeight - 100) + 'px'; +
    +
    + 63 + +
    + + +
    +
    +
    + 64 + +
    + + + var fullScreenBtn = popup.querySelector('.full-scren-modal'); +
    +
    + 65 + +
    + + + if (fullScreenBtn) { +
    +
    + 66 + +
    + + + fullScreenBtn.classList.remove('d-none'); +
    +
    + 67 + +
    + + + } +
    +
    + 68 + +
    + + +
    +
    +
    + 69 + +
    + + + popup.querySelector('.modal-dialog').classList = 'modal-dialog'; +
    +
    + 70 + +
    + + + popup.querySelector('.modal-content').classList = 'modal-content'; +
    +
    + 71 + +
    + + + popup.querySelector('.modal-title').innerHTML = 'Онлайн консультация'; +
    +
    + 72 + +
    + + +
    +
    +
    + 73 + +
    + + + if (typeof $ !== 'undefined' && typeof $(popup).modal === 'function') { +
    +
    + 74 + +
    + + + $(popup).modal('show'); +
    +
    + 75 + +
    + + + } +
    +
    + 76 + +
    + + +
    +
    +
    + 77 + +
    + + + return true; +
    +
    + 78 + +
    + + + } +
    +
    + 79 + +
    + + +
    +
    +
    + 80 + +
    + + + function showMisSessionExpired(popup) { +
    +
    + 81 + +
    + + + if (!popup) { +
    +
    + 82 + +
    + + + window.location.pathname = '/logout'; +
    +
    + 83 + +
    + + + return; +
    +
    + 84 + +
    + + + } +
    +
    + 85 + +
    + + +
    +
    +
    + 86 + +
    + + + popup.querySelector('.modal-dialog').classList = 'modal-dialog'; +
    +
    + 87 + +
    + + + popup.querySelector('.modal-title').innerHTML = 'Сессия виджета истекла'; +
    +
    + 88 + +
    + + + popup.querySelector('#popup-body').innerHTML = +
    +
    + 89 + +
    + + + '<p class="mb-3">Личный кабинет открыт, но сессия виджета записи (MIS) истекла. ' + +
    +
    + 90 + +
    + + + 'Для оплаты и онлайн-приёма нужно войти снова — повторная авторизация в iframe не требуется.</p>' + +
    +
    + 91 + +
    + + + '<button type="button" class="btn btn-outline-secondary w-100" id="mis-session-relogin">Войти снова</button>'; +
    +
    + 92 + +
    + + +
    +
    +
    + 93 + +
    + + + popup.querySelector('#mis-session-relogin').addEventListener('click', function() { +
    +
    + 94 + +
    + + + window.location.pathname = '/logout'; +
    +
    + 95 + +
    + + + }); +
    +
    + 96 + +
    + + +
    +
    +
    + 97 + +
    + + + if (typeof $ !== 'undefined' && typeof $(popup).modal === 'function') { +
    +
    + 98 + +
    + + + $(popup).modal('show'); +
    +
    + 99 + +
    + + + } +
    +
    + 100 + +
    + + + } +
    +
    + 101 + +
    + + +
    +
    +
    + 102 + +
    + + + function handleMisSessionFailure(popup, error, logContext) { +
    +
    + 103 + +
    + + + if (error instanceof MisSessionError || error?.name === 'MisSessionError') { +
    +
    + 104 + +
    + + + showMisSessionExpired(popup); +
    +
    + 105 + +
    + + + return true; +
    +
    + 106 + +
    + + + } +
    +
    + 107 + +
    + + +
    +
    +
    + 108 + +
    + + + return false; +
    +
    + 109 + +
    + + + } +
    +
    + 110 + +
    + + +
    +
    +
    + 111 + +
    + + + module.exports = { +
    +
    + 112 + +
    + + + MisSessionError: MisSessionError, +
    +
    + 113 + +
    + + + isAuthenticated: isAuthenticated, +
    +
    + 114 + +
    + + + ensureAuthenticated: ensureAuthenticated, +
    +
    + 115 + +
    + + + mountConferenceInPopup: mountConferenceInPopup, +
    +
    + 116 + +
    + + + showMisSessionExpired: showMisSessionExpired, +
    +
    + 117 + +
    + + + handleMisSessionFailure: handleMisSessionFailure, +
    +
    + 118 + +
    + + + }; +
    +
    +
    +
    +
    +
    +
    +
    + + assets/controllers/caseHistory_controller.js + CHANGED + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    @@ -2,6 +2,7 @@ import { Controller } from 'stimulus';
    +
    + 2 + +
    +   + import Cookies from 'js-cookie'; +
    +
    + 3 + +
    +   + const loader = require("./../components/loader.js"); +
    +
    + 4 + +
    +   + const helper = require("./../components/helper.js"); +
    +
    + + +
    +   +
    +
    +
    + 5 + +
    +   +
    +
    +
    + 6 + +
    +   + /* +
    +
    + 7 + +
    +   + * This is an example Stimulus controller! +
    +
    +
    @@ -248,20 +249,23 @@ export default class extends Controller {
    +
    + 248 + +
    +   + btnConfirence.setAttribute('data-filial' , data.filial) +
    +
    + 249 + +
    +   + btnConfirence.addEventListener('click', function () { +
    +
    + 250 + +
    +   + popup.querySelector('#popup-body').innerHTML = ''; +
    +
    + 251 + +
    + - + +
    +
    + 252 + +
    + - + webSDK.openConference({ +
    +
    + 253 + +
    + - + schedid: btnConfirence.dataset.id, +
    +
    + 254 + +
    + - + container: popup.querySelector('#popup-body') +
    +
    + 255 + +
    + - + }).then(function () { +
    +
    + 256 + +
    + - + popup.querySelector('.full-scren-modal').classList.remove('d-none'); +
    +
    + 257 + +
    + - + document.getElementById('iframeProtocol').style.height = (window.innerHeight - 100) + 'px'; +
    +
    + 258 + +
    + - + popup.querySelector('.modal-dialog').classList = 'modal-dialog'; +
    +
    + 259 + +
    + - + popup.querySelector('.modal-content').classList = 'modal-content'; +
    +
    + 260 + +
    + - + popup.querySelector('.modal-content').classList = 'modal-content'; +
    +
    + 261 + +
    + - + popup.querySelector('.modal-title').innerHTML = 'Онлайн консультация'; +
    +
    + 262 + +
    +   +
    +
    +
    + 263 + +
    + - + $(popup).modal('show'); +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + 264 + +
    +   + }).catch(function (e) { +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + 265 + +
    +   + if (typeof e.data?.message !== 'undefined') { +
    +
    + 266 + +
    +   + var msg = e.data.message.replace('UTC+3', 'UTC+3 (московское время)'); +
    +
    + 267 + +
    +   + popup.querySelector('#popup-body').innerHTML = msg; +
    +
    +
    @@ -272,9 +276,9 @@ export default class extends Controller {
    +
    + 272 + +
    +   + } +
    +
    + 273 + +
    +   +
    +
    +
    + 274 + +
    +   + helper.sendRequest({ +
    +
    + 275 + +
    + - + data: {'error': e, method: 'openConference'} +
    +
    + 276 + +
    + - + }, helper.getHostname() + '/api/log', "POST", "json", true, "application/json"); +
    +
    + 277 + +
    + - + }); +
    +
    + 278 + +
    +   + }); +
    +
    + 279 + +
    +   +
    +
    +
    + 280 + +
    +   + } else { +
    +
    +
    @@ -376,7 +380,6 @@ export default class extends Controller {
    +
    + 376 + +
    +   + btnPayment.setAttribute('data-amt' , data.payment.amt); +
    +
    + 377 + +
    +   + btnPayment.setAttribute('data-payprofileid' , data.payment.magazineId); +
    +
    + 378 + +
    +   + btnPayment.addEventListener('click', function () { +
    +
    + 379 + +
    + - +
    +
    +
    + 380 + +
    +   + var params = { +
    +
    + 381 + +
    +   + 'orderid': Number(btnPayment.dataset.id) , +
    +
    + 382 + +
    +   + 'payprofileid': btnPayment.dataset.payprofileid, +
    +
    +
    @@ -386,14 +389,26 @@ export default class extends Controller {
    +
    + 386 + +
    +   + 'pcode': webSDK.data.user.id, +
    +
    + 387 + +
    +   + 'successurl': document.location.origin + '/case-history#pay-success', +
    +
    + 388 + +
    +   + 'errorurl': document.location.origin + '/case-history#error', +
    +
    + 389 + +
    + - + 'containerId': 'popup-body', +
    +
    + 390 + +
    +   + }; +
    +
    + 391 + +
    +   +
    +
    +
    + 392 + +
    + - + webSDK.loadPaymentView(params); +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + 393 + +
    +   +
    +
    +
    + 394 + +
    + - + popup.querySelector('#popup-body').innerHTML = ''; +
    +
    + 395 + +
    + - + popup.querySelector('.modal-title').innerHTML = 'Оплата'; +
    +
    + 396 + +
    + - + $(popup).modal('show'); +
    +
    + + +
    +   +
    +
    +
    + 397 + +
    +   + }); +
    +
    + 398 + +
    +   + } else { +
    +
    + 399 + +
    +   + btnPayment.classList.add('d-none'); +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
     
    +
    + 2 + +
    +   + import Cookies from 'js-cookie'; +
    +
    + 3 + +
    +   + const loader = require("./../components/loader.js"); +
    +
    + 4 + +
    +   + const helper = require("./../components/helper.js"); +
    +
    + 5 + +
    + + + const misSession = require("./../components/misSession.js"); +
    +
    + 6 + +
    +   +
    +
    +
    + 7 + +
    +   + /* +
    +
    + 8 + +
    +   + * This is an example Stimulus controller! +
    +
    +
     
    +
    + 249 + +
    +   + btnConfirence.setAttribute('data-filial' , data.filial) +
    +
    + 250 + +
    +   + btnConfirence.addEventListener('click', function () { +
    +
    + 251 + +
    +   + popup.querySelector('#popup-body').innerHTML = ''; +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + + +
    +   +
    +
    +
    + 252 + +
    +   +
    +
    +
    + 253 + +
    + + + misSession.ensureAuthenticated(webSDK).then(function() { +
    +
    + 254 + +
    + + + return webSDK.openConference({ +
    +
    + 255 + +
    + + + schedid: btnConfirence.dataset.id +
    +
    + 256 + +
    + + + }); +
    +
    + 257 + +
    + + + }).then(function () { +
    +
    + 258 + +
    + + + if (!misSession.mountConferenceInPopup(popup)) { +
    +
    + 259 + +
    + + + throw { data: { message: 'Не удалось открыть окно онлайн-консультации.' } }; +
    +
    + 260 + +
    + + + } +
    +
    + 261 + +
    +   + }).catch(function (e) { +
    +
    + 262 + +
    + + + if (misSession.handleMisSessionFailure(popup, e)) { +
    +
    + 263 + +
    + + + helper.sendRequest({ +
    +
    + 264 + +
    + + + data: {'error': e, method: 'openConference', reason: 'mis_session_expired'} +
    +
    + 265 + +
    + + + }, helper.getHostname() + '/api/log', "POST", "json", true, "application/json"); +
    +
    + 266 + +
    + + + return; +
    +
    + 267 + +
    + + + } +
    +
    + 268 + +
    + + +
    +
    +
    + 269 + +
    +   + if (typeof e.data?.message !== 'undefined') { +
    +
    + 270 + +
    +   + var msg = e.data.message.replace('UTC+3', 'UTC+3 (московское время)'); +
    +
    + 271 + +
    +   + popup.querySelector('#popup-body').innerHTML = msg; +
    +
    +
     
    +
    + 276 + +
    +   + } +
    +
    + 277 + +
    +   +
    +
    +
    + 278 + +
    +   + helper.sendRequest({ +
    +
    + 279 + +
    + + + data: {'error': e, method: 'openConference'} +
    +
    + 280 + +
    + + + }, helper.getHostname() + '/api/log', "POST", "json", true, "application/json"); +
    +
    + 281 + +
    + + + }); +
    +
    + 282 + +
    +   + }); +
    +
    + 283 + +
    +   +
    +
    +
    + 284 + +
    +   + } else { +
    +
    +
     
    +
    + 380 + +
    +   + btnPayment.setAttribute('data-amt' , data.payment.amt); +
    +
    + 381 + +
    +   + btnPayment.setAttribute('data-payprofileid' , data.payment.magazineId); +
    +
    + 382 + +
    +   + btnPayment.addEventListener('click', function () { +
    +
    + + +
    +   +
    +
    +
    + 383 + +
    +   + var params = { +
    +
    + 384 + +
    +   + 'orderid': Number(btnPayment.dataset.id) , +
    +
    + 385 + +
    +   + 'payprofileid': btnPayment.dataset.payprofileid, +
    +
    +
     
    +
    + 389 + +
    +   + 'pcode': webSDK.data.user.id, +
    +
    + 390 + +
    +   + 'successurl': document.location.origin + '/case-history#pay-success', +
    +
    + 391 + +
    +   + 'errorurl': document.location.origin + '/case-history#error', +
    +
    + 392 + +
    + + + 'containerId': 'popup-body', +
    +
    + 393 + +
    +   + }; +
    +
    + 394 + +
    +   +
    +
    +
    + 395 + +
    + + + misSession.ensureAuthenticated(webSDK).then(function() { +
    +
    + 396 + +
    + + + webSDK.loadPaymentView(params); +
    +
    + 397 + +
    + + + popup.querySelector('#popup-body').innerHTML = ''; +
    +
    + 398 + +
    + + + popup.querySelector('.modal-title').innerHTML = 'Оплата'; +
    +
    + 399 + +
    + + + $(popup).modal('show'); +
    +
    + 400 + +
    + + + }).catch(function(e) { +
    +
    + 401 + +
    + + + if (misSession.handleMisSessionFailure(popup, e)) { +
    +
    + 402 + +
    + + + helper.sendRequest({ +
    +
    + 403 + +
    + + + data: {'error': e, method: 'loadPaymentView', reason: 'mis_session_expired'} +
    +
    + 404 + +
    + + + }, helper.getHostname() + '/api/log', "POST", "json", true, "application/json"); +
    +
    + 405 + +
    + + + return; +
    +
    + 406 + +
    + + + } +
    +
    + 407 + +
    +   +
    +
    +
    + 408 + +
    + + + helper.sendRequest({ +
    +
    + 409 + +
    + + + data: {'error': e, method: 'loadPaymentView'} +
    +
    + 410 + +
    + + + }, helper.getHostname() + '/api/log', "POST", "json", true, "application/json"); +
    +
    + 411 + +
    + + + }); +
    +
    + 412 + +
    +   + }); +
    +
    + 413 + +
    +   + } else { +
    +
    + 414 + +
    +   + btnPayment.classList.add('d-none'); +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/migrations/.gitignore b/migrations/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/migrations/Version20250905084451.php b/migrations/Version20250905084451.php new file mode 100644 index 0000000..1fdbab9 --- /dev/null +++ b/migrations/Version20250905084451.php @@ -0,0 +1,69 @@ +addSql('DROP SEQUENCE calltouch_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE calltouch_direct_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE calltouch_map_visit_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE calltouch_order_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE calltouch_session_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE calltouch_webhook_orm_id_seq CASCADE'); + $this->addSql('ALTER TABLE calltouch_session DROP CONSTRAINT fk_6ce17ff773c45666'); + $this->addSql('ALTER TABLE calltouch DROP CONSTRAINT fk_9c123ab5a76ed395'); + $this->addSql('ALTER TABLE calltouch_order DROP CONSTRAINT fk_d09e836573c45666'); + $this->addSql('ALTER TABLE calltouch_direct DROP CONSTRAINT fk_e33e2cff73c45666'); + $this->addSql('ALTER TABLE calltouch_map_visit DROP CONSTRAINT fk_4b22276773c45666'); + $this->addSql('DROP TABLE calltouch_session'); + $this->addSql('DROP TABLE calltouch'); + $this->addSql('DROP TABLE calltouch_order'); + $this->addSql('DROP TABLE calltouch_direct'); + $this->addSql('DROP TABLE calltouch_map_visit'); + $this->addSql('DROP TABLE calltouch_webhook'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('CREATE SEQUENCE calltouch_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE calltouch_direct_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE calltouch_map_visit_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE calltouch_order_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE calltouch_session_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE calltouch_webhook_orm_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE calltouch_session (id INT NOT NULL, calltouch_id INT DEFAULT NULL, session_id BIGINT DEFAULT NULL, keywords TEXT DEFAULT NULL, city VARCHAR(255) DEFAULT NULL, source TEXT DEFAULT NULL, medium TEXT DEFAULT NULL, url TEXT DEFAULT NULL, utm_source TEXT DEFAULT NULL, utm_medium TEXT DEFAULT NULL, utm_term TEXT DEFAULT NULL, utm_content TEXT DEFAULT NULL, utm_campaign TEXT DEFAULT NULL, ya_client_id TEXT DEFAULT NULL, gua_client_id TEXT DEFAULT NULL, call_url TEXT DEFAULT NULL, ct_caller_id VARCHAR(255) DEFAULT NULL, status_details VARCHAR(255) DEFAULT NULL, sip_call_id VARCHAR(255) DEFAULT NULL, call_reference_id VARCHAR(255) DEFAULT NULL, ref TEXT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX uniq_6ce17ff773c45666 ON calltouch_session (calltouch_id)'); + $this->addSql('CREATE TABLE calltouch (id INT NOT NULL, user_id INT DEFAULT NULL, request_id BIGINT DEFAULT NULL, request_number TEXT DEFAULT NULL, ct_client_id BIGINT DEFAULT NULL, site_id BIGINT NOT NULL, target_request BOOLEAN NOT NULL, status VARCHAR(255) NOT NULL, unique_request BOOLEAN NOT NULL, uniq_target_request BOOLEAN NOT NULL, comments TEXT NOT NULL, date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, subject VARCHAR(255) DEFAULT NULL, manager VARCHAR(255) DEFAULT NULL, date_str VARCHAR(255) NOT NULL, call_id INT DEFAULT NULL, callphase VARCHAR(255) DEFAULT NULL, call_tags TEXT DEFAULT NULL, duration VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX idx_9c123ab5a76ed395 ON calltouch (user_id)'); + $this->addSql('CREATE TABLE calltouch_order (id INT NOT NULL, calltouch_id INT DEFAULT NULL, order_id BIGINT DEFAULT NULL, call_id BIGINT DEFAULT NULL, status VARCHAR(255) DEFAULT NULL, real_sum VARCHAR(255) DEFAULT NULL, offered VARCHAR(255) DEFAULT NULL, sent DATE DEFAULT NULL, sum DOUBLE PRECISION DEFAULT NULL, is_marked BOOLEAN DEFAULT NULL, comments_count VARCHAR(255) DEFAULT NULL, current_amount DOUBLE PRECISION DEFAULT NULL, order_number VARCHAR(255) DEFAULT NULL, order_sum DOUBLE PRECISION DEFAULT NULL, order_status VARCHAR(255) DEFAULT NULL, order_comments VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX idx_d09e836573c45666 ON calltouch_order (calltouch_id)'); + $this->addSql('CREATE TABLE calltouch_direct (id INT NOT NULL, calltouch_id INT DEFAULT NULL, criteria_id BIGINT DEFAULT NULL, campaign_id BIGINT DEFAULT NULL, ad_group_id BIGINT DEFAULT NULL, ad_id BIGINT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX uniq_e33e2cff73c45666 ON calltouch_direct (calltouch_id)'); + $this->addSql('CREATE TABLE calltouch_map_visit (id INT NOT NULL, calltouch_id INT DEFAULT NULL, utm_source TEXT DEFAULT NULL, session_date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, city TEXT DEFAULT NULL, utm_term TEXT DEFAULT NULL, utm_content TEXT DEFAULT NULL, user_agent TEXT DEFAULT NULL, session_id BIGINT DEFAULT NULL, source TEXT DEFAULT NULL, medium TEXT DEFAULT NULL, utm_campaign TEXT DEFAULT NULL, url TEXT DEFAULT NULL, ref TEXT NOT NULL, additional_tags TEXT DEFAULT NULL, utm_medium TEXT DEFAULT NULL, gua_client_id DOUBLE PRECISION DEFAULT NULL, keyword TEXT DEFAULT NULL, session_date_str VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX idx_4b22276773c45666 ON calltouch_map_visit (calltouch_id)'); + $this->addSql('CREATE TABLE calltouch_webhook (orm_id BIGINT NOT NULL, id BIGINT DEFAULT NULL, leadtype VARCHAR(170) DEFAULT NULL, callphase VARCHAR(170) DEFAULT NULL, changed_fields TEXT DEFAULT NULL, request_id BIGINT DEFAULT NULL, request_number VARCHAR(170) DEFAULT NULL, subject TEXT DEFAULT NULL, ct_caller_id BIGINT DEFAULT NULL, sub_pool_name VARCHAR(170) DEFAULT NULL, redirect_number VARCHAR(170) DEFAULT NULL, duration VARCHAR(170) DEFAULT NULL, waiting_time VARCHAR(170) DEFAULT NULL, calltime VARCHAR(170) DEFAULT NULL, "timestamp" VARCHAR(170) DEFAULT NULL, request_date VARCHAR(170) DEFAULT NULL, status VARCHAR(170) DEFAULT NULL, status_details VARCHAR(170) DEFAULT NULL, unique_str VARCHAR(170) DEFAULT NULL, targetcall VARCHAR(170) DEFAULT NULL, uniqtargetcall VARCHAR(170) DEFAULT NULL, callback VARCHAR(170) DEFAULT NULL, uniquerequest VARCHAR(170) DEFAULT NULL, targetrequest VARCHAR(170) DEFAULT NULL, uniqtargetrequest VARCHAR(170) DEFAULT NULL, worktime VARCHAR(170) DEFAULT NULL, pool VARCHAR(170) DEFAULT NULL, rating VARCHAR(170) DEFAULT NULL, comment TEXT DEFAULT NULL, tags_auto_pr VARCHAR(170) DEFAULT NULL, tags_auto_af VARCHAR(170) DEFAULT NULL, tags_auto_gr VARCHAR(170) DEFAULT NULL, tags_auto_ct VARCHAR(170) DEFAULT NULL, tags_auto_pn VARCHAR(170) DEFAULT NULL, tags_manual VARCHAR(170) DEFAULT NULL, tags_api TEXT DEFAULT NULL, tags_request TEXT DEFAULT NULL, attribution VARCHAR(170) DEFAULT NULL, source VARCHAR(170) DEFAULT NULL, medium VARCHAR(170) DEFAULT NULL, utm_source VARCHAR(170) DEFAULT NULL, utm_medium VARCHAR(170) DEFAULT NULL, utm_campaign VARCHAR(170) DEFAULT NULL, utm_content VARCHAR(170) DEFAULT NULL, utm_term VARCHAR(170) DEFAULT NULL, add_url_params TEXT DEFAULT NULL, gcid BIGINT DEFAULT NULL, ya_client_id BIGINT DEFAULT NULL, session_id BIGINT DEFAULT NULL, ct_client_id BIGINT DEFAULT NULL, ct_global_id BIGINT DEFAULT NULL, hostname VARCHAR(170) DEFAULT NULL, url TEXT DEFAULT NULL, attrs TEXT DEFAULT NULL, call_url TEXT DEFAULT NULL, request_url TEXT DEFAULT NULL, callback_request_id BIGINT DEFAULT NULL, callback_final_attempt VARCHAR(170) DEFAULT NULL, ref TEXT DEFAULT NULL, city VARCHAR(170) DEFAULT NULL, browser VARCHAR(170) DEFAULT NULL, os VARCHAR(170) DEFAULT NULL, device VARCHAR(170) DEFAULT NULL, ip VARCHAR(170) DEFAULT NULL, sip_call_id BIGINT DEFAULT NULL, call_reference_number VARCHAR(170) DEFAULT NULL, reclink TEXT DEFAULT NULL, order_id BIGINT DEFAULT NULL, site_id BIGINT DEFAULT NULL, site_name VARCHAR(170) DEFAULT NULL, user_agent TEXT DEFAULT NULL, sending_timestamp VARCHAR(170) DEFAULT NULL, text TEXT DEFAULT NULL, manager VARCHAR(170) DEFAULT NULL, spam VARCHAR(170) DEFAULT NULL, callback_custom_fields VARCHAR(170) DEFAULT NULL, callback_request_facebook_lead_id BIGINT DEFAULT NULL, PRIMARY KEY(orm_id))'); + $this->addSql('ALTER TABLE calltouch_session ADD CONSTRAINT fk_6ce17ff773c45666 FOREIGN KEY (calltouch_id) REFERENCES calltouch (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE calltouch ADD CONSTRAINT fk_9c123ab5a76ed395 FOREIGN KEY (user_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE calltouch_order ADD CONSTRAINT fk_d09e836573c45666 FOREIGN KEY (calltouch_id) REFERENCES calltouch (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE calltouch_direct ADD CONSTRAINT fk_e33e2cff73c45666 FOREIGN KEY (calltouch_id) REFERENCES calltouch (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE calltouch_map_visit ADD CONSTRAINT fk_4b22276773c45666 FOREIGN KEY (calltouch_id) REFERENCES calltouch (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } +} diff --git a/migrations/Version20250906131236.php b/migrations/Version20250906131236.php new file mode 100644 index 0000000..e1cbd5a --- /dev/null +++ b/migrations/Version20250906131236.php @@ -0,0 +1,34 @@ +addSql('ALTER TABLE department ALTER did TYPE BIGINT USING did::bigint'); + $this->addSql('ALTER TABLE filial ALTER fid TYPE INTEGER USING fid::integer'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('ALTER TABLE department ALTER did TYPE VARCHAR(255)'); + $this->addSql('ALTER TABLE filial ALTER fid TYPE VARCHAR(255)'); + } +} diff --git a/migrations/Version20250907100913.php b/migrations/Version20250907100913.php new file mode 100644 index 0000000..90926df --- /dev/null +++ b/migrations/Version20250907100913.php @@ -0,0 +1,65 @@ +addSql('ALTER TABLE review DROP CONSTRAINT fk_794381c67b100c1a'); + $this->addSql('ALTER TABLE price DROP CONSTRAINT fk_cac822d97b100c1a'); + $this->addSql('DROP SEQUENCE idoctor_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE location_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE specialist_id_seq CASCADE'); + $this->addSql('ALTER TABLE location DROP CONSTRAINT fk_5e9e89cb299b2577'); + $this->addSql('ALTER TABLE location DROP CONSTRAINT fk_5e9e89cb7b100c1a'); + $this->addSql('ALTER TABLE location DROP CONSTRAINT fk_5e9e89cbae80f5df'); + $this->addSql('DROP TABLE specialist'); + $this->addSql('DROP TABLE idoctor'); + $this->addSql('DROP TABLE location'); + $this->addSql('ALTER TABLE department ALTER did TYPE INT'); + $this->addSql('DROP INDEX idx_cac822d97b100c1a'); + $this->addSql('ALTER TABLE price DROP specialist_id'); + $this->addSql('DROP INDEX idx_794381c67b100c1a'); + $this->addSql('ALTER TABLE review DROP specialist_id'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('CREATE SEQUENCE idoctor_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE location_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE specialist_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE specialist (id INT NOT NULL, name VARCHAR(255) NOT NULL, nearest_date DATE DEFAULT NULL, kinder INT DEFAULT NULL, speciality VARCHAR(255) DEFAULT NULL, category VARCHAR(255) DEFAULT NULL, expirience VARCHAR(255) DEFAULT NULL, description TEXT DEFAULT NULL, img VARCHAR(255) DEFAULT NULL, infoclinica BOOLEAN NOT NULL, alias VARCHAR(255) NOT NULL, dcode VARCHAR(255) NOT NULL, updated TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE TABLE idoctor (id INT NOT NULL, dcode INT NOT NULL, name VARCHAR(255) NOT NULL, department_id INT NOT NULL, department_name VARCHAR(255) NOT NULL, filial_id INT NOT NULL, filial_name VARCHAR(255) NOT NULL, comment VARCHAR(255) DEFAULT NULL, nearest_date VARCHAR(255) NOT NULL, view_in_web INT NOT NULL, online_mode INT NOT NULL, updated TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE TABLE location (id INT NOT NULL, department_id INT NOT NULL, filial_id INT NOT NULL, specialist_id INT DEFAULT NULL, dcode INT NOT NULL, online_mode BOOLEAN NOT NULL, active BOOLEAN NOT NULL, updated TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX idx_5e9e89cb299b2577 ON location (filial_id)'); + $this->addSql('CREATE INDEX idx_5e9e89cb7b100c1a ON location (specialist_id)'); + $this->addSql('CREATE INDEX idx_5e9e89cbae80f5df ON location (department_id)'); + $this->addSql('ALTER TABLE location ADD CONSTRAINT fk_5e9e89cb299b2577 FOREIGN KEY (filial_id) REFERENCES filial (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE location ADD CONSTRAINT fk_5e9e89cb7b100c1a FOREIGN KEY (specialist_id) REFERENCES specialist (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE location ADD CONSTRAINT fk_5e9e89cbae80f5df FOREIGN KEY (department_id) REFERENCES department (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE review ADD specialist_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE review ADD CONSTRAINT fk_794381c67b100c1a FOREIGN KEY (specialist_id) REFERENCES specialist (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE INDEX idx_794381c67b100c1a ON review (specialist_id)'); + $this->addSql('ALTER TABLE price ADD specialist_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE price ADD CONSTRAINT fk_cac822d97b100c1a FOREIGN KEY (specialist_id) REFERENCES specialist (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE INDEX idx_cac822d97b100c1a ON price (specialist_id)'); + $this->addSql('ALTER TABLE department ALTER did TYPE BIGINT'); + } +} diff --git a/mr.diff b/mr.diff new file mode 100644 index 0000000..1312eee --- /dev/null +++ b/mr.diff @@ -0,0 +1,225 @@ +diff --git a/assets/components/misSession.js b/assets/components/misSession.js +new file mode 100644 +index 0000000..45088ef +--- /dev/null ++++ b/assets/components/misSession.js +@@ -0,0 +1,118 @@ ++/** ++ * MIS (webSDK / widget.sovamed.ru) session helpers. ++ * Symfony login (ROLE_USER) and MIS session are independent. ++ */ ++ ++function MisSessionError() { ++ this.name = 'MisSessionError'; ++ this.message = 'MIS session is not authenticated'; ++} ++ ++function isAuthenticated(webSDK) { ++ return Boolean(webSDK?.data?.user?.authenticated); ++} ++ ++function ensureAuthenticated(webSDK) { ++ return new Promise(function(resolve, reject) { ++ if (isAuthenticated(webSDK)) { ++ resolve(webSDK); ++ return; ++ } ++ ++ if (typeof webSDK?.isLoggedIn === 'function') { ++ webSDK.isLoggedIn(webSDK.sdkOrigin).then(function(result) { ++ if (result?.authenticated) { ++ resolve(webSDK); ++ return; ++ } ++ reject(new MisSessionError()); ++ }).catch(function() { ++ reject(new MisSessionError()); ++ }); ++ return; ++ } ++ ++ reject(new MisSessionError()); ++ }); ++} ++ ++function removeSdkOverlayModals() { ++ document.querySelectorAll('.wr-sdk-widget-modal').forEach(function(modal) { ++ modal.remove(); ++ }); ++} ++ ++/** ++ * Move #iframeProtocol from SDK overlay into cabinet bootstrap popup. ++ * openConference() must be called without container (SDK applies Guest URL fix). ++ */ ++function mountConferenceInPopup(popup) { ++ var iframe = document.getElementById('iframeProtocol'); ++ var popupBody = popup?.querySelector('#popup-body'); ++ ++ if (!iframe || !popupBody) { ++ return false; ++ } ++ ++ removeSdkOverlayModals(); ++ popupBody.innerHTML = ''; ++ popupBody.appendChild(iframe); ++ iframe.style.width = '100%'; ++ iframe.style.border = 'none'; ++ iframe.style.height = (window.innerHeight - 100) + 'px'; ++ ++ var fullScreenBtn = popup.querySelector('.full-scren-modal'); ++ if (fullScreenBtn) { ++ fullScreenBtn.classList.remove('d-none'); ++ } ++ ++ popup.querySelector('.modal-dialog').classList = 'modal-dialog'; ++ popup.querySelector('.modal-content').classList = 'modal-content'; ++ popup.querySelector('.modal-title').innerHTML = 'Онлайн консультация'; ++ ++ if (typeof $ !== 'undefined' && typeof $(popup).modal === 'function') { ++ $(popup).modal('show'); ++ } ++ ++ return true; ++} ++ ++function showMisSessionExpired(popup) { ++ if (!popup) { ++ window.location.pathname = '/logout'; ++ return; ++ } ++ ++ popup.querySelector('.modal-dialog').classList = 'modal-dialog'; ++ popup.querySelector('.modal-title').innerHTML = 'Сессия виджета истекла'; ++ popup.querySelector('#popup-body').innerHTML = ++ '

    Личный кабинет открыт, но сессия виджета записи (MIS) истекла. ' + ++ 'Для оплаты и онлайн-приёма нужно войти снова — повторная авторизация в iframe не требуется.

    ' + ++ ''; ++ ++ popup.querySelector('#mis-session-relogin').addEventListener('click', function() { ++ window.location.pathname = '/logout'; ++ }); ++ ++ if (typeof $ !== 'undefined' && typeof $(popup).modal === 'function') { ++ $(popup).modal('show'); ++ } ++} ++ ++function handleMisSessionFailure(popup, error, logContext) { ++ if (error instanceof MisSessionError || error?.name === 'MisSessionError') { ++ showMisSessionExpired(popup); ++ return true; ++ } ++ ++ return false; ++} ++ ++module.exports = { ++ MisSessionError: MisSessionError, ++ isAuthenticated: isAuthenticated, ++ ensureAuthenticated: ensureAuthenticated, ++ mountConferenceInPopup: mountConferenceInPopup, ++ showMisSessionExpired: showMisSessionExpired, ++ handleMisSessionFailure: handleMisSessionFailure, ++}; +diff --git a/assets/controllers/caseHistory_controller.js b/assets/controllers/caseHistory_controller.js +index 735c13c..aabafb1 100644 +--- a/assets/controllers/caseHistory_controller.js ++++ b/assets/controllers/caseHistory_controller.js +@@ -2,6 +2,7 @@ import { Controller } from 'stimulus'; + import Cookies from 'js-cookie'; + const loader = require("./../components/loader.js"); + const helper = require("./../components/helper.js"); ++const misSession = require("./../components/misSession.js"); + + /* + * This is an example Stimulus controller! +@@ -248,20 +249,23 @@ export default class extends Controller { + btnConfirence.setAttribute('data-filial' , data.filial) + btnConfirence.addEventListener('click', function () { + popup.querySelector('#popup-body').innerHTML = ''; +- +- webSDK.openConference({ +- schedid: btnConfirence.dataset.id, +- container: popup.querySelector('#popup-body') +- }).then(function () { +- popup.querySelector('.full-scren-modal').classList.remove('d-none'); +- document.getElementById('iframeProtocol').style.height = (window.innerHeight - 100) + 'px'; +- popup.querySelector('.modal-dialog').classList = 'modal-dialog'; +- popup.querySelector('.modal-content').classList = 'modal-content'; +- popup.querySelector('.modal-content').classList = 'modal-content'; +- popup.querySelector('.modal-title').innerHTML = 'Онлайн консультация'; + +- $(popup).modal('show'); ++ misSession.ensureAuthenticated(webSDK).then(function() { ++ return webSDK.openConference({ ++ schedid: btnConfirence.dataset.id ++ }); ++ }).then(function () { ++ if (!misSession.mountConferenceInPopup(popup)) { ++ throw { data: { message: 'Не удалось открыть окно онлайн-консультации.' } }; ++ } + }).catch(function (e) { ++ if (misSession.handleMisSessionFailure(popup, e)) { ++ helper.sendRequest({ ++ data: {'error': e, method: 'openConference', reason: 'mis_session_expired'} ++ }, helper.getHostname() + '/api/log', "POST", "json", true, "application/json"); ++ return; ++ } ++ + if (typeof e.data?.message !== 'undefined') { + var msg = e.data.message.replace('UTC+3', 'UTC+3 (московское время)'); + popup.querySelector('#popup-body').innerHTML = msg; +@@ -272,9 +276,9 @@ export default class extends Controller { + } + + helper.sendRequest({ +- data: {'error': e, method: 'openConference'} +- }, helper.getHostname() + '/api/log', "POST", "json", true, "application/json"); +- }); ++ data: {'error': e, method: 'openConference'} ++ }, helper.getHostname() + '/api/log', "POST", "json", true, "application/json"); ++ }); + }); + + } else { +@@ -376,7 +380,6 @@ export default class extends Controller { + btnPayment.setAttribute('data-amt' , data.payment.amt); + btnPayment.setAttribute('data-payprofileid' , data.payment.magazineId); + btnPayment.addEventListener('click', function () { +- + var params = { + 'orderid': Number(btnPayment.dataset.id) , + 'payprofileid': btnPayment.dataset.payprofileid, +@@ -386,14 +389,26 @@ export default class extends Controller { + 'pcode': webSDK.data.user.id, + 'successurl': document.location.origin + '/case-history#pay-success', + 'errorurl': document.location.origin + '/case-history#error', +- 'containerId': 'popup-body', ++ 'containerId': 'popup-body', + }; + +- webSDK.loadPaymentView(params); ++ misSession.ensureAuthenticated(webSDK).then(function() { ++ webSDK.loadPaymentView(params); ++ popup.querySelector('#popup-body').innerHTML = ''; ++ popup.querySelector('.modal-title').innerHTML = 'Оплата'; ++ $(popup).modal('show'); ++ }).catch(function(e) { ++ if (misSession.handleMisSessionFailure(popup, e)) { ++ helper.sendRequest({ ++ data: {'error': e, method: 'loadPaymentView', reason: 'mis_session_expired'} ++ }, helper.getHostname() + '/api/log', "POST", "json", true, "application/json"); ++ return; ++ } + +- popup.querySelector('#popup-body').innerHTML = ''; +- popup.querySelector('.modal-title').innerHTML = 'Оплата'; +- $(popup).modal('show'); ++ helper.sendRequest({ ++ data: {'error': e, method: 'loadPaymentView'} ++ }, helper.getHostname() + '/api/log', "POST", "json", true, "application/json"); ++ }); + }); + } else { + btnPayment.classList.add('d-none'); diff --git a/package.json b/package.json new file mode 100644 index 0000000..94b75fd --- /dev/null +++ b/package.json @@ -0,0 +1,44 @@ +{ + "devDependencies": { + "@babel/core": "^7.25.2", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/preset-env": "^7.25.2", + "@symfony/stimulus-bridge": "^2.0.0", + "@symfony/webpack-encore": "^5.1.0", + "bootstrap": "^4.6.0", + "ckeditor4": "^4.25.1", + "core-js": "^3.0.0", + "font-awesome": "^4.7.0", + "jquery": "^3.6.0", + "popper.js": "^1.16.1", + "regenerator-runtime": "^0.13.2", + "sass": "^1.47.0", + "sass-loader": "^11.0.0", + "stimulus": "^2.0.0", + "webpack": "^5.93.0", + "webpack-cli": "^5.1.4", + "webpack-notifier": "^1.6.0" + }, + "license": "UNLICENSED", + "private": true, + "scripts": { + "dev-server": "encore dev-server", + "dev": "encore dev", + "watch": "encore dev --watch", + "build": "encore production --progress" + }, + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/preset-env": "^7.25.2", + "@fancyapps/fancybox": "^3.5.7", + "bootstrap-select": "^1.13.18", + "daterangepicker": "^3.1.0", + "inputmask": "^5.0.5", + "js-cookie": "^3.0.5", + "owl.carousel2": "^2.2.2", + "path": "^0.12.7", + "swagger-ui-dist": "^5.17.1", + "webpack": "^5.93.0" + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..0bd20c8 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + tests + + + + + + src + + + + + + + + + + diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..da14756 --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,63 @@ +# Based on http://stackoverflow.com/questions/17313023/symfony-2-2-1-url-rewrite-issue +# Modified to use index.php as per symfony >= 4.0 installer + +# Use the front controller as index file. It serves as fallback solution when +# every other rewrite/redirect fails (e.g. in an aliased environment without +# mod_rewrite). Additionally, this reduces the matching process for the +# startpage (path "/") because otherwise Apache will apply the rewritting rules +# to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl). +DirectoryIndex index.php + + + RewriteEngine On + + # Redirect to URI without front controller to prevent duplicate content + # (with and without `/index.php`). Only do this redirect on the initial + # rewrite by Apache and not on subsequent cycles. Otherwise we would get an + # endless redirect loop (request -> rewrite to front controller -> + # redirect -> request -> ...). + # So in case you get a "too many redirects" error or you always get redirected + # to the startpage because your Apache does not expose the REDIRECT_STATUS + # environment variable, you have 2 choices: + # - disable this feature by commenting the following 2 lines or + # - use Apache >= 2.3.9 and replace all L flags by END flags and remove the + # following RewriteCond (best solution) + RewriteCond %{ENV:REDIRECT_STATUS} ^$ + RewriteRule ^index\.php(/(.*)|$) %{CONTEXT_PREFIX}/$2 [R=301,L] + + # If the requested filename exists, simply serve it. + # We only want to let Apache serve files and not directories. + RewriteCond %{REQUEST_FILENAME} -f + RewriteRule .? - [L] + + # cache-bust assets url rewrite + # Example format: /cpv-10/js/test123.js -> /js/test123.js + # This allows us to change the asset version and "bust" intermediate caches (like varnish) + # See http://symfony.com/doc/current/reference/configuration/framework.html#ref-framework-assets-version + # See http://symfony.com/doc/current/reference/configuration/framework.html#assets-version-format + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule ^cpv-\d+\/(.+)$ $1 [L] + + RewriteCond %{REQUEST_FILENAME} -f + RewriteRule ^(.*)$ index.php [QSA,L] + + # The following rewrites all other queries to the front controller. The + # condition ensures that if you are using Apache aliases to do mass virtual + # hosting, the base path will be prepended to allow proper resolution of the + # index.php file; it will work in non-aliased environments as well, providing + # a safe, one-size fits all solution. + RewriteCond %{REQUEST_URI}::$1 ^(/.+)(.+)::\2$ + RewriteRule ^(.*) - [E=BASE:%1] + RewriteRule .? %{ENV:BASE}index.php [L] + + + + + # When mod_rewrite is not available, we instruct a temporary redirect of + # the startpage to the front controller explicitly so that the website + # and the generated links can still be used. + RedirectMatch 302 ^/$ /index.php/ + # RedirectTemp cannot be used instead + + \ No newline at end of file diff --git a/public/comingSoon.php b/public/comingSoon.php new file mode 100644 index 0000000..e69eb2d --- /dev/null +++ b/public/comingSoon.php @@ -0,0 +1,56 @@ + + + + + В личном кабинете проводятся работы по настройке + + + + + + +
    +
    +
    +
    +
    +
    +
    + В личном кабинете проводятся работы по настройке. +
    +
    + На старую версию личного кабинета +
    +
    +
    +
    +
    +
    + + \ No newline at end of file diff --git a/public/comingSoon/bootstrap.min.css b/public/comingSoon/bootstrap.min.css new file mode 100644 index 0000000..ef399d2 --- /dev/null +++ b/public/comingSoon/bootstrap.min.css @@ -0,0 +1,7 @@ +/*! + * Bootstrap v4.6.0 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors + * Copyright 2011-2021 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([class]){color:inherit;text-decoration:none}a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit;text-align:-webkit-match-parent}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-sm-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-sm-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-md-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-md-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-md-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-md-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-md-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-md-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-lg-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-lg-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-xl-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-xl-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;margin-bottom:1rem;color:#212529}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{color:#212529;background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-primary tbody+tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#7abaff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-secondary tbody+tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#b3b7bb}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-success tbody+tbody,.table-success td,.table-success th,.table-success thead th{border-color:#8fd19e}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-info tbody+tbody,.table-info td,.table-info th,.table-info thead th{border-color:#86cfda}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-warning tbody+tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#ffdf7e}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-danger tbody+tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#ed969e}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-light tbody+tbody,.table-light td,.table-light th,.table-light thead th{border-color:#fbfcfc}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#95999c}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#343a40;border-color:#454d55}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#343a40}.table-dark td,.table-dark th,.table-dark thead th{border-color:#454d55}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}input[type=date].form-control,input[type=datetime-local].form-control,input[type=month].form-control,input[type=time].form-control{-webkit-appearance:none;-moz-appearance:none;appearance:none}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;font-size:1rem;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;left:0;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(40,167,69,.9);border-radius:.25rem}.form-row>.col>.valid-tooltip,.form-row>[class*=col-]>.valid-tooltip{left:5px}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#28a745;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-valid,.was-validated .custom-select:valid{border-color:#28a745;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right .75rem center/8px 10px no-repeat,#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem) no-repeat}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{border-color:#28a745}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{border-color:#34ce57;background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label::before{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;left:0;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.form-row>.col>.invalid-tooltip,.form-row>[class*=col-]>.invalid-tooltip{left:5px}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-invalid,.was-validated .custom-select:invalid{border-color:#dc3545;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right .75rem center/8px 10px no-repeat,#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e") center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem) no-repeat}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{border-color:#dc3545}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{border-color:#e4606d;background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label::before{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;-ms-flex-negative:0;flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#212529;text-align:center;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529;text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}.btn:not(:disabled):not(.disabled){cursor:pointer}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#0069d9;border-color:#0062cc;box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{color:#fff;background-color:#5a6268;border-color:#545b62;box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#218838;border-color:#1e7e34;box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#138496;border-color:#117a8b;box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{color:#212529;background-color:#e0a800;border-color:#d39e00;box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c82333;border-color:#bd2130;box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{color:#212529;background-color:#e2e6ea;border-color:#dae0e5;box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{color:#fff;background-color:#23272b;border-color:#1d2124;box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-outline-primary{color:#007bff;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;text-decoration:none}.btn-link:hover{color:#0056b3;text-decoration:underline}.btn-link.focus,.btn-link:focus{text-decoration:underline}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control,.input-group>.form-control-plaintext{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;min-width:0;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control,.input-group>.form-control-plaintext+.custom-file,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:first-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group:not(.has-validation)>.custom-file:not(:last-child) .custom-file-label::after,.input-group:not(.has-validation)>.custom-select:not(:last-child),.input-group:not(.has-validation)>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.custom-file:nth-last-child(n+3) .custom-file-label::after,.input-group.has-validation>.custom-select:nth-last-child(n+3),.input-group.has-validation>.form-control:nth-last-child(n+3){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-append,.input-group-prepend{display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.custom-select,.input-group-lg>.form-control:not(textarea){height:calc(1.5em + 1rem + 2px)}.input-group-lg>.custom-select,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.custom-select,.input-group-sm>.form-control:not(textarea){height:calc(1.5em + .5rem + 2px)}.input-group-sm>.custom-select,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group.has-validation>.input-group-append:nth-last-child(n+3)>.btn,.input-group.has-validation>.input-group-append:nth-last-child(n+3)>.input-group-text,.input-group:not(.has-validation)>.input-group-append:not(:last-child)>.btn,.input-group:not(.has-validation)>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;z-index:1;display:block;min-height:1.5rem;padding-left:1.5rem;-webkit-print-color-adjust:exact;color-adjust:exact}.custom-control-inline{display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;left:0;z-index:-1;width:1rem;height:1.25rem;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;border-color:#007bff;background-color:#007bff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:focus:not(:checked)~.custom-control-label::before{border-color:#80bdff}.custom-control-input:not(:disabled):active~.custom-control-label::before{color:#fff;background-color:#b3d7ff;border-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label,.custom-control-input[disabled]~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before,.custom-control-input[disabled]~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#adb5bd solid 1px}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:50%/50% 50% no-repeat}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{border-color:#007bff;background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:.5rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label::after{background-color:#fff;-webkit-transform:translateX(.75rem);transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;vertical-align:middle;background:#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right .75rem center/8px 10px no-repeat;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{display:none}.custom-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.custom-select-sm{height:calc(1.5em + .5rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + .75rem + 2px);margin:0;overflow:hidden;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:disabled~.custom-file-label,.custom-file-input[disabled]~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;overflow:hidden;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + .75rem);padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:inherit;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;height:1.4rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#007bff;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{-moz-transition:none;transition:none}}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#007bff;border:0;border-radius:1rem;-ms-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{-ms-transition:none;transition:none}}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item,.nav-fill>.nav-link{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar .container,.navbar .container-fluid,.navbar .container-lg,.navbar .container-md,.navbar .container-sm,.navbar .container-xl{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:50%/100% 100% no-repeat}.navbar-nav-scroll{max-height:75vh;overflow-y:auto}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;min-height:1px;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{-ms-flex-negative:0;flex-shrink:0;width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{-ms-flex:1 0 0%;flex:1 0 0%;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion{overflow-anchor:none}.accordion>.card{overflow:hidden}.accordion>.card:not(:last-of-type){border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:not(:first-of-type){border-top-left-radius:0;border-top-right-radius:0}.accordion>.card>.card-header{border-radius:0;margin-bottom:-1px}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:3;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.badge{transition:none}}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}a.badge-primary:focus,a.badge-primary:hover{color:#fff;background-color:#0062cc}a.badge-primary.focus,a.badge-primary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.badge-secondary{color:#fff;background-color:#6c757d}a.badge-secondary:focus,a.badge-secondary:hover{color:#fff;background-color:#545b62}a.badge-secondary.focus,a.badge-secondary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.badge-success{color:#fff;background-color:#28a745}a.badge-success:focus,a.badge-success:hover{color:#fff;background-color:#1e7e34}a.badge-success.focus,a.badge-success:focus{outline:0;box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.badge-info{color:#fff;background-color:#17a2b8}a.badge-info:focus,a.badge-info:hover{color:#fff;background-color:#117a8b}a.badge-info.focus,a.badge-info:focus{outline:0;box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.badge-warning{color:#212529;background-color:#ffc107}a.badge-warning:focus,a.badge-warning:hover{color:#212529;background-color:#d39e00}a.badge-warning.focus,a.badge-warning:focus{outline:0;box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.badge-danger{color:#fff;background-color:#dc3545}a.badge-danger:focus,a.badge-danger:hover{color:#fff;background-color:#bd2130}a.badge-danger.focus,a.badge-danger:focus{outline:0;box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.badge-light{color:#212529;background-color:#f8f9fa}a.badge-light:focus,a.badge-light:hover{color:#212529;background-color:#dae0e5}a.badge-light.focus,a.badge-light:focus{outline:0;box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.badge-dark{color:#fff;background-color:#343a40}a.badge-dark:focus,a.badge-dark:hover{color:#fff;background-color:#1d2124}a.badge-dark.focus,a.badge-dark:focus{outline:0;box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;z-index:2;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;line-height:0;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.media-body{-ms-flex:1;flex:1}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{opacity:.75}button.close{padding:0;background-color:transparent;border:0}a.close.disabled{pointer-events:none}.toast{-ms-flex-preferred-size:350px;flex-basis:350px;max-width:350px;font-size:.875rem;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .25rem .75rem rgba(0,0,0,.1);opacity:0;border-radius:.25rem}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.25rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-50px);transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal.modal-static .modal-dialog{-webkit-transform:scale(1.02);transform:scale(1.02)}.modal-dialog-scrollable{display:-ms-flexbox;display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-footer,.modal-dialog-scrollable .modal-header{-ms-flex-negative:0;flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);height:-webkit-min-content;height:-moz-min-content;height:min-content;content:""}.modal-dialog-centered.modal-dialog-scrollable{-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem);height:-webkit-min-content;height:-moz-min-content;height:min-content}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top]>.arrow,.bs-popover-top>.arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=top]>.arrow::before,.bs-popover-top>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top]>.arrow::after,.bs-popover-top>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right]>.arrow,.bs-popover-right>.arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right]>.arrow::before,.bs-popover-right>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right]>.arrow::after,.bs-popover-right>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom]>.arrow,.bs-popover-bottom>.arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=bottom]>.arrow::before,.bs-popover-bottom>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom]>.arrow::after,.bs-popover-bottom>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left]>.arrow,.bs-popover-left>.arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left]>.arrow::before,.bs-popover-left>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left]>.arrow::after,.bs-popover-left>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{-ms-touch-action:pan-y;touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){-webkit-transform:translateX(100%);transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){-webkit-transform:translateX(-100%);transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;-webkit-transform:none;transform:none}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:50%/100% 100% no-repeat}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@-webkit-keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1;-webkit-transform:none;transform:none}}@keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1;-webkit-transform:none;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded-sm{border-radius:.2rem!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-lg{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;-ms-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;-ms-user-select:none!important;user-select:none!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.min-vw-100{min-width:100vw!important}.min-vh-100{min-height:100vh!important}.vw-100{width:100vw!important}.vh-100{height:100vh!important}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace!important}.text-justify{text-align:justify!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-lighter{font-weight:lighter!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-weight-bolder{font-weight:bolder!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0056b3!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#494f54!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#19692c!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#0f6674!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#ba8b00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#a71d2a!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#cbd3da!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#121416!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none!important}.text-break{word-break:break-word!important;word-wrap:break-word!important}.text-reset{color:inherit!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/public/docs/ids.pdf b/public/docs/ids.pdf new file mode 100644 index 0000000..8deb81a Binary files /dev/null and b/public/docs/ids.pdf differ diff --git a/public/docs/oferta.pdf b/public/docs/oferta.pdf new file mode 100644 index 0000000..ed46788 Binary files /dev/null and b/public/docs/oferta.pdf differ diff --git a/public/docs/online.pdf b/public/docs/online.pdf new file mode 100644 index 0000000..1756cc7 Binary files /dev/null and b/public/docs/online.pdf differ diff --git a/public/docs/onlinegos.pdf b/public/docs/onlinegos.pdf new file mode 100644 index 0000000..a2df0a7 Binary files /dev/null and b/public/docs/onlinegos.pdf differ diff --git a/public/docs/soglasie-cabinet.pdf b/public/docs/soglasie-cabinet.pdf new file mode 100644 index 0000000..5f40fce Binary files /dev/null and b/public/docs/soglasie-cabinet.pdf differ diff --git a/public/docs/soglasie-site.pdf b/public/docs/soglasie-site.pdf new file mode 100644 index 0000000..a315f3d Binary files /dev/null and b/public/docs/soglasie-site.pdf differ diff --git a/public/docs/sovamed-cookie.pdf b/public/docs/sovamed-cookie.pdf new file mode 100644 index 0000000..c0be506 Binary files /dev/null and b/public/docs/sovamed-cookie.pdf differ diff --git a/public/docs/vozvrat.pdf b/public/docs/vozvrat.pdf new file mode 100644 index 0000000..d9f203a Binary files /dev/null and b/public/docs/vozvrat.pdf differ diff --git a/public/docs/wmtmed-cookie.pdf b/public/docs/wmtmed-cookie.pdf new file mode 100644 index 0000000..40cc6ee Binary files /dev/null and b/public/docs/wmtmed-cookie.pdf differ diff --git a/public/favicon_sovamed.ico b/public/favicon_sovamed.ico new file mode 100644 index 0000000..aef26cb Binary files /dev/null and b/public/favicon_sovamed.ico differ diff --git a/public/favicon_wmtmed.ico b/public/favicon_wmtmed.ico new file mode 100644 index 0000000..e3452d4 Binary files /dev/null and b/public/favicon_wmtmed.ico differ diff --git a/public/images/checkmark.png b/public/images/checkmark.png new file mode 100644 index 0000000..eb0bcd0 Binary files /dev/null and b/public/images/checkmark.png differ diff --git a/public/images/eclipse.gif b/public/images/eclipse.gif new file mode 100644 index 0000000..334e7ac Binary files /dev/null and b/public/images/eclipse.gif differ diff --git a/public/images/eisa.jpg b/public/images/eisa.jpg new file mode 100644 index 0000000..71f7a26 Binary files /dev/null and b/public/images/eisa.jpg differ diff --git a/public/images/logo-sova.jpg b/public/images/logo-sova.jpg new file mode 100644 index 0000000..32745bb Binary files /dev/null and b/public/images/logo-sova.jpg differ diff --git a/public/images/logo.png b/public/images/logo.png new file mode 100644 index 0000000..eefe271 Binary files /dev/null and b/public/images/logo.png differ diff --git a/public/images/logo.svg b/public/images/logo.svg new file mode 100644 index 0000000..5cd77d2 --- /dev/null +++ b/public/images/logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/logo_mobile.png b/public/images/logo_mobile.png new file mode 100644 index 0000000..bf69994 Binary files /dev/null and b/public/images/logo_mobile.png differ diff --git a/public/images/mobile-logo.svg b/public/images/mobile-logo.svg new file mode 100644 index 0000000..fe1d9c0 --- /dev/null +++ b/public/images/mobile-logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/no_img.png b/public/images/no_img.png new file mode 100644 index 0000000..335b4b6 Binary files /dev/null and b/public/images/no_img.png differ diff --git a/public/img/3-staff.svg b/public/img/3-staff.svg new file mode 100644 index 0000000..a6e408b --- /dev/null +++ b/public/img/3-staff.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/public/img/arrow-down.svg b/public/img/arrow-down.svg new file mode 100644 index 0000000..5ce29f0 --- /dev/null +++ b/public/img/arrow-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/calendar-input.svg b/public/img/calendar-input.svg new file mode 100644 index 0000000..88d6533 --- /dev/null +++ b/public/img/calendar-input.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/calendar.svg b/public/img/calendar.svg new file mode 100644 index 0000000..a0302ec --- /dev/null +++ b/public/img/calendar.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/check.svg b/public/img/check.svg new file mode 100644 index 0000000..5702ea3 --- /dev/null +++ b/public/img/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/checkbox.svg b/public/img/checkbox.svg new file mode 100644 index 0000000..2dc8b5d --- /dev/null +++ b/public/img/checkbox.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/checkbox_active.svg b/public/img/checkbox_active.svg new file mode 100644 index 0000000..27e4729 --- /dev/null +++ b/public/img/checkbox_active.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/close.svg b/public/img/close.svg new file mode 100644 index 0000000..399be51 --- /dev/null +++ b/public/img/close.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/fake-img.jpg b/public/img/fake-img.jpg new file mode 100644 index 0000000..152e8b5 Binary files /dev/null and b/public/img/fake-img.jpg differ diff --git a/public/img/favorites.svg b/public/img/favorites.svg new file mode 100644 index 0000000..9a0f71b --- /dev/null +++ b/public/img/favorites.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/filter-ico.svg b/public/img/filter-ico.svg new file mode 100644 index 0000000..8a74212 --- /dev/null +++ b/public/img/filter-ico.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/filter.svg b/public/img/filter.svg new file mode 100644 index 0000000..903e2a3 --- /dev/null +++ b/public/img/filter.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/img/logo.svg b/public/img/logo.svg new file mode 100644 index 0000000..5cd77d2 --- /dev/null +++ b/public/img/logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/img/logo/2gis.png b/public/img/logo/2gis.png new file mode 100644 index 0000000..97a1be4 Binary files /dev/null and b/public/img/logo/2gis.png differ diff --git a/public/img/logo/Google.png b/public/img/logo/Google.png new file mode 100644 index 0000000..e8adecc Binary files /dev/null and b/public/img/logo/Google.png differ diff --git a/public/img/logo/ProDoctorov.png b/public/img/logo/ProDoctorov.png new file mode 100644 index 0000000..c2d489c Binary files /dev/null and b/public/img/logo/ProDoctorov.png differ diff --git a/public/img/logo/ProDoctorovSpecialists.png b/public/img/logo/ProDoctorovSpecialists.png new file mode 100644 index 0000000..526f3fc Binary files /dev/null and b/public/img/logo/ProDoctorovSpecialists.png differ diff --git a/public/img/logo/YandexMap.png b/public/img/logo/YandexMap.png new file mode 100644 index 0000000..5c7fb2e Binary files /dev/null and b/public/img/logo/YandexMap.png differ diff --git a/public/img/logo/zoon.png b/public/img/logo/zoon.png new file mode 100644 index 0000000..945bb46 Binary files /dev/null and b/public/img/logo/zoon.png differ diff --git a/public/img/logo_wmt/logo-pdf.jpg b/public/img/logo_wmt/logo-pdf.jpg new file mode 100644 index 0000000..f3ae508 Binary files /dev/null and b/public/img/logo_wmt/logo-pdf.jpg differ diff --git a/public/img/logo_wmt/logo.svg b/public/img/logo_wmt/logo.svg new file mode 100644 index 0000000..ccbbc8b --- /dev/null +++ b/public/img/logo_wmt/logo.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/logo_wmt/mobile-logo.png b/public/img/logo_wmt/mobile-logo.png new file mode 100644 index 0000000..f3fd450 Binary files /dev/null and b/public/img/logo_wmt/mobile-logo.png differ diff --git a/public/img/mobile-logo.svg b/public/img/mobile-logo.svg new file mode 100644 index 0000000..fe1d9c0 --- /dev/null +++ b/public/img/mobile-logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/place.svg b/public/img/place.svg new file mode 100644 index 0000000..9d56121 --- /dev/null +++ b/public/img/place.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/img/repeat-visit.svg b/public/img/repeat-visit.svg new file mode 100644 index 0000000..39ccbbc --- /dev/null +++ b/public/img/repeat-visit.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/search-ico.svg b/public/img/search-ico.svg new file mode 100644 index 0000000..6114f38 --- /dev/null +++ b/public/img/search-ico.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/select-arrow-down.svg b/public/img/select-arrow-down.svg new file mode 100644 index 0000000..ab5641b --- /dev/null +++ b/public/img/select-arrow-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/select-arrow-up.svg b/public/img/select-arrow-up.svg new file mode 100644 index 0000000..50e9ec8 --- /dev/null +++ b/public/img/select-arrow-up.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/sova-bonus-logo.svg b/public/img/sova-bonus-logo.svg new file mode 100644 index 0000000..fe2a75d --- /dev/null +++ b/public/img/sova-bonus-logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/img/staff-icon.png b/public/img/staff-icon.png new file mode 100644 index 0000000..6b0ff35 Binary files /dev/null and b/public/img/staff-icon.png differ diff --git a/public/img/up-arrow.svg b/public/img/up-arrow.svg new file mode 100644 index 0000000..a13136b --- /dev/null +++ b/public/img/up-arrow.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/public/img/user.svg b/public/img/user.svg new file mode 100644 index 0000000..6deaea0 --- /dev/null +++ b/public/img/user.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..3bcee0b --- /dev/null +++ b/public/index.php @@ -0,0 +1,22 @@ +bootEnv(dirname(__DIR__).'/.env'); + +if ($_SERVER['APP_DEBUG']) { + umask(0000); + + Debug::enable(); +} + +$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); +$request = Request::createFromGlobals(); +$response = $kernel->handle($request); +$response->send(); +$kernel->terminate($request, $response); diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..582e7d8 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,5 @@ +User-agent: * +Allow: /login +Allow: /*.css +Allow: /*.js +Disallow: / \ No newline at end of file diff --git a/public/widgets/_wf_cabinet.js b/public/widgets/_wf_cabinet.js new file mode 100644 index 0000000..e55f09a --- /dev/null +++ b/public/widgets/_wf_cabinet.js @@ -0,0 +1,117 @@ +console.log('wf form load'); + +// Получаем данные из атрибутов текущего скрипта +const currentScript = document.currentScript; +const formNameFancyBox = currentScript.dataset.formNameFancyBox; +const buttonClass = currentScript.dataset.buttonClass || '.wf-button'; // Добавляем fallback значение + +// Кэш для cabinetHost чтобы не определять его каждый раз +const CABINET_HOST = location.host === 'wmtmed.ru' + ? 'https://cabinet.wmtmed.ru' + : 'https://cabinet.sovamed.ru'; + +function wf_renderModal(formName, query, wfId, fbStyle, wfStyle) { + // Удаляем предыдущее модальное окно, если оно существует + const existingPopup = document.getElementById(formNameFancyBox); + if (existingPopup) { + existingPopup.remove(); + } + + // Создаем структуру модального окна + const wf_popup = document.createElement('div'); + wf_popup.id = formNameFancyBox; + wf_popup.className = 'fancybox-content'; + wf_popup.style.cssText = fbStyle || ''; + wf_popup.style.display = 'none'; + document.body.appendChild(wf_popup); + + const wf_modalDialog = document.createElement('div'); + wf_modalDialog.className = 'modal-dialog'; + wf_modalDialog.setAttribute('role', 'document'); + wf_popup.appendChild(wf_modalDialog); + + const wf_modalContent = document.createElement('div'); + wf_modalContent.className = 'modal-content'; + wf_modalDialog.appendChild(wf_modalContent); + + const wf_modalHeader = document.createElement('div'); + wf_modalHeader.className = 'modal-header'; + wf_modalContent.appendChild(wf_modalHeader); + + const wf_modalTitle = document.createElement('h5'); + wf_modalTitle.className = 'modal-title'; + wf_modalTitle.textContent = formName; + wf_modalHeader.appendChild(wf_modalTitle); + + const wf_popupBody = document.createElement('div'); + wf_popupBody.id = 'wf_popup-body'; + wf_popupBody.className = 'modal-body'; + wf_modalContent.appendChild(wf_popupBody); + + const wf_iframe = document.createElement('iframe'); + wf_iframe.id = `${formNameFancyBox}-frame`; + wf_iframe.src = `${CABINET_HOST}/widget/form/${wfId}?${query}`; + wf_iframe.style.cssText = wfStyle || ''; + wf_popupBody.appendChild(wf_iframe); + + return true; +} + +function getSessionId() { + try { + const calltrackingParams = window.ct?.('calltracking_params'); + if (Array.isArray(calltrackingParams) && calltrackingParams.length > 0) { + console.error('getting sessionId:', calltrackingParams[0]?.sessionId); + return calltrackingParams[0]?.sessionId || null; + } + return null; + } catch (e) { + console.error('Error getting sessionId:', e); + return null; + } +} + +function wf_listiner(wf_evn) { + try { + const target = wf_evn.target; + if (!target.dataset) return; + + // Базовые параметры + const baseQuery = { + 'ref': btoa(location.origin), + 'sessionId': getSessionId(), + 'ymNum': target.dataset?.ymNum, + 'ymId': target.dataset?.ymId + }; + + // Дополнительные параметры из data-row + let additionalParams = {}; + if (target.dataset.row) { + try { + additionalParams = JSON.parse(atob(target.dataset.row)); + } catch (e) { + console.error('Error parsing dataset.row:', e); + } + } + + // Объединяем параметры + const mergedQuery = {...additionalParams, ...baseQuery}; + const queryString = new URLSearchParams(mergedQuery).toString(); + + // Вызываем рендер модального окна + wf_renderModal( + target.dataset.formName, + queryString, + target.dataset.wfId, + target.dataset.fbStyle, + target.dataset.wfStyle + ); + } catch (error) { + console.error('Error in wf_listiner:', error); + } +} + +// Инициализация слушателей событий +document.querySelectorAll(buttonClass).forEach(button => { + button.addEventListener('click', wf_listiner); +}); \ No newline at end of file diff --git a/public/widgets/wf_cabinet.md b/public/widgets/wf_cabinet.md new file mode 100644 index 0000000..8e44831 --- /dev/null +++ b/public/widgets/wf_cabinet.md @@ -0,0 +1,16 @@ + + "Название услуги или ФИО врача", // если удалить, то будет отображаться поле в форме. + "fields[OPPORTUNITY]" => "990" // стоимость услуги, если не надо передавать, то удалить, либо 0 +])); ?> + + + + + + diff --git a/public/widgets/wf_cabinet.min.js b/public/widgets/wf_cabinet.min.js new file mode 100644 index 0000000..bb3aa1f --- /dev/null +++ b/public/widgets/wf_cabinet.min.js @@ -0,0 +1 @@ +console.log("wf form load");const currentScript=document.currentScript,formNameFancyBox=currentScript.dataset.formNameFancyBox,buttonClass=currentScript.dataset.buttonClass||".wf-button",CABINET_HOST="wmtmed.ru"===location.host?"https://cabinet.wmtmed.ru":"https://cabinet.sovamed.ru";function wf_renderModal(e,t,n,o,a){const r=document.getElementById(formNameFancyBox);r&&r.remove();const s=document.createElement("div");s.id=formNameFancyBox,s.className="fancybox-content",s.style.cssText=o||"",s.style.display="none",document.body.appendChild(s);const c=document.createElement("div");c.className="modal-dialog",c.setAttribute("role","document"),s.appendChild(c);const d=document.createElement("div");d.className="modal-content",c.appendChild(d);const l=document.createElement("div");l.className="modal-header",d.appendChild(l);const m=document.createElement("h5");m.className="modal-title",m.textContent=e,l.appendChild(m);const i=document.createElement("div");i.id="wf_popup-body",i.className="modal-body",d.appendChild(i);const u=document.createElement("iframe");return u.id=`${formNameFancyBox}-frame`,u.src=`${CABINET_HOST}/widget/form/${n}?${t}`,u.style.cssText=a||"",i.appendChild(u),!0}function getSessionId(){try{const e=window.ct?.("calltracking_params");return Array.isArray(e)&&e.length>0?(console.error("getting sessionId:",e[0]?.sessionId),e[0]?.sessionId||null):null}catch(e){return console.error("Error getting sessionId:",e),null}}function wf_listiner(e){try{const t=e.target;if(!t.dataset)return;const n={ref:btoa(location.origin),sessionId:getSessionId(),ymNum:t.dataset?.ymNum,ymId:t.dataset?.ymId};let o={};if(t.dataset.row)try{o=JSON.parse(atob(t.dataset.row))}catch(e){console.error("Error parsing dataset.row:",e)}const a={...o,...n},r=new URLSearchParams(a).toString();wf_renderModal(t.dataset.formName,r,t.dataset.wfId,t.dataset.fbStyle,t.dataset.wfStyle)}catch(e){console.error("Error in wf_listiner:",e)}}document.querySelectorAll(buttonClass).forEach((e=>{e.addEventListener("click",wf_listiner)})); \ No newline at end of file diff --git a/src/Bundle/Bitrix/Request.php b/src/Bundle/Bitrix/Request.php new file mode 100644 index 0000000..d6c260c --- /dev/null +++ b/src/Bundle/Bitrix/Request.php @@ -0,0 +1,317 @@ +connect = DriverManager::getConnection([ + 'url' => $_ENV['DATABASE_BITRIX_URL'] + ]); + + self::$instance->connect->executeQuery("SET NAMES utf8"); + + if ($regionId == NULL) { + self::$instance->regionId = Region::getId(); + } else { + self::$instance->regionId = $regionId; + } + } + + return self::$instance; + } + + public function setRegionId($id) + { + $this->regionId = $id; + + return $this; + } + + public function getPropPriceId($param = 'value') + { + $props = [ + '91' => [ + 'value' => 2980, + 'name' => 'Саратов' + ], + 'comfort' => [ + 'value' => 2980, + 'name' => 'Саратов Comfort' + ], + '93' => [ + 'value' => 2985, + 'name' => 'Воронеж' + ], + '92' => [ + 'value' => 2990, + 'name' => 'Волгоград' + ], + '94' => [ + 'value' => 4477, + 'name' => 'Краснодар' + ], + + ]; + + return $props[$this->regionId][$param]; + } + + public function getBlockId($param = 'value') + { + $iblock_ids = [ + 'sovenok' => [ + 'value' => 174, + 'uslugi' => 175, + 'name' => 'Саратов Совенок' + ], + 'comfort' => [ + 'value' => 145, + 'uslugi' => 146, + 'name' => 'Саратов Comfort' + ], + '91' => [ + 'value' => 91, + 'uslugi' => 165, + 'name' => 'Саратов' + ], + '92' => [ + 'value' => 92, + 'uslugi' => 167, + 'name' => 'Волгоград' + ], + '93' => [ + 'value' => 93, + 'uslugi' => 166, + 'name' => 'Воронеж' + ], + '94' => [ + 'value' => 219, + 'uslugi' => 229, + 'name' => 'Краснодар' + ], + ]; + + return $iblock_ids[$this->regionId][$param]; + } + + public function getDoctors($isActive = false) + { + $specialist = $this->connect->createQueryBuilder() + ->select('*') + ->from('`b_iblock_element`', 'el') + ->where('el.IBLOCK_ID = :IBLOCK_ID') + ->andWhere('el.WF_PARENT_ELEMENT_ID IS NULL') + ->andWhere('el.PREVIEW_PICTURE IS NOT NULL') + ->setParameter('IBLOCK_ID', $this->getBlockId()); + + if ($isActive) { + $specialist + ->andWhere('el.ACTIVE = :ACTIVE') + ->setParameter('ACTIVE', 'Y'); + } + + $specialist = $specialist + ->execute() + ->fetchAll() + ; + + return $specialist; + } + + public function getSpecialist($sid, $infoclinica = false) + { + $specialist = $this->connect->createQueryBuilder() + ->select('*') + ->from('`b_iblock_element`', 'el'); + + if ($infoclinica) { + $specialist + ->where('el.XML_ID = :XML_ID') + ->setParameter('XML_ID', $sid); + } else { + $specialist + ->where('el.ID = :ID') + ->setParameter('ID', $sid); + } + + $specialist = $specialist + ->execute() + ->fetch() + ; + + if ($specialist) { + $specialist['NAME'] = explode(' ', trim($specialist['NAME'])); + } + + return $specialist; + } + + + // SELECT biep.IBLOCK_ELEMENT_ID as DOCTOR_ID FROM b_iblock_element_property biep LEFT JOIN b_iblock_property bip ON biep.IBLOCK_PROPERTY_ID = bip.ID WHERE biep.VALUE = $doctorId AND bip.CODE REGEXP 'MEDIC' AND bip.ACTIVE = 'Y' + + // SELECT biep.VALUE, bip.NAME, bip.CODE FROM b_iblock_element bie INNER JOIN b_iblock_element_property biep ON biep.IBLOCK_ELEMENT_ID = bie.ID LEFT JOIN b_iblock_property bip ON biep.IBLOCK_PROPERTY_ID = bip.ID WHERE bie.ID = $itemID; + public function getReviews($doctorId, $infoclinica) + { + $specialist = $this->getSpecialist($doctorId, $infoclinica); + + if (! $specialist) { + return []; + } + + $items = $this->connect->createQueryBuilder() + ->select('biep.IBLOCK_ELEMENT_ID as REVIEW_ID') + ->from('b_iblock_element_property', 'biep') + ->leftJoin('biep', 'b_iblock_property', 'bip', 'biep.IBLOCK_PROPERTY_ID = bip.ID') + ->where('biep.VALUE = :VALUE') + ->andWhere('bip.CODE REGEXP :CODE') + ->setParameter('VALUE', $specialist['ID']) + ->setParameter('CODE', 'MEDIC') + ->execute() + ->fetchAll() + ; + + foreach ($this->docPropsCode($specialist['ID'], 'LINK_REVIEWS') as $item) { + $items[]['REVIEW_ID'] = $item['VALUE']; + } + + if (! empty($items)) { + foreach ($items as $key => $item) { + $items[$key]['DATA'] = $this->docPropsCode($item['REVIEW_ID']); + + foreach ($items[$key]['DATA'] as $i => $props) { + if ($props['CODE'] == 'MESSAGE') { + $data = preg_replace_callback('!s:(\d+):"(.*?)";!s', function($m) { + $len = strlen($m[2]); + $result = "s:$len:\"{$m[2]}\";"; + return $result; + }, $props['VALUE']); + + if (@unserialize($data) !== false) { + $items[$key]['DATA'][$i]['VALUE'] = unserialize($data)['TEXT']; + } + } + } + } + } + + return $items; + } + + // select biep.ID, bie.ACTIVE, bie.IBLOCK_SECTION_ID, bie.DATE_CREATE, biep.VALUE, bip.NAME, bip.CODE + // from b_iblock_element bie + // inner join b_iblock_element_property biep on biep.IBLOCK_ELEMENT_ID = bie.ID + // left join b_iblock_property bip on biep.IBLOCK_PROPERTY_ID = bip.ID + // where bie.ID = :ID + public function docPropsCode($elementId, $code = NULL, $all = true) + { + $qb = $this->connect->createQueryBuilder() + ->select('bie.NAME as BIE_NAME, biep.ID, bie.ACTIVE, bie.IBLOCK_SECTION_ID, bie.DATE_CREATE, biep.VALUE, bip.NAME, bip.CODE') + ->from('b_iblock_element', 'bie') + ->innerJoin('bie', 'b_iblock_element_property', 'biep', 'biep.IBLOCK_ELEMENT_ID = bie.ID') + ->leftJoin('biep', 'b_iblock_property', 'bip', 'biep.IBLOCK_PROPERTY_ID = bip.ID') + ->where('bie.ID = :ID') + ->setParameter('ID', $elementId); + + if ($code) { + $qb->andWhere('bip.CODE = :CODE') + ->setParameter('CODE', $code); + } + + if ($all) { + return $qb->execute()->fetchAll(); + } + + return $qb->execute()->fetch(); + } + + public function getImage($id) + { + $data = $this->connect->createQueryBuilder() + ->select('*') + ->from('b_file', 'f') + ->where('f.ID = :ID') + ->setParameter('ID', $id) + ->execute() + ->fetch() + ; + + if (empty($data['FILE_NAME'])) { + $response = '/images/no_img.png'; + } else { + $response = 'https://sovamed.ru/upload/'. $data['SUBDIR']; + $response .= '/'. $data['FILE_NAME']; + } + + return $response; + } + +// SELECT * FROM `b_iblock_section` WHERE ACTIVE = 'Y' AND IBLOCK_SECTION_ID IS NULL AND (IBLOCK_ID = 165 OR IBLOCK_ID = 166 OR IBLOCK_ID = 167); + public function getFilials() + { + return $this->connect->createQueryBuilder() + ->select('ID, NAME, IBLOCK_ID, IBLOCK_SECTION_ID, ACTIVE') + ->from('`b_iblock_section`', 'el') + ->where('el.ACTIVE = :ACTIVE') + ->andWhere('el.IBLOCK_ID = :IBLOCK_ID') + ->andWhere('el.IBLOCK_SECTION_ID IS NULL') + ->setParameter('ACTIVE', 'Y') + ->setParameter('IBLOCK_ID', $this->getBlockId('uslugi')) + ->execute() + ->fetchAll() + ; + } + + public function getDepartments($sectionId) + { + try { + return $this->connect->createQueryBuilder() + ->select('ID, NAME, IBLOCK_ID, IBLOCK_SECTION_ID, ACTIVE') + ->from('`b_iblock_section`', 'el') + ->where('el.ACTIVE = :ACTIVE') + ->andWhere('el.IBLOCK_ID = :IBLOCK_ID') + ->andWhere('el.IBLOCK_SECTION_ID = :IBLOCK_SECTION_ID') + ->setParameter('ACTIVE', 'Y') + ->setParameter('IBLOCK_SECTION_ID', $sectionId) + ->setParameter('IBLOCK_ID', $this->getBlockId('uslugi')) + ->execute() + ->fetchAll() + ; + } catch (Exception $e) { + return false; + } + } + + public function getServices($sectionId) + { + return $this->connect->createQueryBuilder() + ->select('el.ID, el.NAME, el.IBLOCK_ID, el.IBLOCK_SECTION_ID, el.ACTIVE, prop.VALUE') + ->from('`b_iblock_element`', 'el') + ->leftJoin('el', 'b_iblock_element_property', 'prop', + 'prop.IBLOCK_ELEMENT_ID = el.ID AND prop.IBLOCK_PROPERTY_ID = IBLOCK_PROPERTY_ID' + ) + ->where('el.ACTIVE = :ACTIVE') + ->andWhere('el.IBLOCK_ID = :IBLOCK_ID') + ->andWhere('el.IBLOCK_SECTION_ID = :IBLOCK_SECTION_ID') + ->setParameter('ACTIVE', 'Y') + ->setParameter('IBLOCK_SECTION_ID', $sectionId) + ->setParameter('IBLOCK_PROPERTY_ID', $this->getPropPriceId()) + ->setParameter('IBLOCK_ID', $this->getBlockId('uslugi')) + ->execute() + ->fetchAll() + ; + } +} \ No newline at end of file diff --git a/src/Bundle/Calltouch/Request.php b/src/Bundle/Calltouch/Request.php new file mode 100644 index 0000000..112e372 --- /dev/null +++ b/src/Bundle/Calltouch/Request.php @@ -0,0 +1,188 @@ +client = Client::init('https://api.calltouch.ru/'); + + $this->changeRegion(Region::getId()); + } + + public function changeRegion($regionId) + { + switch ($regionId) { + case '92': + $this->clientApiId = 'iaCkzrxUYemoqAactN83KsOUPP6IU4QO89Q9XQHwjKZsw'; + $this->clientSiteId = '17285'; + $this->client + ->setHeader('Access-Token', $this->clientApiId) + ->setHeader('SiteId', $this->clientSiteId); + return $this; + + // Волгоград + // volgograd.sovamed.ru + + // site_id - 17285 + // mod_id - 886a1412 + // ID личного кабинета 17285 + // Токен iaCkzrxUYemoqAactN83KsOUPP6IU4QO89Q9XQHwjKZsw + // ID счетчика (mod_id) 886a1412 + + break; + + case '93': + $this->clientApiId = 'mNgA1yKdoJD2ZESfX2v4gFB/6mrgGnIOsbs7ICR7cuXC6'; + $this->clientSiteId = '16650'; + $this->client + ->setHeader('Access-Token', $this->clientApiId) + ->setHeader('SiteId', $this->clientSiteId); + return $this; + + // Воронеж + // voronezh.sovamed.ru: + // site_id - 16650 + // mod_id - ce64c813 + // ID личного кабинета 16650 + // Токен mNgA1yKdoJD2ZESfX2v4gFB/6mrgGnIOsbs7ICR7cuXC6 + // ID счетчика (mod_id) ce64c813 + break; + + case '94': + $this->clientApiId = 'WAh7BnATs/6KBcsblRiUijb.U0lmiUX.b6yL7TR7GSVps'; + $this->clientSiteId = '41767'; + $this->client + ->setHeader('Access-Token', $this->clientApiId) + ->setHeader('SiteId', $this->clientSiteId); + return $this; + + // Краснодар + // wmtmed.ru + // site_id - 41767 + // mod_id - yde3clnb + // ID личного кабинета 41767 + // Токен WAh7BnATs/6KBcsblRiUijb.U0lmiUX.b6yL7TR7GSVps + // ID счетчика (mod_id) yde3clnb + break; + + case '95': + $this->clientApiId = 'yJbKffJdPo0nt0aG5GdrEeZB8g90XWAOjbnB9PZ8qUHBq'; + $this->clientSiteId = '52294'; + $this->client + ->setHeader('Access-Token', $this->clientApiId) + ->setHeader('SiteId', $this->clientSiteId); + return $this; + + // Саратов + // sovenok.sovamed.ru + // site_id - 52294 + // mod_id - e9bu10ds + // ID личного кабинета 52294 + // Токен yJbKffJdPo0nt0aG5GdrEeZB8g90XWAOjbnB9PZ8qUHBq + // ID счетчика (mod_id) e9bu10ds + break; + + case '96': + $this->clientApiId = 'ekz0aK2iVJHNpbupQmWUYiIGZi9Be9ReHpBAXktxeuRPm'; + $this->clientSiteId = '43907'; + $this->client + ->setHeader('Access-Token', $this->clientApiId) + ->setHeader('SiteId', $this->clientSiteId); + return $this; + + // Комфорт + // comfort.sovamed.ru + // ID личного кабинета (site_id) + // 43907 + // ID счетчика (mod_id) + // 4p0md55a + // API токен (access_token) + // ekz0aK2iVJHNpbupQmWUYiIGZi9Be9ReHpBAXktxeuRPm + break; + + default: + $this->clientApiId = 'eMpN4lhkNVQ/js63VpbUh/fIjcCxooEuGh2pOYictz5ZO'; + $this->clientSiteId = '16205'; + $this->client + ->setHeader('Access-Token', $this->clientApiId) + ->setHeader('SiteId', $this->clientSiteId); + return $this; + + // Саратов + // sovamed.ru: + // site_id - 16205 + // mod_id - 95271cfe + // ID личного кабинета 16205 + // Токен eMpN4lhkNVQ/js63VpbUh/fIjcCxooEuGh2pOYictz5ZO + // ID счетчика (mod_id) 95271cfe + break; + } + } + + public function create($requests) + { + $data = $this->client->setBodyParams(['requests' => [$requests]]) + ->setPath('/lead-service/v1/api/request/create') + ->exec() + ->dataResponse; + + return $data; + } + + + public function selectcCalls($params = []) + { + $path = '/calls-service/RestAPI/'. $this->clientSiteId .'/calls-diary/calls'; + $path .= '?clientApiId=' . $this->clientApiId; + $path .= '&dateFrom=' . $params['dateFrom']; + $path .= '&dateTo=' . $params['dateTo']; + $path .= '&withCallTags=true'; + + if (isset($params['withMapVisits'])) { + $path .= '&withMapVisits=' . $params['withMapVisits']; + } + + if (isset($params['withYandexDirect'])) { + $path .= '&withYandexDirect=' . $params['withYandexDirect']; + } + + $path .= '&page=1&limit=500'; + $data = $this->client + ->clearParams() + ->setPath($path) + ->exec()->dataResponse; + + return $data; + } + + public function selectRequests($params = []) + { + $path = '/calls-service/RestAPI/requests/'; + $path .= '?clientApiId=' . $this->clientApiId; + $path .= '&dateFrom=' . $params['dateFrom']; + $path .= '&dateTo=' . $params['dateTo']; + + if (isset($params['withMapVisits'])) { + $path .= '&withMapVisits=' . $params['withMapVisits']; + } + + if (isset($params['withYandexDirect'])) { + $path .= '&withYandexDirect=' . $params['withYandexDirect']; + } + + $data = $this->client + ->clearParams() + ->setPath($path) + ->exec()->dataResponse; + + return $data; + } +} diff --git a/src/Bundle/Crypt/AES.php b/src/Bundle/Crypt/AES.php new file mode 100644 index 0000000..acbf712 --- /dev/null +++ b/src/Bundle/Crypt/AES.php @@ -0,0 +1,38 @@ +morph(...) + */ + public function num2str($num) { + $nul='ноль'; + $ten=array( + array('','один','два','три','четыре','пять','шесть','семь', 'восемь','девять'), + array('','одна','две','три','четыре','пять','шесть','семь', 'восемь','девять'), + ); + $a20=array('десять','одиннадцать','двенадцать','тринадцать','четырнадцать' ,'пятнадцать','шестнадцать','семнадцать','восемнадцать','девятнадцать'); + $tens=array(2=>'двадцать','тридцать','сорок','пятьдесят','шестьдесят','семьдесят' ,'восемьдесят','девяносто'); + $hundred=array('','сто','двести','триста','четыреста','пятьсот','шестьсот', 'семьсот','восемьсот','девятьсот'); + $unit=array( // Units + array('копейка' ,'копейки' ,'копеек', 1), + array('рубль' ,'рубля' ,'рублей' ,0), + array('тысяча' ,'тысячи' ,'тысяч' ,1), + array('миллион' ,'миллиона','миллионов' ,0), + array('миллиард','милиарда','миллиардов',0), + ); + // + list($rub,$kop) = explode('.',sprintf("%015.2f", floatval($num))); + $out = array(); + if (intval($rub)>0) { + foreach(str_split($rub,3) as $uk=>$v) { // by 3 symbols + if (!intval($v)) continue; + $uk = sizeof($unit)-$uk-1; // unit key + $gender = $unit[$uk][3]; + list($i1,$i2,$i3) = array_map('intval',str_split($v,1)); + // mega-logic + $out[] = $hundred[$i1]; # 1xx-9xx + if ($i2>1) $out[]= $tens[$i2].' '.$ten[$gender][$i3]; # 20-99 + else $out[]= $i2>0 ? $a20[$i3] : $ten[$gender][$i3]; # 10-19 | 1-9 + // units without rub & kop + if ($uk>1) $out[]= $this->morph($v,$unit[$uk][0],$unit[$uk][1],$unit[$uk][2]); + } //foreach + } + else $out[] = $nul; + $out[] = $this->morph(intval($rub), $unit[1][0],$unit[1][1],$unit[1][2]); // rub + $out[] = $kop.' '.$this->morph($kop,$unit[0][0],$unit[0][1],$unit[0][2]); // kop + return trim(preg_replace('/ {2,}/', ' ', join(' ',$out))); + } + + /** + * Склоняем словоформу + * @ author runcore + */ + private function morph($n, $f1, $f2, $f5) { + $n = abs(intval($n)) % 100; + if ($n>10 && $n<20) return $f5; + $n = $n % 10; + if ($n>1 && $n<5) return $f2; + if ($n==1) return $f1; + return $f5; + } +} \ No newline at end of file diff --git a/src/Bundle/Infoclinica/.gitignore b/src/Bundle/Infoclinica/.gitignore new file mode 100644 index 0000000..17301ef --- /dev/null +++ b/src/Bundle/Infoclinica/.gitignore @@ -0,0 +1 @@ +/*.pem \ No newline at end of file diff --git a/src/Bundle/Infoclinica/Client.php b/src/Bundle/Infoclinica/Client.php new file mode 100644 index 0000000..4c1b483 --- /dev/null +++ b/src/Bundle/Infoclinica/Client.php @@ -0,0 +1,188 @@ + + */ +class Client +{ + public static $instance = NULL; + private $cookieJar = NULL; + private $client = NULL; + private $baseUri = NULL; + private $path = NULL; + public $dataResponse = []; + private $method = 'GET'; + public $params = [ + 'debug' => false, + 'verify' => false, + 'http_errors' => false, + 'allow_redirects' => [ + 'max' => 10, + 'strict' => true, + 'referer' => true, + 'track_redirects' => true, + ], + ]; + + + /** + * @return type $instance + */ + public static function init($baseUri = 'https://widget.sovamed.ru') + { + if (self::$instance === null) { + self::$instance = new self(); + self::$instance->setBaseUri($baseUri); + + $request = SymfonyRequest::createFromGlobals(); + + /*$cookieJar = CookieJar::fromArray([ + 'WR_SESSION' => $request->cookies->get('WR_SESSION'), + 'WR_DETAIL' => $request->cookies->get('WR_DETAIL'), + 'PLAY_SESSION' => $request->cookies->get('PLAY_SESSION'), + ], '.sovamed.ru'); + */ + // self::$instance->params['cookies'] = $cookieJar; + self::$instance->client = new gzClient([ + 'cookies' => false, + 'base_uri' => self::$instance->baseUri, + 'timeout' => 60, + 'headers' => [ + 'Origin' => self::$instance->baseUri, + 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:66.0) Gecko/20100101 Firefox/66.0', + 'Accept-Language' => 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3', + 'Accept' => 'application/json, text/javascript, */*; q=0.01', + 'Content-Type' => 'application/json; charset=UTF-8', + 'X-Requested-With' => 'XMLHttpRequest', + 'X-Integration-Type' => 'WEBSDK' + ] + ]); + } + + return self::$instance; + } + + public function setAuth($username, $password) + { + $this->params['headers']['Authorization'] = 'Basic ' . base64_encode("$username:$password"); + return $this; + } + + public function setHeader($key, $value) + { + $this->params['headers'][$key] = $value; + return $this; + } + + public function setBaseUri($url) + { + $this->baseUri = $url; + return $this; + } + + /** + * @param type $params + */ + public function setParams($params) + { + $this->params = $params; + $this->method = 'POST'; + return $this; + } + + /** + * @param type $formParams + */ + public function setBodyParams($formParams) + { + $this->params['body'] = json_encode($formParams); + $this->method = 'POST'; + return $this; + } + + public function clearParams() + { + unset($this->params['form_params']); + unset($this->params['body']); + $this->method = 'GET'; + return $this; + } + + public function setMethod($method) + { + $this->method = $method; + return $this; + } + + /** + * @return type + */ + public function setFormParams($params = []) + { + $this->params['form_params'] = $params; + $this->method = 'POST'; + return $this; + } + + /** + * @param type $path + */ + public function setPath($path) + { + $this->path = $path; + return $this; + } + + /** + * @return type string + */ + public function exec() + { + + $response = $this->client->request($this->method, $this->path, $this->params); + + // if (in_array($this->path, ['/registration', '/login'])) { + // if (!empty($cookie = $this->params['cookies']->getCookieByName('WR_SESSION'))) + // $this->dataResponse['cookies']['WR_SESSION'] = $cookie->getValue(); + + // if (!empty($cookie = $this->params['cookies']->getCookieByName('WR_DETAIL'))) + // $this->dataResponse['cookies']['WR_DETAIL'] = $cookie->getValue(); + + // if (!empty($cookie = $this->params['cookies']->getCookieByName('PLAY_SESSION'))) + // $this->dataResponse['cookies']['PLAY_SESSION'] = $cookie->getValue(); + + // if (!empty($cookie = $this->params['cookies']->getCookieByName('PLAY_FLASH'))) + // $this->dataResponse['cookies']['PLAY_FLASH'] = $cookie->getValue(); + // } + + $this->dataResponse['response'] = json_decode($response->getBody()->getContents(), true); + + return $this; + } + + public function raw($body = []) + { + $jar = new \GuzzleHttp\Cookie\CookieJar; + + $response = $this->client->post($this->baseUri . $this->path, [ + 'cookies' => $jar, + 'headers' => [ + 'User-Agent' => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:88.0) Gecko/20100101 Firefox/88.0', + 'Content-Type' => 'application/json', + 'X-Requested-With' => 'XMLHttpRequest', + 'X-Integration-Type' => 'WEBSDK' + ], + 'body' => json_encode($body) + ]); + + return $response->getBody()->getContents(); + } +} diff --git a/src/Bundle/Infoclinica/Region.php b/src/Bundle/Infoclinica/Region.php new file mode 100644 index 0000000..d128eb6 --- /dev/null +++ b/src/Bundle/Infoclinica/Region.php @@ -0,0 +1,97 @@ +getHost())) { + return $request->cookies->get('region'); + } + + return 94; + } + + public static function getTemplite() + { + if (self::getId() == 94) { + return 'base_wmtmed'; + } + + return 'base'; + } +} \ No newline at end of file diff --git a/src/Bundle/Infoclinica/Rest.php b/src/Bundle/Infoclinica/Rest.php new file mode 100644 index 0000000..1cfdbca --- /dev/null +++ b/src/Bundle/Infoclinica/Rest.php @@ -0,0 +1,434 @@ +client = Client::init(); + } + + public function getRecords() + { + $path = '/records/list?'; + $path .= 'st=19900101'; + $path .= '&en=21000101'; + $path .= '&draw=1'; + $path .= '&columns[0][data]=0'; + $path .= '&columns[0][name]='; + $path .= '&columns[0][searchable]=true'; + $path .= '&columns[0][orderable]=false'; + $path .= '&columns[0][search][value]='; + $path .= '&columns[0][search][regex]=false'; + $path .= '&columns[1][data]=1'; + $path .= '&columns[1][name]='; + $path .= '&columns[1][searchable]=true'; + $path .= '&columns[1][orderable]=false'; + $path .= '&columns[1][search][value]='; + $path .= '&columns[1][search][regex]=false'; + $path .= '&columns[2][data]=filialName'; + $path .= '&columns[2][name]='; + $path .= '&columns[2][searchable]=true'; + $path .= '&columns[2][orderable]=false'; + $path .= '&columns[2][search][value]='; + $path .= '&columns[2][search][regex]=false'; + $path .= '&columns[3][data]=checkData'; + $path .= '&columns[3][name]='; + $path .= '&columns[3][searchable]=true'; + $path .= '&columns[3][orderable]=false'; + $path .= '&columns[3][search][value]='; + $path .= '&columns[3][search][regex]=false'; + $path .= '&columns[4][data]=payment'; + $path .= '&columns[4][name]='; + $path .= '&columns[4][searchable]=true'; + $path .= '&columns[4][orderable]=false'; + $path .= '&columns[4][search][value]='; + $path .= '&columns[4][search][regex]=false'; + $path .= '&columns[5][data]='; + $path .= '&columns[5][name]='; + $path .= '&columns[5][searchable]=true'; + $path .= '&columns[5][orderable]=false'; + $path .= '&columns[5][search][value]='; + $path .= '&columns[5][search][regex]=false'; + $path .= '&start=0'; + $path .= '&length=20'; + $path .= '&search[value]='; + $path .= '&search[regex]=false'; + + $data = $this->client + ->clearParams() + ->setPath($path) + ->exec()->dataResponse; + + if (! empty($data['response']['data'])) { + foreach ($data['response']['data'] as $key => $dataItem) { + $data['response']['data'][$key]['workDate'] = date('d-m-Y', strtotime($dataItem['workDate'])); + } + } + + return $data; + } + + public function deleteRecord($id, $fid) + { + $path = '/api/reservation/reserve?'; + $path .= 'reserveId='. $id; + $path .= '&filialId=' . $fid; + + $this->client + ->clearParams() + ->setMethod('DELETE') + ->setPath($path) + ->exec()->dataResponse; + } + + public function anonymousReserve($data = []) + { + $data = $this->client->setParams($data) + ->setPath('/api/reservation/anonymous-reserve') + ->exec() + ->dataResponse; + + return $data; + } + + public function getProfile($fio, $password) + { + $path = '/profile'; + + $data = $this->client->setFormParams([ + 'fio' => $fio, + 'password.password' => $password, + 'password.confirm' => $password + ])->setPath('/profile')->exec()->dataResponse; + + return $data; + } + + + public function getPayments() + { + $path = '/payments/list'; + + $data = $this->client + ->clearParams() + ->setPath($path) + ->exec()->dataResponse; + + if (! empty($data['response']['data'])) { + foreach ($data['response']['data'] as $key => $dataItem) { + $data['response']['data'][$key]['date'] = date('d-m-Y', strtotime($dataItem['date'])); + } + } + + return $data; + } + + public function getBonus() + { + $path = '/bonus'; + + return $this->client + ->clearParams() + ->setPath($path) + ->exec()->dataResponse; + } + + public function getReferrals() + { + $path = '/referrals/list'; + + return $this->client + ->clearParams() + ->setPath($path) + ->exec()->dataResponse; + } + + public function getCaseHistory() + { + $path = '/case-history/filter?st=19900101&en=' . date('Ymd'); + $data = $this->client + ->clearParams() + ->setPath($path) + ->exec()->dataResponse; + + if (! empty($data['response']['data'])) { + foreach ($data['response']['data'] as $key => $dataItem) { + $data['response']['data'][$key]['date'] = date('d-m-Y', strtotime($dataItem['date'])); + } + } + + return $data; + } + + public function reserve($data = []) + { + $data = $this->client->setBodyParams($data) + ->setPath('/api/reservation/reserve') + ->exec() + ->dataResponse; + + return $data; + } + + public function login($email, $password) + { + try { + $data = $this->client->setBodyParams([ + 'formKey' => 'pcode', + 'password' => $password, + 'username' => $email + ])->setPath('/login')->exec()->dataResponse; + + if ($data['response']['success'] == true) { + setcookie('WR_SESSION', $data['cookies']['WR_SESSION'], []); + setcookie('WR_DETAIL', $data['cookies']['WR_DETAIL'], []); + setcookie('PLAY_SESSION', $data['cookies']['PLAY_SESSION'], []); + } + + return $data; + } catch (Exception $e) { + $data['response']['success'] = false; + $data['error']['message'] = $e->getMessage(); + + return $data; + } + } + + public function loginEsia() + { + $params = [ + "authType" => 'esia', + "esiaSdk" => '1', + "newWindow" => 'true', + "redirectUrl" => "http://cabinet.sovamed.ru/" + ]; + + $data = $this->client + ->setPath('/esia/esia-login-sdk') + ->raw($params); + + return $data; + } + + public function getUser() + { + return $this->client + ->setPath('/logged-in') + ->clearParams() + ->exec() + ->dataResponse; + } + + public function logout() + { + return $this->client + ->setPath('/logout') + ->clearParams() + ->exec() + ->dataResponse; + } + + public function getFilials() + { + return $this->client + ->clearParams() + ->setPath('/filials/list') + ->exec()->dataResponse; + } + + public function getDepartments() + { + return $this->client + ->clearParams() + ->setPath('/specialists/departments') + ->exec()->dataResponse; + } + + public function getSpecialists($departments, $onlineMode = 0) + { + $path = '/specialists/doctors?departments=' . $departments; + $path .= '&onlineMode='. $onlineMode; + + return $this->client + ->clearParams() + ->setPath($path) + ->exec()->dataResponse; + } + + public function getInterval($sid, $did, $date, $fid = NULL, $onlineMode = 1) + { + if (is_array($date)) { + $st = strtotime($date['startInterval']); + $en = strtotime($date['endInterval']); + } else { + $st = strtotime('first day of this month', strtotime($date)); + $en = strtotime('last day of this month', strtotime($date)); + } + + $path = '/api/reservation/intervals?dcode='. $sid; + $path .= '&spec='. $did; + $path .= '&st='. date("Ymd", $st); + $path .= '&en='. date("Ymd", $en); + $path .= '&onlineMode=' . ($onlineMode ? 1 : 0); + + if (! empty($fid)) { + $path .= '&filialId='. $fid; + $path .= '&inFilials='. $fid; + } + + $intervals = $this->client + ->clearParams() + ->setPath($path) + ->exec()->dataResponse; + + $dataResponse = []; + if (isset($intervals['response']['data'])) { + foreach ($intervals['response']['data'] as $data) { + if (isset($data['workdates'])) { + foreach ($data['workdates'] as $key => $workdates) { + foreach ($workdates as $workdate => $item) { + $workDate = \DateTime::createFromFormat( + 'Ymd', + $workdate + ); + + $intervalKey = 0; + + for ($i=0; $i < count($item); $i++) { + foreach ($item[$i]['intervals'] as $intervaldata) { + $dataResponse[$workdate][$intervalKey]['workDate'] = $workDate; + $dataResponse[$workdate][$intervalKey]['schedident'] = $item[$i]['schedident']; + $dataResponse[$workdate][$intervalKey]['time'] = $intervaldata['time']; + $dataResponse[$workdate][$intervalKey]['isFree'] = $intervaldata['isFree']; + $dataResponse[$workdate][$intervalKey]['rNum'] = $item[$i]['rnum']; + $intervalKey++; + } + } + } + } + } + } + } + + return $dataResponse; + } + + public function getSchedule($sid, $did, $date, $fid = NULL, $onlineMode = 1) + { + if (is_array($date)) { + $st = strtotime($date['startInterval']); + $en = strtotime($date['endInterval']); + } else { + $st = strtotime('first day of this month', strtotime($date)); + $en = strtotime('last day of this month', strtotime($date)); + } + + $path = '/api/reservation/schedule?doctor='. $sid; + $path .= '&speclist='. $did; + $path .= '&onlineMode=' . ($onlineMode ? 1 : 0); + $path .= '&st='. date("Ymd", $st); + $path .= '&en='. date("Ymd", $en); + + if (! empty($fid)) { + $path .= '&filialId='. $fid; + } + + return $this->client + ->clearParams() + ->setPath($path) + ->exec()->dataResponse; + } + + public function forgot($email) + { + return $this->client + ->clearParams() + ->setPath('/forgot-password?username=' . $email) + ->exec()->dataResponse; + } + + public function changePassword($pwdToken, $smsCode, $password) + { + return $this->client->setFormParams([ + 'pwdToken' => $pwdToken, + 'code' => $smsCode, + 'password.password' => $password, + 'password.confirm' => $password + ])->setPath('/change-password-web')->exec()->dataResponse; + } + + public function register($data = []) + { + try { + $birthDate = $data['registration_form']['birthDate']['day']; + $birthDate .= '.'; + $birthDate .= $data['registration_form']['birthDate']['month']; + $birthDate .= '.'; + $birthDate .= $data['registration_form']['birthDate']['year']; + + $data = $this->client->setFormParams([ + 'birthDate' => $birthDate, + 'accept' => 'true', + 'refuseCall' => 'true', + 'refuseSms' => 'true', + 'confirmed' => '', + 'checkData' => '', + 'firstName' => $data['registration_form']['firstName'], + 'lastName' => $data['registration_form']['lastName'], + 'middleName' => $data['registration_form']['middleName'], + 'email' => $data['registration_form']['email'], + 'phone' => $data['registration_form']['phone'], + 'gender' => $data['registration_form']['gender'], + 'captcha' => $data['g-recaptcha-response'], + 'viewtype' => 1 + ])->setPath('/registration')->exec()->dataResponse; + + if ($data['response']['success'] == true) { + setcookie('PLAY_SESSION', $data['cookies']['PLAY_SESSION'], []); + } + + return $data; + } catch (\Exception $e) { + $data['response']['success'] = false; + $data['error']['message'] = $e->getMessage(); + + return $data; + } + } + + public function confirm($rToken, $smsCode, $password) + { + $data = $this->client->setFormParams([ + 'smsCode' => $smsCode, + 'password.password' => $password, + 'password.confirm' => $password + ])->setPath('/registration/confirm?rToken='. $rToken)->exec()->dataResponse; + + return $data; + } + + public function payment() + { + $params = [ + 'id' => 43229262, + 'filial' => 4, + 'amt' => '1200.00', + 'errorUrl' => "https://sova.infoclinica.ru/payments", + 'magazineId' => "5ed8acaa27734416bde98c89", + 'paymethod' => "AC", + 'status' => ['id' => 0, 'name' => "NotPaid"], + 'successurl' => "https://sova.infoclinica.ru/payments", + ]; + // var_dump($this->getUser()); + $data = $this->client + ->setPath('/payments/testApiV3') + ->setBodyParams($params) + ->exec()->dataResponse; + + return $data; + } +} \ No newline at end of file diff --git a/src/Bundle/Notisend/Request.php b/src/Bundle/Notisend/Request.php new file mode 100644 index 0000000..b5523eb --- /dev/null +++ b/src/Bundle/Notisend/Request.php @@ -0,0 +1,49 @@ +client = new \GuzzleHttp\Client( + ['base_uri' => 'https://api.notisend.ru'] + );; + } + + public function send($method = 'POST', $path = '/v1/email/messages', $params = NULL) + { + $options = [ + 'debug' => false, + 'verify' => false, + 'http_errors' => false, + 'allow_redirects' => [ + 'max' => 10, + 'strict' => true, + 'referer' => true, + 'track_redirects' => true, + ], + 'headers' => [ + 'Authorization' => 'Bearer ' . $this->token, + 'Content-Encoding' => 'UTF-8', + 'Content-Type' => 'application/json', + ] + ]; + + if ($params) { + $options['body'] = json_encode($params); + } + + $response = $this->client->request($method, $path, $options); + + return $response->getBody()->getContents(); + } + + public function getTemplates() + { + return $this->send('GET', '/v1/email/templates'); + } +} diff --git a/src/Bundle/Sms/Manager.php b/src/Bundle/Sms/Manager.php new file mode 100644 index 0000000..29637ae --- /dev/null +++ b/src/Bundle/Sms/Manager.php @@ -0,0 +1,70 @@ +client = $client; + } + + public function sendSmsSova(string $to, string $msg) + { + $response = $this->client->request('GET', '/sms/send', [ + 'verify_peer' => false, + 'verify_host' => false, + 'base_uri' => 'https://sms.ru', + 'headers' => [ + 'Content-Type' => 'application/json', + 'User-Agent' => 'sovamed_bot' + ], + 'query' => [ + 'to' => $to, + 'msg' => $msg, + 'from' => $_ENV['SMSRU_FROM_SOVAMED'], + 'json' => 1, + 'api_id' => $_ENV['SMSRU_KEY_API'], + 'test' => 0 + ], + ]); + + $response = $response->toArray(); + + Logger::send($response, 'sms.log'); + + return $response; + } + + public function sendSmsWmt(string $to, string $msg) + { + $response = $this->client->request('POST', '/v1/sms', [ + 'verify_peer' => false, + 'verify_host' => false, + 'base_uri' => 'https://api.sms4b.ru', + 'headers' => [ + 'Authorization' => $_ENV['SMS4B_TOKEN'], + 'Content-Type' => 'application/json', + 'User-Agent' => 'wmtmed_bot' + ], + 'body' => json_encode([ + 'sender' => $_ENV['SMS4B_FROM_WMTMED'], + 'messages' => [ + [ + 'number' => $to, + 'text' => $msg + ] + ] + ]), + ]); + + $response = $response->toArray(); + + Logger::send($response, 'sms4b.log'); + + return $response; + } +} \ No newline at end of file diff --git a/src/Bundle/Utils/Logger.php b/src/Bundle/Utils/Logger.php new file mode 100644 index 0000000..4bc60e6 --- /dev/null +++ b/src/Bundle/Utils/Logger.php @@ -0,0 +1,30 @@ + \date('Y-m-d H:i:s'), + 'data' => $data + ], JSON_UNESCAPED_UNICODE); + + file_put_contents($fileLog, $current); + + // $conn = $event->getConnection(); + // $conn->executeQuery("SET NAMES UTF8"); + } +} diff --git a/src/Bundle/Yandex/Direct.php b/src/Bundle/Yandex/Direct.php new file mode 100644 index 0000000..e2cdf4c --- /dev/null +++ b/src/Bundle/Yandex/Direct.php @@ -0,0 +1,428 @@ +changeRegion($regionId); + } + + public function changeRegion($regionId = false) + { + switch ($regionId) { + case 92: + // return 'Волгоград'; + $this->token = ''; + $this->login = ''; + + break; + + case 93: + // return 'Воронеж'; + $this->token = 'y0_AgAAAABUjm5UAAgI4QAAAADRC5rdh5RD9KWSSIKGNb0cDBniUfVh6KU'; + $this->login = 'e-16766240'; + + break; + + case 94: + // return 'Краснодар'; + $this->token = ''; + $this->login = ''; + + break; + + case 100: + // return 'Комфорт'; + $this->token = ''; + $this->login = ''; + + break; + + case 101: + // return 'Совёнок'; + $this->token = ''; + $this->login = ''; + + break; + + case 102: + // return 'ОМС'; + $this->token = ''; + $this->login = ''; + + break; + + default: + // return 'Саратов'; + $this->token = ''; + $this->login = ''; + + break; + } + } + + public function getError() + { + return $this->error; + } + + /** + * @param array $params + * @param string $url + * @return mixed + */ + public function Request($params = [], $url, $report = false) + { + + $headers = array( + "Authorization: Bearer $this->token", // OAuth-токен. Использование слова Bearer обязательно + "Client-Login: $this->login", // Логин клиента рекламного агентства + "Accept-Language: ru", // Язык ответных сообщений + "Content-Type: application/json; charset=utf-8" // Тип данных и кодировка запроса + ); + if ($report == true) { + $headers []= "processingMode: auto"; // Режим формирования отчета online, offline , auto + $headers []= "returnMoneyInMicros: false"; // Не выводить в отчете строку с названием отчета и диапазоном дат + $headers []= "skipReportHeader: true"; // Не выводить в отчете строку с названием отчета и диапазоном дат + $headers []= "skipColumnHeader: true"; // Не выводить в отчете строку с названиями полей + $headers []= "skipReportSummary: true"; // Не выводить в отчете строку с количеством строк статистики + } + + $body = json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + $curl = curl_init(); + + curl_setopt($curl, CURLOPT_URL, $url); + curl_setopt($curl, CURLOPT_POST, true); + curl_setopt($curl, CURLOPT_POSTFIELDS, $body); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_HEADER, true); + curl_setopt($curl, CURLINFO_HEADER_OUT, true); + curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); + + while (true) { + + $result = curl_exec($curl); + + if (!$result) { + + echo ('Ошибка cURL: '.curl_errno($curl).' - '.curl_error($curl)); + + break; + + } else { + + // Разделение HTTP-заголовков и тела ответа + $responseHeadersSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE); + $responseHeaders = substr($result, 0, $responseHeadersSize); + $responseBody = substr($result, $responseHeadersSize); + + // Получение кода состояния HTTP + $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); + + // Извлечение HTTP-заголовков ответа + // Идентификатор запроса + $requestId = preg_match('/RequestId: (\d+)/', $responseHeaders, $arr) ? $arr[1] : false; + // Рекомендуемый интервал в секундах для проверки готовности отчета + $retryIn = preg_match('/retryIn: (\d+)/', $responseHeaders, $arr) ? $arr[1] : 160; + + if ($httpCode == 400) { + + echo "Параметры запроса указаны неверно или достигнут лимит отчетов в очереди
    \n"; + echo "RequestId: {$requestId}
    \n"; + echo "JSON-код запроса:
    {$body}
    \n"; + echo "JSON-код ответа сервера:
    {$responseBody}
    \n"; + + return $responseBody; + + break; + + } elseif ($httpCode == 200) { + return $responseBody; + + break; + } elseif ($httpCode == 201) { + + echo "Отчет успешно поставлен в очередь в режиме офлайн
    \n"; + echo "Повторная отправка запроса через {$retryIn} секунд
    \n"; + echo "RequestId: {$requestId}
    \n"; + + sleep($retryIn); + + } elseif ($httpCode == 202) { + + echo "Отчет формируется в режиме offline.
    \n"; + echo "Повторная отправка запроса через {$retryIn} секунд
    \n"; + echo "RequestId: {$requestId}
    \n"; + + sleep($retryIn); + + } elseif ($httpCode == 500) { + + echo "При формировании отчета произошла ошибка. Пожалуйста, попробуйте повторить запрос позднее
    \n"; + echo "RequestId: {$requestId}
    \n"; + echo "JSON-код ответа сервера:
    \n{$responseBody}
    \n"; + + break; + + } elseif ($httpCode == 502) { + + echo "Время формирования отчета превысило серверное ограничение.
    "; + echo "Пожалуйста, попробуйте изменить параметры запроса - уменьшить период и количество запрашиваемых данных.
    "; + echo "RequestId: {$requestId}
    "; + + break; + + } else { + + echo "Произошла непредвиденная ошибка.
    "; + echo "RequestId: {$requestId}
    "; + echo "JSON-код запроса:
    {$body}
    "; + echo "JSON-код ответа сервера:
    {$responseBody}
    "; + + break; + + } + } + } + curl_close($curl); + } + + /** + * @return Array|int + */ + public function getCompanies() + { + $params = [ + 'method' => 'get', // Используемый метод сервиса Campaigns + 'params' => array( + 'SelectionCriteria' => (object)array( + 'States' => [ + 'ON', + 'OFF', + ], + + ), // Критерий отбора кампаний. Для получения всех кампаний должен быть пустым + 'FieldNames' => [ // Названия параметров, которые требуется получить + "Id", + "Name", + "State", + "Type", + ], + + ) + ]; + + return $this->response($this->Request($params, $this->url . '/campaigns')); + } + + private function response($data) + { + $data = json_decode($data, true); + + if (empty($data['error'])) { + return $data['result']; + } + + return $data['error']; + } + + public function getRegions() + { + $params = [ + "method" => "get", + "params" => [ + "DictionaryNames" => [ + //"Currencies", + //"MetroStations", + "GeoRegions", + //"TimeZones", + //"Constants", + // "AdCategories", + // "OperationSystemVersions", + // "ProductivityAssertions", + // "SupplySidePlatforms", + // "Interests", + // "AudienceCriteriaTypes", + // "AudienceDemographicProfiles", + // "AudienceInterests" + ], + ], + ]; + + return $this->response($this->Request($params, $this->url . '/dictionaries')); + } + + public function getKey($group) + { + $params = [ + 'method' => 'get', // Используемый метод сервиса Campaigns + 'params' => [ + 'SelectionCriteria' => [ + 'AdGroupIds' => $group, + ], + 'FieldNames' => [ + "Id", + "Keyword", + "State", + "Status", + "ServingStatus", + "AdGroupId", + "CampaignId", + "Bid", + "ContextBid", + "StrategyPriority", + "UserParam1", + "UserParam2", + "Productivity", + "StatisticsSearch", + "StatisticsNetwork", + ] + ] + ]; + + return $this->response($this->Request($params, $this->url . '/keywords')); + } + + public function getGroups($CampaignIds = array()) + { + $params = [ + 'method' => 'get', // Используемый метод сервиса Campaigns + 'params' => [ + 'SelectionCriteria' => [ + 'CampaignIds' => $CampaignIds, + 'Statuses' => [ + "ACCEPTED", + "DRAFT", + "MODERATION", + "PREACCEPTED", + "REJECTED" + ], + ], + 'FieldNames' => [ + "Id", + "CampaignId", + "Name", + "RegionIds", + "ServingStatus", + "Status", + "Type", + ] + ] + ]; + + return $this->response($this->Request($params, $this->url . '/adgroups')); + } + + public function getAds($CampaignIds = array()) + { + $params = [ + 'method' => 'get', // Используемый метод сервиса Campaigns + 'params' => [ + 'SelectionCriteria' => [ + 'CampaignIds' => $CampaignIds, + ], + 'FieldNames' => [ + "Id", + "AdGroupId", + "CampaignId", + "State", + "StatusClarification", + "Type", + ], + 'TextAdFieldNames' => [ + "Title", + "Title2", + "Text", + "Href", + "Mobile", + "DisplayUrlPath", + ], + ] + ]; + + return $this->response($this->Request($params, $this->url . '/ads')); + } + + public function getReport() + { + $params = [ + "params" => [ + "SelectionCriteria" => (object) [ + // 'DateFrom' => \date('Y-m-d', strtotime('-7 day')), + // 'DateTo' => \date('Y-m-d', strtotime($dateTo)) + ], + "FieldNames" => [ + "Week", + "CampaignId", + "AdGroupId", + "AdId", + "Impressions", + "Clicks", + "Cost", + "Conversions" + ], + "ReportName" => 'отчет -'.time(), + "ReportType" => "AD_PERFORMANCE_REPORT", + "DateRangeType" => "LAST_WEEK", + "Format" => "TSV", + "IncludeVAT" => "NO", + "IncludeDiscount" => "NO" + ] + ]; + + $response = $this->Request($params, $this->url . '/reports', true); + + if ($jsonResponse = json_decode($response, true)) { + return $jsonResponse['error']; + } + + $linearray = []; + + foreach(explode("\n", $response) as $key => $line ) { + if(!empty($line)) { + list( + $date, + $campaignId, + $adGroupId, + $adId, + //$LocationOfPresenceId, + $impressions, + $clicks, + $cost, + $conversions + //$AdFormat, + //$AdNetworkType, + //$Age + ) = explode("\t", $line); + + $linearray []= array( + 'Date' => $date, + 'CampaignId' => $campaignId, + 'AdGroupId' => $adGroupId, + 'AdId' => $adId, + //'LocationOfPresenceId' => $LocationOfPresenceId, + 'Impressions' => $impressions, + 'Clicks' => $clicks, + 'Cost' => $cost, + 'Conversions' => $conversions, + //'AdFormat' => $AdFormat, + //'AdNetworkType' => $AdNetworkType, + //'Age' => $Age, + ); + } + } + + return $linearray; + } +} diff --git a/src/Command/AesCommand.php b/src/Command/AesCommand.php new file mode 100644 index 0000000..ac69f9d --- /dev/null +++ b/src/Command/AesCommand.php @@ -0,0 +1,54 @@ +container = $container; + } + + protected static $defaultName = 'app:aes'; + protected static $defaultDescription = 'Add a short description for your command'; + + protected function configure(): void + { + $this + ->setDescription(self::$defaultDescription) + ->addArgument('phone', InputArgument::OPTIONAL, 'variant update table') + ->addOption('decode', null, InputOption::VALUE_OPTIONAL, 'Option description') + ; + } + + // php bin/console app:aes + protected function execute(InputInterface $input, OutputInterface $output): int + { + $entityManager = $this->container->get('doctrine')->getManager(); + $io = new SymfonyStyle($input, $output); + + $decode = $input->getOption('decode'); + $phone = $input->getArgument('phone'); + + if ($decode == true) { + $io->success('AES encrypt: ' . AES::encrypt($phone)); + } else { + $io->success('AES decrypt: ' . AES::decrypt($phone)); + } + + return Command::SUCCESS; + } +} diff --git a/src/Command/BaseCommand.php b/src/Command/BaseCommand.php new file mode 100644 index 0000000..39719af --- /dev/null +++ b/src/Command/BaseCommand.php @@ -0,0 +1,99 @@ +container = $container; + } + + protected static $defaultName = 'app:base'; + protected static $defaultDescription = 'Add a short description for your command'; + + protected function configure(): void + { + $this + ->setDescription(self::$defaultDescription) + ->addArgument('var', InputArgument::OPTIONAL, 'variant update table') + ->addOption('option1', null, InputOption::VALUE_NONE, 'Option description') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $entityManager = $this->container->get('doctrine')->getManager(); + $io = new SymfonyStyle($input, $output); + + $var = $input->getArgument('var'); + + // php bin/console app:base setAlias + if ($var == 'setAlias') { + $departments = $entityManager->getRepository(Department::class) + ->findAll(); + + foreach ($departments as $department) { + $department->setAlias($this->translit($department->getName(), '-')); + + $entityManager->persist($department); + $entityManager->flush(); + + $io->success('departament updated ' . $department->getAlias()); + } + + $specialists = $entityManager->getRepository(Specialist::class) + ->findAll(); + + foreach ($specialists as $specialist) { + $specialist->setAlias($specialist->getId() .'-'. $this->translit($specialist->getName(), '-')); + + $entityManager->persist($specialist); + $entityManager->flush(); + + $io->success('specialist updated ' . $specialist->getAlias()); + } + } + + return Command::SUCCESS; + } + + private function translit($string, $replacement = '_') { + if (empty($string)) + return $string; + + $converter = array( + 'а' => 'a', 'б' => 'b', 'в' => 'v', + 'г' => 'g', 'д' => 'd', 'е' => 'ye', + 'ё' => 'yo', 'ж' => 'zh', 'з' => 'z', + 'и' => 'i', 'й' => 'y', 'к' => 'k', + 'л' => 'l', 'м' => 'm', 'н' => 'n', + 'о' => 'o', 'п' => 'p', 'р' => 'r', + 'с' => 's', 'т' => 't', 'у' => 'u', + 'ф' => 'f', 'х' => 'kh', 'ц' => 'ts', + 'ч' => 'ch', 'ш' => 'sh', 'щ' => 'shch', + 'ь' => '', 'ы' => 'y', 'ъ' => '', + 'э' => 'e', 'ю' => 'yu', 'я' => 'ya' + ); + + $string = mb_strtolower($string); + $string = strtr($string, $converter); + $string = preg_replace('~[^-a-z0-9_]+~u', $replacement, $string); + $string = preg_replace('/\-{2,10}/m', $replacement, $string); + return trim($string, "-"); + } +} diff --git a/src/Command/DiffDoctorsCommand.php b/src/Command/DiffDoctorsCommand.php new file mode 100644 index 0000000..b04b657 --- /dev/null +++ b/src/Command/DiffDoctorsCommand.php @@ -0,0 +1,195 @@ +container = $container; + } + + protected static $defaultName = 'app:DiffDoctors'; + protected static $defaultDescription = 'различие Врачей'; + protected $io = null; + protected $bitrix = null; + protected $entityManager = null; + + protected function configure(): void + { + $this + ->setDescription(self::$defaultDescription) + ->addArgument('regionId', InputArgument::OPTIONAL, 'region id') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $infoclinica = new Rest(); + $this->entityManager = $this->container->get('doctrine')->getManager(); + $this->io = new SymfonyStyle($input, $output); + $regionId = $input->getArgument('regionId'); + $this->bitrix = (\App\Bundle\Bitrix\Request::init())->setRegionId($regionId); + $doctors = $this->bitrix->getDoctors(false); + + foreach ($doctors as $doctor) { + $params = $this->loadInfoclinicaIds($doctor); + + if($this->ioError($params['docIds'], 'INFOCLINICA_ID_DOC is NULL', $doctor['ID'])) { + $params['docIds'][] = 0; + } + + if($this->ioError($params['docDeps'], 'INFOCLINICA_ID_DEP is NULL', $doctor['ID'])) { + $params['docDeps'][] = 0; + } + + if($this->ioError($params['docFilials'], 'INFOCLINICA_ID_FILIAL is NULL', $doctor['ID'])) { + $params['docFilials'][] = 0; + } + + $this->addFile($doctor); + + if ($this->bitrix->docPropsCode($doctor['ID'], 'HIDE_TIMETABLE', false)) { + $this->io->warning('infoclinica false'); + + var_dump($this->createUpdateForiDoctor($doctor, $params)); + } else { + var_dump($this->createUpdateForiDoctor($doctor, $params)); + } + + $this->io->warning('end:' . $doctor['ID']); + } + + return Command::SUCCESS; + } + + private function addFile($data = '') + { + $file = dirname(__DIR__, 2) + . DIRECTORY_SEPARATOR .'var' + . DIRECTORY_SEPARATOR. 'report' + . DIRECTORY_SEPARATOR . \date('Y-m-d') . '.csv'; + $current = ''; + + if (file_exists($file)) { + $current = file_get_contents($file); + $current .= "\n"; + } else { + $current .= "ID;NAME;DCODE;DEPARTMENT;FILIAL;B_ACTIVE;I_ACTIVE;C_ACTIVE;\n"; + } + + $current .= $data . "\n"; + + file_put_contents($file, $current); + } + + private function createUpdateForiDoctor($doctor, $params) + { + $getIdoc = function($dcode, $deps) { + $iDoctors = []; + + foreach ($deps as $departmentId) { + $iDoctors[] = $this->entityManager->getRepository(Idoctor::class) + ->findOneBy([ + 'dcode' => $dcode, + 'departmentId' => $departmentId + ]); + } + + return $iDoctors; + }; + + foreach ($params['docIds'] as $dcode) { + foreach ($getIdoc($dcode, $params['docDeps']) as $iDoctor) { + if (!empty($iDoctor)) { + $attrs = [ + 'name' => $iDoctor->getName(), + 'dcode' => $iDoctor->getDcode(), + 'department' => $iDoctor->getDepartmentId(), + 'filial' => $iDoctor->getFilialId(), + 'onlineMode' => $iDoctor->getOnlineMode(), + 'nearestDate' => $iDoctor->getNearestDate(), + 'infoclinica' => true + ]; + + return ['doctor' => $doctor, 'params' => $params, 'attrs' => $attrs]; + } else { + $this->io->error('not found iDoctor:' . $dcode); + } + } + } + } + + private function ioError($prop, $msg, $id, $continue = true) + { + if (empty($prop[0])) { + $this->io->error($msg. ': '. $id); + + return true; + } + + return false; + } + + private function loadInfoclinicaIds($doctor) + { + $this->io->success($doctor['NAME']. ' bitrix id: '. $doctor['ID']); + $data = [ + 'docIds' => [], + 'docDeps' => [], + 'docFilials' => [] + ]; + + if (trim($doctor['XML_ID']) !== trim($doctor['ID'])) { + $data['docIds'][] = preg_replace( '/[^0-9]/', '', $doctor['XML_ID']); + } + + foreach ($this->bitrix->docPropsCode($doctor['ID'], 'INFOCLINICA_ID') as $prop) { + $data['docIds'][] = preg_replace( '/[^0-9]/', '', $prop['VALUE']); + } + + foreach ($this->bitrix->docPropsCode($doctor['ID'], 'DEPARTMENT_INFO') as $prop) { + $data['docDeps'][] = preg_replace( '/[^0-9]/', '', $prop['VALUE']); + } + + foreach ($this->bitrix->docPropsCode($doctor['ID'], 'INFOCLINICA_ID_DEP') as $prop) { + $data['docDeps'][] = preg_replace( '/[^0-9]/', '', $prop['VALUE']); + } + + foreach ($this->bitrix->docPropsCode($doctor['ID'], 'FILIAL_INFO') as $prop) { + $data['docFilials'][] = preg_replace( '/[^0-9]/', '', $prop['VALUE']); + } + + $data['docFilials'] = array_diff(array_unique($data['docFilials']), ['']); + $data['docIds'] = array_diff(array_unique($data['docIds']), ['', 0, '0', '00']); + $data['docDeps'] = array_diff(array_unique($data['docDeps']), ['']); + + $this->io->info('FILIALS_INFO:'. implode(',', $data['docFilials'])); + $this->io->info('INFOCLINICA_IDS:'. implode(',', $data['docIds'])); + $this->io->info('INFOCLINICA_ID_DEPS:'. implode(',', $data['docDeps'])); + + return $data; + } +} diff --git a/src/Command/DirectCommand.php b/src/Command/DirectCommand.php new file mode 100644 index 0000000..710e8ea --- /dev/null +++ b/src/Command/DirectCommand.php @@ -0,0 +1,152 @@ +em = $container->get('doctrine')->getManager(); + } + + protected static $defaultName = 'app:direct'; + protected static $defaultDescription = 'Add a short description for your command'; + + protected function configure(): void + { + $this + ->setDescription(self::$defaultDescription) + // ->addArgument('file', InputArgument::OPTIONAL, 'variant update table') + ->addArgument('regionId', InputArgument::OPTIONAL, 'variant update table') + ; + } + + // php bin/console app:direct vlg.csv + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->io = new SymfonyStyle($input, $output); + + // $file = $input->getArgument('file'); + $regionId = $input->getArgument('regionId'); + + $report = (new Direct($regionId))->getReport(); + + foreach ($report as $item) { + $this->addDirectReport($item); + } + + return Command::SUCCESS; + } + + private function addDirectReport($item) + { + // $directReport = $this->em->getRepository(DirectReport::class) + // ->findOneBy(['companyId' => (int) $item['1']]); + + $directReport = new DirectReport(); + + $date = \DateTime::createFromFormat( + 'Y-m-d', + \date("Y-m-d", \strtotime($item['Date'])) + ); + + $directReport + ->setDate($date) + ->setCampaignId($item['CampaignId']) + ->setAdGroupId($item['AdGroupId']) + ->setAdId($item['AdId']) + ->setImpressions($item['Impressions']) + ->setClicks($item['Clicks']) + ->setCost($item['Cost']) + ->setConversions($item['Conversions']) + ; + + $this->em->persist($directReport); + $this->em->flush(); + + $this->io->success($directReport->getId()); + } + + private function uploadCSV($csv) + { + $city = Region::getCurrentName($regionId); + + $dir = dirname(__FILE__, 3) . '/var/direct/'; + + $csv = $this->readCSV($dir. $file); + + if ($csv['success'] == true) { + + foreach ($csv['data'] as $item) { + if ((int) $item['1'] == 0) { + $this->io->warning('companyId = 0'); + + continue; + } + + $directCompany = $this->em->getRepository(DirectCompany::class) + ->findOneBy(['companyId' => (int) $item['1']]); + + if (!$directCompany) { + $directCompany = new DirectCompany; + } + + $directCompany + ->setCompanyId((int) $item['1']) + ->setName($item['0']) + ->setCity($city); + + $this->em->persist($directCompany); + $this->em->flush(); + $this->io->success('success: ' . $directCompany->getId()); + } + + } else { + $this->io->error($csv['message']); + } + } + + private function readCSV($path = '') + { + $response = ['success' => false]; + + if (! file_exists($path)) { + $response['message'] = 'csv file not exists: ' . $path; + return $response; + } + + $row = 1; + + if (($handle = fopen($path, "r")) !== FALSE) { + while (($data = fgetcsv($handle, 1000, ";")) !== FALSE) { + $num = count($data); + $row++; + + for ($c=0; $c < $num; $c++) { + $response['data'][$row][$c] = $data[$c]; + } + } + + fclose($handle); + $response['success'] = true; + } + + return $response; + } +} diff --git a/src/Command/InfoclinicaCommand.php b/src/Command/InfoclinicaCommand.php new file mode 100644 index 0000000..6cc9169 --- /dev/null +++ b/src/Command/InfoclinicaCommand.php @@ -0,0 +1,307 @@ +container = $container; + } + + protected static $defaultName = 'app:Infoclinica'; + protected static $defaultDescription = 'Add a short description for your command'; + + protected function configure(): void + { + $this + ->setDescription(self::$defaultDescription) + ->addArgument('var', InputArgument::OPTIONAL, 'variant update table') + ->addOption('option1', null, InputOption::VALUE_NONE, 'Option description') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $infoclinica = new Rest(); + $entityManager = $this->container->get('doctrine')->getManager(); + $io = new SymfonyStyle($input, $output); + + $var = $input->getArgument('var'); + + // php bin/console app:Infoclinica departaments + if ($var == 'departaments') { + $departments = $infoclinica->getDepartments(); + + if ($departments['response']['success'] == true) { + foreach ($departments['response']['data'] as $item) { + $department = $entityManager->getRepository(Department::class) + ->findOneBy(['did' => $item['id'], 'onlineMode' => $item['onlineMode']]); + + if (is_null($department)) { + $department = new Department; + } + + if (empty($item['viewInWeb'])) { + $item['viewInWeb'] = 0; + } + + $department + ->setAlias('') + ->setActive($item['viewInWeb']) + ->setDid($item['id']) + ->setName($item['name']) + ->setGroupName($item['groupName']) + ->setOnlineMode($item['onlineMode']); + + $entityManager->persist($department); + $entityManager->flush(); + $io->success('id '. $department->getId() .' updated'); + } + + } else { + $io->error('departament not updated'); + } + + // php bin/console app:Infoclinica reviews + } elseif ($var == 'reviews') { + $bitrix = Bitrix::init(); + $locations = $entityManager->getRepository(Location::class) + ->findAll(); + + foreach ($locations as $location) { + try { + $reviews = $bitrix->getReviews($location->getDcode(), $location->getSpecialist()->getInfoclinica()); + } catch (Exception $e) { + $io->error($location->getSpecialist()->getId()); + $io->error($e->getErrors()); + } + + $chunksize = count($reviews) - 1; + + foreach ($reviews as $key => $params) { + $review = $entityManager->getRepository(Review::class) + ->findOneBy(['bitrixId' => $params['REVIEW_ID']]); + + $active = ($params['DATA'][0]["ACTIVE"] == 'Y')? true: false; + + if (! $review) { + if (! $active) { + continue; + } + + $review = new Review; + } + $dateCreate = \DateTime::createFromFormat( + 'Y-m-d H:i:s', + $params['DATA'][0]["DATE_CREATE"] + ); + + $review + ->setBitrixId((int) $params['REVIEW_ID']) + ->setSpecialist($location->getSpecialist()) + ->setRating(5) + ->setMessage('') + ->setAuthor('') + ->setActive($active) + ->setDateCreate($dateCreate); + + foreach ($params['DATA'] as $data) { + if ($data['CODE'] == "NAME") { + $review->setAuthor(trim(strip_tags($data['VALUE']))); + } + + if ($data['CODE'] == "MESSAGE") { + $review->setMessage(trim(strip_tags($data['VALUE']))); + } + + if ($data['CODE'] == "SOURCE_LINK") { + $re = '/(http|ftp|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])/m'; + preg_match_all($re, $data['VALUE'], $matches, PREG_SET_ORDER, 0); + + if (! empty($matches[0][0])) { + $review->setSource($matches[0][0]); + } + } + + if ($data['CODE'] == "RATING") { + $review->setRating((int) $data['VALUE']); + } + } + + $entityManager->persist($review); + + if ($chunksize == $key) { + $entityManager->flush(); + $io->success('review in '. $review->getId(). ' updated rows: ' . $chunksize); + $chunksize = 0; + } + } + } + + // php bin/console app:Infoclinica schedules + } elseif ($var == 'schedules') { + $specialists = $entityManager->getRepository(Specialist::class) + ->findAll(); + + foreach ($specialists as $specialist) { + $fid = NULL; + + if (! empty($filial = $specialist->getFilial())) { + $fid = $filial->getFid(); + } + + $io->error('specialist id:' . $specialist->getSid() + . ' department id:' . $specialist->getDepartment()->getDid() + . ' filial id:' . $fid + . ' onlineMode:' . $specialist->getOnlineMode() + ); + + $schedules =$infoclinica->getSchedule( + $specialist->getSid(), + $specialist->getDepartment()->getDid(), + date('ymd', time()), + $fid, + $specialist->getOnlineMode() + ); + + if (! empty($schedules['response']['data'])) { + if ($schedules['response']['success'] == true) { + foreach ($schedules['response']['data'] as $data) { + foreach($data['intervals'] as $interval) { + $startInterval = \DateTime::createFromFormat( + 'H:i', + $interval['startInterval'] + ); + $endInterval = \DateTime::createFromFormat( + 'H:i', + $interval['endInterval'] + ); + + $schedule = $entityManager->getRepository(Schedule::class) + ->findOneBy([ + 'schedident' => $interval['schedident'], + 'startInterval' => $startInterval, + 'endInterval' => $endInterval + ]); + + + if (is_null($schedule)) { + $schedule = new Schedule; + } + + $workDate = \DateTime::createFromFormat( + 'Ymd', + $interval['workDate'] + ); + + $schedule + ->setSpecialist($specialist) + ->setFilial($specialist->getFilial()) + ->setDepartment($specialist->getDepartment()) + ->setSchedident($interval['schedident']) + ->setIsFree($interval['isFree']) + ->setIsAvailable($interval['isAvailable']) + ->setOnlineMode($specialist->getOnlineMode()) + ->setStartInterval($startInterval) + ->setEndInterval($endInterval) + ->setWorkDate($workDate); + + $entityManager->persist($schedule); + $entityManager->flush(); + $io->success('schedident in '. $schedule->getSchedident(). ' updated'); + } + } + } + } else { + $io->error('specialist id:' . $specialist->getSid() + . 'department id:' . $specialist->getDepartment()->getDid() + . 'filial id:' . $fid + ); + + $io->error('schedident in '. $schedule->getSchedident(). ' not updated'); + } + } + + // php bin/console app:Infoclinica intervals + } elseif ($var == 'intervals') { + $specialists = $entityManager->getRepository(Specialist::class) + ->findAll(); + + foreach ($specialists as $specialist) { + $fid = NULL; + + if (! empty($filial = $specialist->getFilial())) { + $fid = $filial->getFid(); + } + + $intervals = $infoclinica->getInterval( + $specialist->getSid(), + $specialist->getDepartment()->getDid(), + date('ymd', time()), + $fid, + $specialist->getOnlineMode() + ); + + if (! empty($intervals)) { + foreach ($intervals as $data) { + foreach ($data as $intervalData) { + $interval = $entityManager->getRepository(Interval::class) + ->findOneBy([ + 'schedident' => $intervalData['schedident'], + 'time' => $intervalData['time'], + 'workDate' => $intervalData['workDate'] + ]); + + if (is_null($interval)) { + $interval = new Interval; + } + + $interval + ->setWorkDate($intervalData['workDate']) + ->setSpecialist($specialist) + ->setDepartment($specialist->getDepartment()) + ->setSchedident($intervalData['schedident']) + ->setTime($intervalData['time']) + ->setRnum($intervalData['rNum']) + ->setIsFree($intervalData['isFree']); + + $entityManager->persist($interval); + $entityManager->flush(); + $io->success('interval in '. $interval->getId(). ' updated'); + } + } + } else { + $io->error('intervals '. $specialist->getSid(). ' in not updated'); + var_dump($intervals); + } + } + } + + return Command::SUCCESS; + } +} diff --git a/src/Command/UploadDepInfoclinicaCommand.php b/src/Command/UploadDepInfoclinicaCommand.php new file mode 100644 index 0000000..e8d10f3 --- /dev/null +++ b/src/Command/UploadDepInfoclinicaCommand.php @@ -0,0 +1,88 @@ +container = $container; + $this->client = $client; + } + + protected static $defaultName = 'app:UploadDep'; + protected static $defaultDescription = 'Обновление отделений для врачей'; + + protected function configure(): void + { + $this + ->setDescription(self::$defaultDescription) + ->addArgument('var', InputArgument::OPTIONAL, 'variant update table') + ->addOption('option1', null, InputOption::VALUE_NONE, 'Option description') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $entityManager = $this->container->get('doctrine')->getManager(); + $io = new SymfonyStyle($input, $output); + + $response = $this->client->request('GET', '/specialists/departments', [ + 'verify_peer' => false, + 'verify_host' => false, + 'base_uri' => 'https://widget.sovamed.ru', + 'headers' => [ + 'Content-Type' => 'application/json', + 'User-Agent' => 'sovamed_bot' + ], + ]); + + $response = $response->toArray(); + + foreach ($response['data'] as $item) { + $department = $entityManager->getRepository(Department::class) + ->findOneBy([ + 'did' => $item['id'] + ]); + + if (is_null($department)) { + $department = new Department(); + } + + if (empty($item['viewInWeb'])) { + $item['viewInWeb'] = 0; + } + + $department + ->setDid($item['id']) + ->setName($item['name']) + ->setAlias('') + ->setGroupName($item['groupName']) + ->setActive($item['viewInWeb']) + ->setOnlineMode($item['onlineMode']) + ; + + $entityManager->persist($department); + $entityManager->flush(); + + $io->success('load: '. $department->getId()); + } + + return Command::SUCCESS; + } +} diff --git a/src/Command/UploadDoctorsCommand.php b/src/Command/UploadDoctorsCommand.php new file mode 100644 index 0000000..34ef043 --- /dev/null +++ b/src/Command/UploadDoctorsCommand.php @@ -0,0 +1,339 @@ +container = $container; + } + + protected static $defaultName = 'app:UploadDoctors'; + protected static $defaultDescription = 'Обновление и загрузка Врачей'; + protected $io = null; + protected $bitrix = null; + protected $entityManager = null; + private $countPersisted; + + protected function configure(): void + { + $this + ->setDescription(self::$defaultDescription) + ->addArgument('regionId', InputArgument::OPTIONAL, 'region id') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $infoclinica = new Rest(); + $this->entityManager = $this->container->get('doctrine')->getManager(); + $this->io = new SymfonyStyle($input, $output); + $regionId = $input->getArgument('regionId'); + $this->bitrix = (\App\Bundle\Bitrix\Request::init())->setRegionId($regionId); + $doctors = $this->bitrix->getDoctors(true); + + $this->io->info('start updating:' . count($doctors)); + + foreach ($doctors as $doctor) { + $params = $this->loadInfoclinicaIds($doctor); + if($this->ioError($params['docIds'], 'INFOCLINICA_ID_DOC is NULL', $doctor['ID'])) { + $params['docIds'][] = trim($doctor['ID']); + } + + if($this->ioError($params['docDeps'], 'INFOCLINICA_ID_DEP is NULL', $doctor['ID'])) { + $params['docDeps'][] = 0; + } + + if($this->ioError($params['docFilials'], 'INFOCLINICA_ID_FILIAL is NULL', $doctor['ID'])) { + $params['docFilials'][] = 0; + } + + if ($this->bitrix->docPropsCode($doctor['ID'], 'HIDE_TIMETABLE', false)) { + $this->createUpdateForDoctor($doctor, $params); + } else { + $this->createUpdateForiDoctor($doctor, $params); + } + + } + + $this->io->info('update rows:' . $this->countPersisted); + $this->entityManager->flush(); + + return Command::SUCCESS; + } + + private function createUpdateForDoctor($doctor, $params) + { + $nearestDate = \date('d.m.Y'); + + foreach ($params['docFilials'] as $filialId) { + foreach ($params['docIds'] as $dcode) { + foreach ($params['docDeps'] as $departmentId) { + $attrs = [ + 'name' => trim($doctor['NAME']), + 'dcode' => $dcode, + 'department' => $departmentId, + 'filial' => $filialId, + 'onlineMode' => 0, + 'nearestDate' => $nearestDate, + 'infoclinica' => false + ]; + + $this->addDoctor($doctor, $params, $attrs); + } + } + } + } + + private function createUpdateForiDoctor($doctor, $params) + { + $getIdoc = function($dcode, $deps) { + $iDoctors = []; + + foreach ($deps as $departmentId) { + $iDoctors[] = $this->entityManager->getRepository(Idoctor::class) + ->findOneBy([ + 'dcode' => $dcode, + 'departmentId' => $departmentId, + 'onlineMode' => 0 + ]); + } + + return $iDoctors; + }; + + foreach ($params['docIds'] as $dcode) { + foreach ($getIdoc($dcode, $params['docDeps']) as $iDoctor) { + if (!empty($iDoctor)) { + $attrs = [ + 'name' => $iDoctor->getName(), + 'dcode' => $iDoctor->getDcode(), + 'department' => $iDoctor->getDepartmentId(), + 'filial' => $iDoctor->getFilialId(), + 'onlineMode' => $iDoctor->getOnlineMode(), + 'nearestDate' => $iDoctor->getNearestDate(), + 'infoclinica' => true + ]; + + $this->addDoctor($doctor, $params, $attrs); + } + } + } + } + + private function addDoctor($doctor, $params, $attrs) + { + $active = ($doctor['ACTIVE'] == 'Y')? true : false; + + if (empty($attrs['department']) || empty($attrs['filial'])) { + $active = false; + } + + $filial = $this->entityManager->getRepository(Filial::class) + ->findOneBy(['fid' => $attrs['filial']]); + + if (is_null($filial)) { + $filial = $this->entityManager->getRepository(Filial::class) + ->findOneBy(['fid' => 0]); + } + + $department = $this->entityManager->getRepository(Department::class) + ->findOneBy(['did' => $attrs['department']]); + + if (is_null($department)) { + $department = $this->entityManager->getRepository(Department::class) + ->findOneBy(['did' => 0]); + } + + $locationSearch = ['dcode' => $attrs['dcode']]; + $locationSearch['department'] = $department; + $locationSearch['filial'] = $filial; + + $locationEntity = $this->entityManager->getRepository(Location::class) + ->findOneBy($locationSearch); + + if (is_null($locationEntity)) { + $locationEntity = new Location(); + } + + $specialist = $this->entityManager->getRepository(Specialist::class) + ->findByDcode($attrs['dcode']); + + if (is_null($specialist)) { + $params['docIds'][] = trim($doctor['ID']); + $specialist = new Specialist(); + $specialist->setDcode(json_encode($params['docIds'], JSON_FORCE_OBJECT)); + } else { + $dcodes = json_decode($specialist->getDcode(), true); + $dcodes[] = $doctor['ID']; + $dcodes = array_unique($dcodes); + $specialist->setDcode(json_encode($dcodes, JSON_FORCE_OBJECT)); + } + + $specialist = $this->setSpecialist($specialist, $doctor); + $nearestDate = \DateTime::createFromFormat( + 'Y-m-d', + date('Y-m-d', strtotime($attrs['nearestDate'])) + ); + + $specialist + ->setImg($doctor['ID']) + ->setNearestDate($nearestDate) + ->setInfoclinica($attrs['infoclinica']); + + $locationEntity + ->setActive($active) + ->setOnlineMode(0) + ->setDcode($attrs['dcode']) + ->setFilial($filial) + ->setDepartment($department) + ->setSpecialist($specialist); + + try { + $this->countPersisted++; + $this->entityManager->persist($locationEntity); + } catch (Exception $e) { + $this->io->error( $e->getMessage()); + } + + } + + private function setSpecialist($specialist, $doctor) + { + $specialist->setName(trim($doctor['NAME'])) + ->setAlias('') + ; + + $post = $this->bitrix->docPropsCode($doctor['ID'], 'POST', false); + + if (! empty($post['VALUE'])) { + $specialist->setSpeciality($post['VALUE']); + } + + if (! empty($doctor['DETAIL_TEXT'])) { + $specialist->setDescription($doctor['DETAIL_TEXT']); + } + + $category = $this->bitrix->docPropsCode($doctor['ID'], 'CATEGORY', false); + + if (! empty($category['VALUE'])) { + $specialist->setCategory(mb_strtolower(trim($category['VALUE']))); + } + + $kinder = $this->bitrix->docPropsCode($doctor['ID'], 'KIDS_AGE', false); + + if (! empty($kinder['VALUE'])) { + $specialist->setKinder((int) $kinder['VALUE']); + } + + $expirience = $this->bitrix->docPropsCode($doctor['ID'], 'EXPERIENCE', false); + + if (! empty($expirience['VALUE'])) { + if (iconv_strlen($expirience['VALUE']) == 4) { + $specialist->setExpirience(date('Y') - $expirience['VALUE']); + } else { + $specialist->setExpirience((int) $expirience['VALUE']); + } + } + + $linkPriceIds = $this->bitrix->docPropsCode($doctor['ID'], 'LINK_PRICE_1'); + + if ($linkPriceIds) { + foreach ($specialist->getPrices() as $delPrice) { + $specialist->removePrice($delPrice); + } + + foreach ($linkPriceIds as $linkPrice) { + $price = $this->bitrix->docPropsCode($linkPrice['VALUE'], 'PRICE', false); + + if (! empty($price)) { + $priceEntity = $this->entityManager->getRepository(Price::class) + ->findOneBy(['propertyValueId' => (int) $price['ID']]); + + if (! $priceEntity) { + $priceEntity = new Price(); + } + + if (! empty($price['BIE_NAME'])) { + $priceEntity + ->setName(trim($price['BIE_NAME'])) + ->setValue((int) str_replace(',', '.', $price['VALUE'])) + ->setDateUpdate(new \DateTime()) + ->setPropertyValueId($price['ID'] . '.' . $doctor['ID']); + + $specialist->addPrice($priceEntity); + } + } + } + } + + return $specialist; + } + + private function ioError($prop, $msg, $id, $continue = true) + { + if (empty($prop[0])) { + return true; + } + + return false; + } + + private function loadInfoclinicaIds($doctor) + { + $data = [ + 'docIds' => [], + 'docDeps' => [], + 'docFilials' => [] + ]; + + if (trim($doctor['XML_ID']) !== trim($doctor['ID'])) { + $data['docIds'][] = preg_replace( '/[^0-9]/', '', $doctor['XML_ID']); + } + + foreach ($this->bitrix->docPropsCode($doctor['ID'], 'INFOCLINICA_ID') as $prop) { + $data['docIds'][] = preg_replace( '/[^0-9]/', '', $prop['VALUE']); + } + + foreach ($this->bitrix->docPropsCode($doctor['ID'], 'DEPARTMENT_INFO') as $prop) { + $data['docDeps'][] = preg_replace( '/[^0-9]/', '', $prop['VALUE']); + } + + foreach ($this->bitrix->docPropsCode($doctor['ID'], 'INFOCLINICA_ID_DEP') as $prop) { + $data['docDeps'][] = preg_replace( '/[^0-9]/', '', $prop['VALUE']); + } + + foreach ($this->bitrix->docPropsCode($doctor['ID'], 'FILIAL_INFO') as $prop) { + $data['docFilials'][] = preg_replace( '/[^0-9]/', '', $prop['VALUE']); + } + + $data['docFilials'] = array_diff(array_unique($data['docFilials']), ['']); + $data['docIds'] = array_diff(array_unique($data['docIds']), ['', 0, '0', '00']); + $data['docDeps'] = array_diff(array_unique($data['docDeps']), ['']); + + return $data; + } +} diff --git a/src/Command/UploadDoctorsInfoclinicaCommand.php b/src/Command/UploadDoctorsInfoclinicaCommand.php new file mode 100644 index 0000000..3307a86 --- /dev/null +++ b/src/Command/UploadDoctorsInfoclinicaCommand.php @@ -0,0 +1,79 @@ +entityManager = $container->get('doctrine')->getManager(); + } + + protected function configure(): void + { + $this->setDescription(self::$defaultDescription) + ->addArgument('onlineMode', InputArgument::OPTIONAL, 'onlineMode [0,1]') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->io = new SymfonyStyle($input, $output); + $infoclinica = new Rest(); + $onlineMode = $input->getArgument('onlineMode'); + + foreach ($this->entityManager->getRepository(Department::class)->findAll() as $department) { + foreach ($infoclinica->getSpecialists($department->getDid(), $onlineMode)['response']['data'] as $doctor) { + $iDoctor = $this->entityManager->getRepository(Idoctor::class) + ->findOneBy([ + 'dcode' => $doctor['dcode'], + 'departmentId' => $doctor['departmentId'], + 'onlineMode' => $onlineMode + ]); + + if (! $iDoctor) { + $this->io->success('create in specialist '. $doctor['dcode']); + $iDoctor = new Idoctor(); + } else { + $this->io->success('upgrade in specialist '. $doctor['dcode']); + } + + $iDoctor + ->setDcode($doctor['dcode']) + ->setName($doctor['name']) + ->setDepartmentId($doctor['departmentId']) + ->setDepartmentName($doctor['departmentName']) + ->setFilialId($doctor['filialId']) + ->setFilialName($doctor['filialName']) + ->setComment(empty($doctor['comment'])? '':$doctor['comment']) + ->setNearestDate($doctor['nearestDate']) + ->setViewInWeb($doctor['viewInWeb']) + ->setOnlineMode($onlineMode) + ; + + $this->entityManager->persist($iDoctor); + } + + $this->entityManager->flush(); + } + + return Command::SUCCESS; + } +} diff --git a/src/Command/UploadDoctorsOnlineCommand.php b/src/Command/UploadDoctorsOnlineCommand.php new file mode 100644 index 0000000..87752c7 --- /dev/null +++ b/src/Command/UploadDoctorsOnlineCommand.php @@ -0,0 +1,83 @@ +entityManager = $container->get('doctrine')->getManager(); + } + + protected function configure(): void + { + $this->setDescription(self::$defaultDescription); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->io = new SymfonyStyle($input, $output); + + $doctorsOnline = $this->entityManager + ->getRepository(Idoctor::class) + ->findBy(['onlineMode' => 1]); + + foreach ($doctorsOnline as $doctor) { + $specialist = $this->entityManager->getRepository(Specialist::class) + ->findByDcode($doctor->getDcode()); + + if ($specialist) { + $locationEntity = $this->entityManager->getRepository(Location::class) + ->findOneBy([ + 'dcode' => $doctor->getDcode(), + 'department' => $doctor->getDepartmentId(), + 'filial' => $doctor->getFilialId(), + 'onlineMode' => $doctor->getOnlineMode() + ]); + + $filial = $this->entityManager->getRepository(Filial::class) + ->findOneBy(['fid' => $doctor->getFilialId()]); + $department = $this->entityManager->getRepository(Department::class) + ->findOneBy(['did' => $doctor->getDepartmentId()]); + + if (is_null($locationEntity)) { + $locationEntity = new Location(); + } + + $locationEntity + ->setDcode($doctor->getDcode()) + ->setDepartment($department) + ->setFilial($filial) + ->setSpecialist($specialist) + ->setOnlineMode($doctor->getOnlineMode()) + ->setActive(1) + ; + + $this->entityManager->persist($locationEntity); + } + } + + $this->entityManager->flush(); + + return Command::SUCCESS; + } +} diff --git a/src/Command/UploadPriceDepInfoclinicaCommand.php b/src/Command/UploadPriceDepInfoclinicaCommand.php new file mode 100644 index 0000000..773b300 --- /dev/null +++ b/src/Command/UploadPriceDepInfoclinicaCommand.php @@ -0,0 +1,87 @@ +container = $container; + $this->client = $client; + } + + protected static $defaultName = 'app:UploadPiceDep'; + protected static $defaultDescription = 'Обновление отделений для получения цен'; + + protected function configure(): void + { + $this + ->setDescription(self::$defaultDescription) + ->addArgument('var', InputArgument::OPTIONAL, 'variant update table') + ->addOption('option1', null, InputOption::VALUE_NONE, 'Option description') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $entityManager = $this->container->get('doctrine')->getManager(); + $io = new SymfonyStyle($input, $output); + + $response = $this->client->request('GET', '/pricelist/departments', [ + 'verify_peer' => false, + 'verify_host' => false, + 'base_uri' => 'https://widget.sovamed.ru', + 'headers' => [ + 'Content-Type' => 'application/json', + 'User-Agent' => 'sovamed_bot' + ], + ]); + + $response = $response->toArray(); + + foreach ($response['data'] as $item) { + $department = $entityManager->getRepository(PriceDepartment::class) + ->findOneBy([ + 'groupId' => $item['id'] + ]); + + if (is_null($department)) { + $department = new PriceDepartment(); + } + + if (empty($item['viewInWeb'])) { + $item['viewInWeb'] = 0; + } + + $department + ->setGroupId($item['id']) + ->setName($item['name']) + ->setGroupName($item['groupName']) + ->setViewInWeb($item['viewInWeb']) + ->setDoctCount($item['schCount']) + ; + + $entityManager->persist($department); + $entityManager->flush(); + + $io->success('load: '. $department->getId()); + } + + return Command::SUCCESS; + } +} diff --git a/src/Command/UploadPriceInfoclinicaCommand.php b/src/Command/UploadPriceInfoclinicaCommand.php new file mode 100644 index 0000000..e8ba60b --- /dev/null +++ b/src/Command/UploadPriceInfoclinicaCommand.php @@ -0,0 +1,152 @@ +container = $container; + $this->client = $client; + } + + protected static $defaultName = 'app:UploadPice'; + protected static $defaultDescription = 'Обновление цен'; + + protected function configure(): void + { + $this + ->setDescription(self::$defaultDescription) + ->addArgument('did', InputArgument::OPTIONAL, 'department id update') + ->addOption('debug', null, InputOption::VALUE_NONE, 'debug on') + ->addOption('nosleep', null, InputOption::VALUE_OPTIONAL, 'sleep true|false default false') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $entityManager = $this->container->get('doctrine')->getManager(); + $io = new SymfonyStyle($input, $output); + $departments = $entityManager->getRepository(PriceDepartment::class)->findAll(); + $departmentId = $input->getArgument('did'); + $debug = $input->getOption('debug'); + $nosleep = $input->getOption('nosleep'); + + if ($departmentId) { + $departments = $entityManager->getRepository(PriceDepartment::class) + ->findBy(['groupId' => $departmentId]); + } + + if (empty($departments)) + return Command::FAILURE; + + foreach ($departments as $department) { + foreach ($this->getPricelist($department->getGroupId()) as $collection) { + foreach ($collection as $item) { + $priceList = $entityManager->getRepository(PriceList::class) + ->findOneBy([ + 'kodoper' => $item['kodoper'], + 'speccode' => $item['speccode'], + 'filial' => $item['filial'], + 'groupId' => $department->getGroupId() + ]); + + $text = 'update: '; + + if (is_null($priceList)) { + $priceList = new priceList(); + $text = 'create: '; + } + + $priceList + ->setKodoper($item['kodoper']) + ->setSchname($item['schname']) + ->setSpecname($item['specname']) + ->setSpeccode($item['speccode']) + ->setPriceInfo($item['priceInfo']) + ->setDiscpercent($item['discpercent']) + ->setDiscprice($item['discprice']) + ->setStructname($item['structname']) + ->setFname($item['fname']) + ->setFilial($item['filial']) + ->setComment($item['comment']) + ->setMediaId($item['mediaId']) + ->setDateUpdate(new \DateTime()) + ->setGroupId($department->getGroupId()); + + $entityManager->persist($priceList); + $entityManager->flush(); + + if ($debug) + $io->success($text. $priceList->getId()); + } + } + + if ($debug) + $io->info('sleep: 2 sec'); + + if (empty($nosleep)) + sleep(2); + } + + $io->success('successful'); + + return Command::SUCCESS; + } + + private function getPricelist($depnum) + { + $response = []; + $flag = true; + $firstrow = 1; + $lastrow = 500; + + while ($flag) { + $result = $this->client->request('GET', 'pricelist/list', [ + 'verify_peer' => false, + 'verify_host' => false, + 'base_uri' => 'https://widget.sovamed.ru', + 'headers' => [ + 'Content-Type' => 'application/json', + 'User-Agent' => 'sovamed_bot' + ], + 'query' => [ + 'depnum' => $depnum, + 'firstrow' => $firstrow, + 'lastrow' => $lastrow + ], + ]); + + $firstrow = $lastrow + 1; + $lastrow = $lastrow + 500; + $result = $result->toArray(); + + if (empty($result['data'])) { + $flag = false; + } else { + $response[] = $result['data']; + } + + if (empty($nosleep)) + sleep(3); + } + + return $response; + } +} diff --git a/src/Command/UserCleanupCommand.php b/src/Command/UserCleanupCommand.php new file mode 100644 index 0000000..33500b8 --- /dev/null +++ b/src/Command/UserCleanupCommand.php @@ -0,0 +1,66 @@ +userCleanupService = $userCleanupService; + } + + protected static $defaultName = 'app:user:cleanup'; + protected static $defaultDescription = 'Удаляет пользователей с только ROLE_USER после истечения времени жизни кукисов'; + + protected function configure(): void + { + $this + ->setDescription(self::$defaultDescription) + ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Показать, какие пользователи будут удалены, без фактического удаления') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $dryRun = $input->getOption('dry-run'); + + if ($dryRun) { + $io->note('Режим проверки (dry-run). Пользователи не будут удалены.'); + } + + $io->title('Очистка пользователей'); + + try { + if ($dryRun) { + $io->note('Режим проверки (dry-run). Пользователи не будут удалены.'); + // В dry-run режиме просто показываем сколько будет удалено + // Но для этого нужно добавить метод в сервис или просто запустить и показать результат + } + + $deletedCount = $this->userCleanupService->cleanupExpiredUsers(); + + if ($dryRun) { + $io->success(sprintf('Найдено пользователей для удаления: %d', $deletedCount)); + } else { + $io->success(sprintf('Успешно удалено пользователей: %d', $deletedCount)); + } + + return Command::SUCCESS; + } catch (\Exception $e) { + $io->error(sprintf('Ошибка при выполнении очистки: %s', $e->getMessage())); + $io->error($e->getTraceAsString()); + return Command::FAILURE; + } + } +} diff --git a/src/Controller/.gitignore b/src/Controller/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/src/Controller/BannerController.php b/src/Controller/BannerController.php new file mode 100644 index 0000000..15650e1 --- /dev/null +++ b/src/Controller/BannerController.php @@ -0,0 +1,130 @@ +render('banner/index.html.twig', [ + 'banners' => $bannerRepository->findAll(), + ]); + } + + /** + * @Route("/new", name="banner_new", methods={"GET","POST"}) + */ + public function new(Request $request, SluggerInterface $slugger): Response + { + $banner = new Banner(); + $form = $this->createForm(BannerType::class, $banner); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $srcFile = $form->get('file')->getData(); + + if ($srcFile) { + $originalFilename = pathinfo($srcFile->getClientOriginalName(), PATHINFO_FILENAME); + + $safeFilename = $slugger->slug($originalFilename); + $newFilename = $safeFilename.'-'.uniqid().'.'.$srcFile->guessExtension(); + $srcFile->move( + $this->getParameter('banners_directory'), + $newFilename + ); + + $banner->setSrc($newFilename); + } + + $entityManager = $this->getDoctrine()->getManager(); + $entityManager->persist($banner); + $entityManager->flush(); + + return $this->redirectToRoute('admin_banner_index'); + } + + return $this->render('banner/new.html.twig', [ + 'banner' => $banner, + 'form' => $form->createView(), + ]); + } + + /** + * @Route("/{id}/edit", name="banner_edit", methods={"GET","POST"}) + */ + public function edit(Request $request, Banner $banner, SluggerInterface $slugger): Response + { + $form = $this->createForm(BannerType::class, $banner); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $file = $this->getParameter('banners_directory') . DIRECTORY_SEPARATOR . $banner->getSrc(); + + if (file_exists($file)) { + unlink($file); + } + + $srcFile = $form->get('file')->getData(); + + if ($srcFile) { + $originalFilename = pathinfo($srcFile->getClientOriginalName(), PATHINFO_FILENAME); + + $safeFilename = $slugger->slug($originalFilename); + $newFilename = $safeFilename.'-'.uniqid().'.'.$srcFile->guessExtension(); + $srcFile->move( + $this->getParameter('banners_directory'), + $newFilename + ); + + $banner->setSrc($newFilename); + } + + $this->getDoctrine()->getManager()->flush(); + + return $this->redirectToRoute('admin_banner_index'); + } + + return $this->render('banner/edit.html.twig', [ + 'banner' => $banner, + 'form' => $form->createView(), + ]); + } + + /** + * @Route("/{id}", name="banner_delete", methods={"POST"}) + */ + public function delete(Request $request, Banner $banner): Response + { + if ($this->isCsrfTokenValid('delete'.$banner->getId(), $request->request->get('_token'))) { + $file = $this->getParameter('banners_directory') . DIRECTORY_SEPARATOR . $banner->getSrc(); + + if (file_exists($file)) { + unlink($file); + } + + $entityManager = $this->getDoctrine()->getManager(); + $entityManager->remove($banner); + $entityManager->flush(); + } + + return $this->redirectToRoute('admin_banner_index'); + } +} diff --git a/src/Controller/CalltouchAPIController.php b/src/Controller/CalltouchAPIController.php new file mode 100644 index 0000000..54fbb2f --- /dev/null +++ b/src/Controller/CalltouchAPIController.php @@ -0,0 +1,65 @@ + \md5(\time()), + 'subject' => $request->request->get('subject'), + 'requestUrl' => $request->request->get('requestUrl'), + 'requestDate' => \date('d-m-Y H:i:s'), + 'fio' => $request->request->get('fio'), + ]; + + if (! empty($request->request->get('sessionId'))) { + $data['sessionId'] = $request->request->get('sessionId'); + } + + if (! empty($request->request->get('customSources'))) { + if (!empty($request->request->get('customSources')['source'])) { + $data['customSources'] = $request->request->get('customSources'); + } + } + + if (! empty($request->request->get('tag'))) { + $data['addTags'][] = ['tag' => $request->request->get('tag')]; + } + + if (! empty($request->request->get('comment'))) { + $data['comment']['text'] = json_encode($request->request->get('comment'), JSON_UNESCAPED_UNICODE); + } + + if (! empty($request->request->get('phoneNumber'))) { + $data['phoneNumber'] = $request->request->get('phoneNumber'); + } + + if (! empty($request->request->get('email'))) { + $data['email'] = $request->request->get('email'); + } + + $calltouch = new CalltouchRequest(); + + if (! empty($request->request->get('regionId'))) { + $calltouch->changeRegion($request->request->get('regionId')); + } + + $calltouch = $calltouch->create($data); + + return $this->json(['data' => $calltouch]); + } +} diff --git a/src/Controller/CategoryPageController.php b/src/Controller/CategoryPageController.php new file mode 100644 index 0000000..352e827 --- /dev/null +++ b/src/Controller/CategoryPageController.php @@ -0,0 +1,96 @@ +render('category_page/index.html.twig', [ + 'category_pages' => $categoryPageRepository->findAll(), + ]); + } + + /** + * @Route("/new", name="category_page_new", methods={"GET","POST"}) + */ + public function new(Request $request): Response + { + $categoryPage = new CategoryPage(); + $form = $this->createForm(CategoryPageType::class, $categoryPage); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $entityManager = $this->getDoctrine()->getManager(); + $entityManager->persist($categoryPage); + $entityManager->flush(); + + return $this->redirectToRoute('category_page_index'); + } + + return $this->render('category_page/new.html.twig', [ + 'category_page' => $categoryPage, + 'form' => $form->createView(), + ]); + } + + /** + * @Route("/{id}", name="category_page_show", methods={"GET"}) + */ + public function show(CategoryPage $categoryPage): Response + { + return $this->render('category_page/show.html.twig', [ + 'category_page' => $categoryPage, + ]); + } + + /** + * @Route("/{id}/edit", name="category_page_edit", methods={"GET","POST"}) + */ + public function edit(Request $request, CategoryPage $categoryPage): Response + { + $form = $this->createForm(CategoryPageType::class, $categoryPage); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->getDoctrine()->getManager()->flush(); + + return $this->redirectToRoute('category_page_index'); + } + + return $this->render('category_page/edit.html.twig', [ + 'category_page' => $categoryPage, + 'form' => $form->createView(), + ]); + } + + /** + * @Route("/{id}", name="category_page_delete", methods={"POST"}) + */ + public function delete(Request $request, CategoryPage $categoryPage): Response + { + if ($this->isCsrfTokenValid('delete'.$categoryPage->getId(), $request->request->get('_token'))) { + $entityManager = $this->getDoctrine()->getManager(); + $entityManager->remove($categoryPage); + $entityManager->flush(); + } + + return $this->redirectToRoute('category_page_index'); + } +} diff --git a/src/Controller/DefaultController.php b/src/Controller/DefaultController.php new file mode 100644 index 0000000..4285b4c --- /dev/null +++ b/src/Controller/DefaultController.php @@ -0,0 +1,245 @@ +client = $client; + $this->kernel = $kernel; + } + + /** + * @IsGranted("ROLE_USER") + * @Route("/", name="default_index") + */ + public function index(): Response + { + return $this->render('base/index.html.twig', [ + 'template' => Region::getTemplite(), + 'alias' => '', + 'title' => 'Личный кабинет' + ]); + } + + /** + * @Route("/doctor-your-home", name="default_doc_your_home") + */ + public function doctorYourHome(): Response + { + return $this->render('base/doc_your_home.html.twig', [ + 'template' => Region::getTemplite(), + 'title' => 'Вызов врача на дом' + ]); + } + + + /** + * @Route("/stoimost-uslug", name="default_price") + */ + public function price(PriceListService $priceListService, PaginatorInterface $paginator, Request $request): Response + { + $priceList = new PriceList(); + $searchForm = $this->createForm(PriceListFormType::class, $priceList, [ + 'action' => $this->generateUrl('default_price'), + 'method' => 'GET', + ]); + + $searchForm->handleRequest($request); + + $filters = $request->query->get('price_list_form', []); + $priceListQuery = $priceListService->getFilteredPriceListQuery($filters); + + $pagination = $paginator->paginate( + $priceListQuery->getQuery(), + $request->query->getInt('page', 1), + 50 + ); + + return $this->render('base/price.html.twig', [ + 'title' => 'Стоимость услуг', + 'template' => Region::getTemplite(), + 'pagination' => $pagination, + 'searchForm' => $searchForm->createView() + ]); + } + + /** + * @IsGranted("ROLE_ADMIN") + * @Route("/update/price-list", name="default_update_price_list", methods={"POST"}) + */ + public function uploadPrice(Request $request): Response + { + $application = new Application($this->kernel); + $application->setAutoExit(false); + + $input = new ArrayInput([ + 'command' => 'app:UploadPice', + 'did' => $request->request->get('groupId'), + '--nosleep' => true + ]); + + // Вы можете использовать NullOutput(), если вам не нужен вывод + $output = new BufferedOutput(); + $application->run($input, $output); + + return $this->json( + (strpos($output->fetch(), '[OK] successful') === false)? ['status' => false]: ['status' => true] + ); + } + + /** + * @IsGranted("ROLE_ADMIN") + * @Route("/price-list", name="default_price_list") + */ + public function priceList( + PriceListService $priceListService, + PriceListRepository $priceListRepository, + PaginatorInterface $paginator, + Request $request + ): Response { + $priceList = new PriceList(); + $searchForm = $this->createForm(PriceListAdminFormType::class, $priceList, [ + 'action' => $this->generateUrl('default_price_list'), + 'method' => 'GET', + ]); + $searchForm->handleRequest($request); + + $params = $request->query->get('default_price_list',[]); + + $priceListQuery = $priceListService->getPriceListQuery($params); + $pagination = $paginator->paginate( + $priceListQuery->getQuery(), + $request->query->getInt('page', 1), + 50 + ); + + return $this->render('base/price_list.html.twig', [ + 'priceList' => $priceList, + 'dateActive' => (new \DateTime())->modify('-2 day')->format('Y-m-d 00:00:00'), + 'title' => 'Сравнение цен', + 'template' => Region::getTemplite(), + 'pagination' => $pagination, + 'searchForm' => $searchForm->createView() + ]); + } + + private function getPricelist($depnum) + { + $response = []; + $flag = true; + $firstrow = 1; + $lastrow = 500; + + while ($flag) { + $result = $this->client->request('GET', 'pricelist/list', [ + 'verify_peer' => false, + 'verify_host' => false, + 'base_uri' => 'https://widget.sovamed.ru', + 'headers' => [ + 'Content-Type' => 'application/json', + 'User-Agent' => 'sovamed_bot' + ], + 'query' => [ + 'depnum' => $depnum, + 'firstrow' => $firstrow, + 'lastrow' => $lastrow + ], + ]); + + $firstrow = $lastrow + 1; + $lastrow = $lastrow + 500; + $result = $result->toArray(); + + if (empty($result['data'])) { + $flag = false; + } else { + $response[] = $result['data']; + } + } + + return $response; + } + + /** + * @Route("/info", name="default_info") + */ + public function info(): Response + { + return $this->render('base/doc.html.twig', [ + 'template' => Region::getTemplite(), + 'title' => 'Информация' + ]); + } + + /** + * @Route("/help", name="default_help") + */ + public function help(Request $request): Response + { + if ($request->getMethod() == 'POST') { + $params = $request->request->get('help'); + $to = 'i.alexandrov@sova.clinic'; + $subject = $params['team']; + $headers = 'From: ' . $params['email'] . "\r\n" . + 'Reply-To: ' . $params['email'] . "\r\n" . + 'X-Mailer: PHP/' . phpversion(); + + $message = 'ФИО:' . $params['fio'] ."\r\n"; + $message .= 'Телефон:' . $params['phone'] ."\r\n"; + $message .= 'Вопрос:' . $params['question'] ."\r\n"; + + $message = wordwrap($message, 70, "\r\n"); + + if (mail($to, $subject, $message, $headers)) { + $this->addFlash( + 'success', + 'Спасибо, мы получили Ваше сообщение.' + ); + } else { + $this->addFlash( + 'notice', + 'Cервис временно не доступен!' + ); + } + } + + return $this->render('base/help.html.twig', [ + 'template' => Region::getTemplite(), + 'title' => 'Помощь' + ]); + } +} diff --git a/src/Controller/DepartmentController.php b/src/Controller/DepartmentController.php new file mode 100644 index 0000000..49bbf41 --- /dev/null +++ b/src/Controller/DepartmentController.php @@ -0,0 +1,49 @@ +render('department/index.html.twig', [ + 'departments' => $departmentRepository->findAll(), + ]); + } + + /** + * @Route("/{id}/edit", name="department_edit", methods={"GET","POST"}) + */ + public function edit(Request $request, Department $department): Response + { + $form = $this->createForm(DepartmentType::class, $department); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->getDoctrine()->getManager()->flush(); + + return $this->redirectToRoute('department_index'); + } + + return $this->render('department/edit.html.twig', [ + 'department' => $department, + 'form' => $form->createView(), + ]); + } +} diff --git a/src/Controller/InternalAPIController.php b/src/Controller/InternalAPIController.php new file mode 100644 index 0000000..107b25d --- /dev/null +++ b/src/Controller/InternalAPIController.php @@ -0,0 +1,304 @@ +rootPath = $rootPath; + $store = new Store($rootPath . '/var/HttpClient'); + $this->client = new CachingHttpClient($client, $store); + } + + /** + * @Route("/swagger.json", name="public_api_swagger_js") + */ + public function swaggerJson(): Response + { + $openapi = \OpenApi\Generator::scan([$this->rootPath . '/src/']); + $response = new Response( + $openapi->toJson(), + Response::HTTP_OK, + ['content-type' => 'application/json'] + ); + + return $response; + } + + /** + * @Route("/swagger", name="public_api_swagger") + */ + public function swaggerUI():response + { + return $this->render('internal_api/swagger.html.twig', [ + 'title' => 'Open API sovamed' + ]); + } + + /** + * @Route("/smart-captcha", name="public_smart_captcha", methods={"POST"}) + */ + public function smartCaptcha(Request $request):response + { + $res = $this->client->request('POST', '/validate', [ + 'verify_peer' => false, + 'verify_host' => false, + 'base_uri' => 'https://smartcaptcha.yandexcloud.net', + 'headers' => [ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + 'User-Agent' => 'sovamed_bot', + ], + 'query' => [ + "secret" => $_ENV['SMARTCAPTCHA_SERVER_KEY'], + "token" => $request->request->get('smart-token'), + "ip" => $_SERVER['REMOTE_ADDR'], + ] + ]); + + return $this->json($res->toArray()); + } + + + /** + * @Route("/banner/{regionId}", name="banner_show", methods={"GET"}) + */ + public function show($regionId, CityRepository $cityRepository): Response + { + $banner = $cityRepository->findOneBy(['regionId' => $regionId])->getBanner(); + $data = [ + 'active' => false, + ]; + + if ($banner) { + $data = [ + 'src' => $banner->getSrc(), + 'href' => $banner->getHref(), + 'active' => $banner->getActive(), + ]; + } + + return $this->json($data); + } + + /** + * @Route("/log", name="api_log", methods={"POST"}) + */ + public function log(Request $request): Response + { + Logger::send($request->toArray()); + + return $this->json(['success' => true]); + } + + /** + * @Route("/count-record", methods={"POST"}) + */ + public function countRecord(RecordRepository $recordRepository, Request $request): Response + { + $stDate = date('Y-m-d H:00', \time()); + $enDate = date('Y-m-d H:00', strtotime('+1 hours', time())); + + $count = $recordRepository + ->createQueryBuilder('r') + ->select('count(r.id)') + ->where('r.hash in (:hash)') + ->andWhere('r.createAt BETWEEN :currentDate AND :nextDate') + ->setParameter('currentDate', new \DateTime($stDate)) + ->setParameter('nextDate', new \DateTime($enDate)) + ->setParameter('hash', md5($request->request->get('phone'))) + ->getQuery() + ->getSingleScalarResult(); + + return $this->json(['data' => [ + 'stDate' => $stDate, + 'enDate' => $enDate, + 'count' => $count, + 'hash' => md5($request->request->get('phone')), + 'phone' => $request->request->get('phone'), + ]]); + } + + /** + * @Route("/add-record", methods={"POST"}) + */ + public function addRecord(Request $request): Response + { + $entityManager = $this->getDoctrine()->getManager(); + + $record = new Record(); + $record + ->setSpecialistId((int) $request->request->get('dcode')) + ->setPhone($request->request->get('phone')) + ->setHash($request->request->get('phone')) + ->setCreateAt(new \DateTime('NOW')); + + try { + $entityManager->persist($record); + $entityManager->flush(); + + return $this->json(['data' => true]); + } catch (Exception $e) { + return $this->json(['data' => $e->getMessage()]); + } + } + + + /** + * https://sms.ru/code/call?phone=79626293193&ip=33.22.11.55&api_id=B58070E1-E89B-95B0-D9BA-37A108868CAF + * @Route("/msg", methods={"POST"}) + */ + public function msg(Request $request): Response + { + return $this->json(['status' => 'OK']); + } + + /** + * https://sms.ru/code/call?phone=79626293193&ip=33.22.11.55&api_id=B58070E1-E89B-95B0-D9BA-37A108868CAF + * @Route("/veretify", methods={"POST"}) + */ + public function veretify(Request $request): Response + { + $phone = preg_replace( '/[^0-9]/', '', $request->request->get('phone')); + $sms = new SmsManager($this->client); + $code = rand(1000, 9999); + $msg = 'Код: ' . $code . ' для подтверждения. Никому не сообщайте пароль.'; + + if (Region::getTemplite() == 'base') { + $response = $sms->sendSmsSova($phone, $msg); + } else { + if ($response = $sms->sendSmsWmt($phone, $msg)) { + $response['status'] = "OK"; + } + } + + if ($response['status'] == 'OK' ) { + if (! empty($code)) { + return $this->json([ + 'status' => 'OK', + 'code' => base64_encode($code) + ]); + } else { + return $this->json(['status' => 'OK']); + } + + } + + return $this->json($response); + } + + /** + * @Route("/search", methods={"POST"}) + */ + public function search( + Request $request, + SpecialistViewRepository $specialistViewRepository, + DepartmentRepository $departmentRepository + ): Response { + $searchType = $request->request->get('type'); + $searchQuery = $request->request->get('q'); + + if ($searchType === 'name') { + + $specialistQuery = $specialistViewRepository->createFilteredQueryBuilder([ + 'name' => $searchQuery, + 'onlineMode' => $request->request->getInt('onlineMode', 0) === 1, + 'regionId' => $request->cookies->getInt('region', 0) + ]); + + $query = $specialistQuery->getQuery(); + } else { + $departmentQuery = $departmentRepository + ->createQueryBuilder('d') + ->where('d.name LIKE :name') + ->setParameter('name', '%' . mb_convert_case($searchQuery, MB_CASE_TITLE, "UTF-8") . '%'); + + $query = $departmentQuery->getQuery(); + } + + return $this->json(['data' => $query->getResult()]); + } + + /** + * @Route("/departments", methods={"GET"}) + */ + public function departments( + Request $request, + DepartmentRepository $departmentRepository + ): Response { + try { + $regionId = $request->cookies->getInt('region'); + $regionId = ($regionId > 0) ? $regionId : null; + $kinder = $request->query->getInt('kinder', 0); + $kinder = ($kinder == 1) ? 1 : null; + + // Используем ту же логику, что и в форме SpecialistSearchType + $qb = $departmentRepository->createQueryBuilder('d') + ->select('d.did, d.name') + ->distinct() + ->innerJoin('App\Entity\LocationView', 'l', 'WITH', 'l.department = d.did AND l.active = true') + ->innerJoin('App\Entity\SpecialistView', 's', 'WITH', 's.id = l.specialistId AND s.active = true') + ->leftJoin('App\Entity\Filial', 'f', 'WITH', 'f.fid = l.filial') + ->where('f.address LIKE :address') + ->andWhere('d.did <> :did') + ->setParameter('did', 0) + ->setParameter('address', '%' . \App\Bundle\Infoclinica\Region::getCurrentName() . '%'); + + // Добавляем фильтр по региону, если он указан + if ($regionId !== null && $regionId > 0) { + $qb->andWhere('s.regionId = :regionId') + ->setParameter('regionId', $regionId); + } + + // Добавляем фильтр по детским специализациям, если выбран "Детский врач" + if ($kinder !== null && $kinder == 1) { + $qb->andWhere('s.sType = :sType') + ->setParameter('sType', 1); + } + // Если "Взрослый врач" или не выбран, показываем все специализации (без фильтра по sType) + + $departments = $qb->orderBy('d.name', 'ASC') + ->getQuery() + ->getArrayResult(); + + $result = []; + foreach ($departments as $department) { + $result[] = [ + 'did' => $department['did'], + 'name' => $department['name'] + ]; + } + + return $this->json(['data' => $result]); + } catch (\Exception $e) { + return $this->json([ + 'error' => $e->getMessage(), + 'data' => [] + ], 500); + } + } +} diff --git a/src/Controller/PageController.php b/src/Controller/PageController.php new file mode 100644 index 0000000..e82cf89 --- /dev/null +++ b/src/Controller/PageController.php @@ -0,0 +1,102 @@ +render('page/index.html.twig', [ + 'pages' => $pageRepository->findAll(), + ]); + } + + /** + * @IsGranted("ROLE_ADMIN") + * @Route("/new", name="page_new", methods={"GET","POST"}) + */ + public function new(Request $request, CategoryPageRepository $categoryPageRepository): Response + { + $page = new Page(); + $form = $this->createForm(PageType::class, $page); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $cp = $categoryPageRepository->findOneBy(['id' => $request->request->get('page')['category']]); + $page->setCategory($cp); + $entityManager = $this->getDoctrine()->getManager(); + $entityManager->persist($page); + $entityManager->flush(); + + return $this->redirectToRoute('page_index'); + } + + return $this->render('page/new.html.twig', [ + 'page' => $page, + 'form' => $form->createView(), + ]); + } + + /** + * @Route("/{alias}", name="page_show", methods={"GET"}) + */ + public function show(Page $page): Response + { + return $this->render('page/show.html.twig', [ + 'page' => $page, + ]); + } + + /** + * @IsGranted("ROLE_ADMIN") + * @Route("/{id}/edit", name="page_edit", methods={"GET","POST"}) + */ + public function edit(Request $request, Page $page): Response + { + $form = $this->createForm(PageType::class, $page); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->getDoctrine()->getManager()->flush(); + + return $this->redirectToRoute('page_index'); + } + + return $this->render('page/edit.html.twig', [ + 'page' => $page, + 'form' => $form->createView(), + ]); + } + + /** + * @IsGranted("ROLE_ADMIN") + * @Route("/{id}", name="page_delete", methods={"POST"}) + */ + public function delete(Request $request, Page $page): Response + { + if ($this->isCsrfTokenValid('delete'.$page->getId(), $request->request->get('_token'))) { + $entityManager = $this->getDoctrine()->getManager(); + $entityManager->remove($page); + $entityManager->flush(); + } + + return $this->redirectToRoute('page_index'); + } +} diff --git a/src/Controller/PublicAPIController.php b/src/Controller/PublicAPIController.php new file mode 100644 index 0000000..a3c402a --- /dev/null +++ b/src/Controller/PublicAPIController.php @@ -0,0 +1,688 @@ +client = new CachingHttpClient($client, $store); + } + + /** + * @Route("/anonymous-reserve", methods={"POST"}) + */ + public function anonymousReserve(Request $request): Response + { + try { + $timezone = Region::getTimezone(); + + if (!empty($request->request->get('timezone'))) { + $timezone = (int) $request->request->get('timezone'); + } + + $reserve = [ + 'date' => date('Ymd', strtotime($request->request->get('workDate'))), + 'st' => explode('-', $request->request->get('time'))[0], + 'en' => explode('-', $request->request->get('time'))[1], + 'services' => [], + 'filial' => (int) $request->request->get('filial'), + 'timezone' => $timezone, + 'schedident' => (int) $request->request->get('schedident'), + 'rnum' => $request->request->get('rnum') === 'undefined' ? null : $request->request->get('rnum'), + 'dcode' => (int) $request->request->get('specialist') + ]; + + $requestData = [ + 'accept' => 'true', + 'fio' => $request->request->get('fio'), + 'captcha' => $request->request->get('captcha'), + 'email' => $request->request->get('email'), + 'phone' => $request->request->get('phone'), + 'reserve' => json_encode($reserve, JSON_UNESCAPED_SLASHES) + ]; + + $referer = $request->headers->get('referer'); + $response = $this->client->request('POST', '/api/reservation/anonymous-reserve', [ + 'verify_peer' => false, + 'verify_host' => false, + 'base_uri' => $_ENV['MIS'], + 'headers' => [ + 'Referer' => $referer, + 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:66.0) Gecko/20100101 Firefox/66.0', + 'Accept-Language' => 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3', + 'Accept' => 'application/json, text/javascript, */*; q=0.01', + 'Content-Type' => 'application/json; charset=UTF-8', + 'X-Requested-With' => 'XMLHttpRequest', + 'X-Integration-Type' => 'WEBSDK' + ], + 'body' => json_encode($requestData) + ]); + + // Проверяем статус ответа + $statusCode = $response->getStatusCode(); + + if ($statusCode !== 200) { + throw new \Exception("External API returned status: {$statusCode}"); + } + + $intervals = $response->toArray(); + + // Сохраняем запись + $entityManager = $this->getDoctrine()->getManager(); + $record = new Record(); + $record + ->setSpecialistId((int) $request->request->get('specialist')) + ->setPhone($request->request->get('phone')) + ->setHash($request->request->get('phone')) + ->setReserve($reserve) + ->setCreateAt(new \DateTime('NOW')); + + $entityManager->persist($record); + $entityManager->flush(); + + return $this->json([ + 'success' => true, + 'data' => [ + 'intervals' => $intervals, + 'hash' => md5($request->request->get('phone')), + 'phone' => $request->request->get('phone'), + 'recordId' => $record->getId(), + ] + ]); + + } catch (\Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface $e) { + // Ошибка 4xx + return $this->json([ + 'success' => false, + 'error' => 'Client error: ' . $e->getMessage() + ], 400); + } catch (\Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface $e) { + // Ошибка 5xx + return $this->json([ + 'success' => false, + 'error' => 'Server error: ' . $e->getMessage() + ], 502); + } catch (\Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface $e) { + // Ошибки сети + return $this->json([ + 'success' => false, + 'error' => 'Network error: ' . $e->getMessage() + ], 503); + } catch (\Exception $e) { + // Другие ошибки + return $this->json([ + 'success' => false, + 'error' => 'Internal error: ' . $e->getMessage() + ], 500); + } + } + + /** + * @OA\Get( + * tags= {"Расписание врача"}, + * path="/interval", + * summary="Получение сетки расписания", + * @OA\Parameter( + * name="startInterval", + * description="Начальная дата (Y-m-d)", + * in="query", + * required=true, + * @OA\Schema( + * type="string", + * format="Y-m-d" + * + * ) + * ), + * @OA\Parameter( + * name="endInterval", + * description="Конечна дата (Y-m-d)", + * in="query", + * required=true, + * @OA\Schema( + * type="string", + * format="Y-m-d" + * + * ) + * ), + * @OA\Parameter( + * name="department", + * description="ID отделения", + * in="query", + * required=true, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( + * name="doctor", + * description="ID врача", + * in="query", + * required=true, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( + * name="filial", + * description="ID филиала", + * in="query", + * required=true, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Response( + * response=200, + * description="json response" + * ) + * ) + * + * @Route("/interval", methods={"GET"}) + */ + public function interval(Request $request): Response + { + $dateFormat = $request->query->get('dateFormat'); + + if (empty($dateFormat)) { + $dateFormat = 'Y-m-d'; + } + + $startInterval = $request->query->get('startInterval'); + $endInterval = $request->query->get('endInterval'); + $doctor = $request->query->get('doctor'); + $department = $request->query->get('department'); + $filial = $request->query->get('filial'); + $onlineMode = OnlineMode::isOnline($request->query->get('onlineMode')); + $isFree = true; + $nearestDate = NULL; + + if (empty($doctor) || empty($startInterval) || empty($endInterval) || empty($department) || empty($filial)) { + throw new BadRequestHttpException('Bad request'); + } + + $schedules = $this->getSchedule($doctor, $department, $filial, $onlineMode, $startInterval, $endInterval); + $intervals = $this->getInterval($doctor, $department, $filial, $onlineMode, $startInterval, $endInterval); + + $findInterval = function ($schedident, $workDate) use($intervals, $onlineMode, $isFree, $nearestDate, $dateFormat) { + $intervalsData = []; + + if (!empty($intervals)) { + foreach ($intervals[date('Ymd', strtotime($workDate))] as $key => $interval) { + if ($interval['schedident'] == $schedident) { + $intervalsData[$key]['time'] = $interval['time']; + $intervalsData[$key]['rNum'] = isset($interval['rNum'])? $interval['rNum']: null; + $intervalsData[$key]['startTime'] = explode('-', $interval['time'])[0]; + $intervalsData[$key]['endTime'] = explode('-', $interval['time'])[1]; + $intervalsData[$key]['schedident'] = $interval['schedident']; + $intervalsData[$key]['isFree'] = $interval['isFree']; + $intervalsData[$key]['onlineMode'] = $onlineMode; + $intervalsData[$key]['workDate'] = $interval['workDate']->format($dateFormat); + + if ($interval['isFree'] && $isFree && is_null($nearestDate)) { + $nearestDate = $interval['workDate']->format('Y-m-d'); + $intervalsData[$key]['nearestDate'] = $interval['workDate']->format($dateFormat); + $isFree = false; + } + } + } + } + + return $intervalsData; + }; + + $dataResponse = []; + $i = 0; + + if (isset($schedules['success'])) { + if ($schedules['success'] == true) { + $uniqueIntervals = []; + + foreach ($schedules['data'] as $key => $data) { + uasort($data['intervals'], function ($a, $b) { + if ($a['workDate'] == $b['workDate']) { + return $a['startInterval'] <=> $b['startInterval']; + } + return 0; + }); + + foreach($data['intervals'] as $interval) { + if ($interval['isFree'] === true) { + $workDate = date($dateFormat, strtotime($interval['workDate'])); + + $uniqueKey = $interval['schedident'] + . '-' . $workDate + . '-' . $interval['startInterval'] + . '-' . $interval['endInterval']; + + if (!isset($uniqueIntervals[$uniqueKey])) { + $dataIntervals = $findInterval($interval['schedident'], $workDate); + if ($dataIntervals) { + $uniqueIntervals[$uniqueKey] = [ + 'workDate' => $workDate, + 'isFree' => $interval['isFree'], + 'startInterval' => $interval['startInterval'], + 'endInterval' => $interval['endInterval'], + 'intervals' => $dataIntervals + ]; + } + } + } + } + } + + $dataResponse = array_values($uniqueIntervals); + } + } + + + $uid = false; + + if (! is_null($this->getUser())) { + $uid = $this->getUser()->getUid(); + } + + return $this->json(['data' => ['userInfo' => $uid, 'intervalsData' => $dataResponse]]); + } + + private function getInterval($doctor, $department, $filial, $onlineMode, $startInterval, $endInterval) + { + $response = $this->client->request('GET', '/api/reservation/intervals', [ + 'verify_peer' => false, + 'verify_host' => false, + 'base_uri' => $_ENV['MIS'], + 'headers' => [ + 'Content-Type' => 'application/json', + 'User-Agent' => 'sovamed_bot' + ], + 'query' => [ + 'dcode' => $doctor, + 'spec' => $department, + 'onlineMode' => ($onlineMode ? 1 : 0), + 'st' => \date("Ymd", strtotime($startInterval)), + 'en' => \date("Ymd", strtotime($endInterval)), + 'filialId' => $filial, + 'inFilials' => $filial + ] + ]); + + $intervals = $response->toArray(); + $dataResponse = []; + + if (isset($intervals['data'])) { + foreach ($intervals['data'] as $data) { + if (isset($data['workdates'])) { + foreach ($data['workdates'] as $key => $workdates) { + foreach ($workdates as $workdate => $item) { + $workDate = \DateTime::createFromFormat( + 'Ymd', + $workdate + ); + + $intervalKey = 0; + + for ($i=0; $i < count($item); $i++) { + foreach ($item[$i]['intervals'] as $intervaldata) { + $dataResponse[$workdate][$intervalKey]['workDate'] = $workDate; + $dataResponse[$workdate][$intervalKey]['schedident'] = $item[$i]['schedident']; + $dataResponse[$workdate][$intervalKey]['time'] = $intervaldata['time']; + $dataResponse[$workdate][$intervalKey]['isFree'] = $intervaldata['isFree']; + $dataResponse[$workdate][$intervalKey]['rNum'] = isset($item[$i]['rnum'])? $item[$i]['rnum']: null; + $intervalKey++; + } + } + } + } + } + } + } + + return $dataResponse; + } + + private function getSchedule($doctor, $department, $filial, $onlineMode, $startInterval, $endInterval) + { + $response = $this->client->request('GET', '/api/reservation/schedule', [ + 'verify_peer' => false, + 'verify_host' => false, + 'base_uri' => $_ENV['MIS'], + 'headers' => [ + 'Content-Type' => 'application/json', + 'User-Agent' => 'sovamed_bot' + ], + 'query' => [ + 'doctor' => $doctor, + 'department' => $department, + 'onlineMode' => ($onlineMode ? 1 : 0), + 'st' => date("Ymd", strtotime($startInterval)), + 'en' => date("Ymd", strtotime($endInterval)), + 'filialId' => $filial + ] + ]); + + return $response->toArray(); + } + + /** + * @Route("/userInfo", methods={"GET"}) + */ + public function user(): Response + { + $uid = false; + + if (! is_null($this->getUser())) { + $uid = $this->getUser()->getUid(); + } + + return $this->json(['data' => $uid]); + } + + /** + * @OA\Get( + * tags= {"Услуги и цены"}, + * path="/pricelist/departments", + * summary="Получение списка отделений", + * @OA\Response( + * response=200, + * description="json response" + * ) + * ) + * + * @Route("/pricelist/departments", methods={"GET"}) + */ + public function pricelistDepartments(Request $request): Response + { + $response = []; + + $entityManager = $this->getDoctrine()->getManager(); + $departments = $entityManager->getRepository(PriceDepartment::class) + ->findAll(); + + if ($departments) { + foreach ($departments as $key => $item) { + $item = $item->toArray(); + + unset($item['__initializer__']); + unset($item['__isInitialized__']); + unset($item['__cloner__']); + unset($item['id']); + $response[$key] = $item; + } + } + + return $this->json(['data' => $response]); + } + + /** + * @OA\Get( + * tags= {"Услуги и цены"}, + * path="/pricelist", + * summary="Получение списка услуг и цен", + * @OA\Parameter( + * name="depnum", + * description="ID отделения", + * in="query", + * required=true, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( + * name="filial", + * description="ID филиала", + * in="query", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( + * name="active", + * description="Только активные", + * in="query", + * required=false, + * @OA\Schema( + * type="boolean" + * ) + * ), + * @OA\Response( + * response=200, + * description="json response" + * ) + * ) + * + * @Route("/pricelist", methods={"GET"}) + */ + public function pricelist(Request $request, PaginatorInterface $paginator, PriceListService $priceListService): JsonResponse + { + $params = [ + 'kodoper' => $request->query->get('kodoper'), + 'groupId' => $request->query->get('depnum'), + 'filial' => $request->query->get('filial'), + 'actual' => $request->query->get('active') + ]; + + $priceListQuery = $priceListService->getPriceListQuery($params); + + $pagination = $paginator->paginate( + $priceListQuery->getQuery(), + $request->query->getInt('page', 1), + 1000 + ); + + $totalItems = $pagination->getTotalItemCount(); // Общее количество элементов + $itemCount = $pagination->count(); // Количество элементов на текущей странице + $currentPage = $pagination->getCurrentPageNumber(); // Текущая страница + $totalPages = ceil($totalItems / 1000); // Общее количество страниц + + return $this->json([ + 'items' => $pagination, + 'totalItems' => $totalItems, + 'totalPages' => $totalPages, + 'currentPage' => $currentPage, + 'itemCount' => $itemCount + ]); + } + + private function getSpecialistResponse($specialist) + { + $response = []; + + if ($specialist) { + $response = $specialist->toArray(); + + unset($response['pecialistMore']); + + $response['img'] = 'https://api.sovamed.ru/specialist/picture/' . $specialist->getId(); + + if (!empty($response['kinder'])) { + $response['kinder'] = $response['kinder'] . ' ' . $this->textYear($response['kinder'], false); + } + + if (!empty($response['experience'])) { + $response['experience'] = $response['experience'] . ' ' . $this->textYear($response['experience'], true); + } + + $specialistMore = $specialist->getSpecialistMore(); + + if ($defaultLocation = $specialistMore->defaultLocation()) { + $response['nearestDate'] = $defaultLocation['nearestDate']; + $response['filial'] = [ + 'id' => $defaultLocation['filial'], + 'address' => $defaultLocation['address'], + ]; + $response['department'] = [ + 'id' => $defaultLocation['department'], + 'name' => $defaultLocation['name'], + ]; + } + + $response['reviews'] = $specialistMore->getReviews(); + $response['prices'] = $specialistMore->getPrices(); + } + + return $response; + } + + private function textYear($year, $exp = true) + { + $t1 = 0; + $t2 = 0; + $year = abs($year); + $t1 = $year % 10; + $t2 = $year % 100; + if ($exp) { + return ($t1 == 1 && $t2 != 11 ? "год" : ($t1 >= 2 && $t1 <= 4 && ($t2 < 10 || $t2 >= 20) ? "года" : "лет")); + } else { + return ($t1 == 1 ? "года" : "лет"); + } + } + + /** + * @OA\Get( + * tags= {"Врачи"}, + * path="/doctor", + * summary="Получение данных о враче", + * @OA\Parameter( + * name="sid", + * description="ID врача", + * in="query", + * required=true, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( + * name="reviews", + * description="Показывать отзывы", + * in="query", + * required=false, + * @OA\Schema( + * type="boolean", + * default=false + * ) + * ), + * @OA\Response( + * response=200, + * description="json response" + * ) + * ) + * + * @Route("/doctor", methods={"GET"}) + */ + public function doctor(SpecialistService $specialistService, Request $request): Response + { + if (empty($request->query->getInt('sid'))) { + return $this->json(['data' => false]); + } + + $specialist = $specialistService->show([ + 'dcode' => $request->query->get('sid') + ]); + + return $this->json(['data' => $this->getSpecialistResponse($specialist)]); + } + + /** + * @OA\Get( + * tags= {"Врачи"}, + * path="/doctors/{region}", + * summary="Получение данных врачей по регионам", + * @OA\Parameter( + * name="region", + * description="Название города", + * in="path", + * required=true, + * @OA\Schema( + * type="string", + * default="saratov" + * ) + * ), + * @OA\Parameter( + * name="reviews", + * description="Показывать отзывы", + * in="query", + * required=false, + * @OA\Schema( + * type="boolean", + * default=true + * ) + * ), + * @OA\Response( + * response=200, + * description="json response" + * ) + * ) + * + * @Route("/doctors/{region}", methods={"GET"}) + */ + public function index(SpecialistService $specialistService, Request $request, $region = 'saratov'): Response + { + $regionId = match($region) { + 'krasnodar' => 94, + 'voronej' => 93, + 'volgograd' => 92, + default => 91 + }; + + $pagination = $specialistService->listPaginated( + ['regionId' => $regionId], + $request->query->getInt('page', 1), + 500 + ); + + $totalItems = $pagination->getTotalItemCount(); // Общее количество элементов + $itemCount = $pagination->count(); // Количество элементов на текущей странице + $currentPage = $pagination->getCurrentPageNumber(); // Текущая страница + $totalPages = ceil($totalItems / 1000); // Общее количество страниц + + $response = []; + + foreach ($pagination as $key => $specialist) { + $response[$key] = $this->getSpecialistResponse($specialist); + } + + return $this->json([ + 'data' => $response, + 'totalItems' => $totalItems, + 'totalPages' => $totalPages, + 'currentPage' => $currentPage, + 'itemCount' => $itemCount + ]); + } + +} diff --git a/src/Controller/ReviewSourceController.php b/src/Controller/ReviewSourceController.php new file mode 100644 index 0000000..3d7847e --- /dev/null +++ b/src/Controller/ReviewSourceController.php @@ -0,0 +1,80 @@ +render('review_source/index.html.twig', [ + 'review_sources' => $reviewSourceRepository->findAll(), + ]); + } + + /** + * @Route("/new", name="app_review_source_new", methods={"GET", "POST"}) + */ + public function new(Request $request, ReviewSourceRepository $reviewSourceRepository): Response + { + $reviewSource = new ReviewSource(); + $form = $this->createForm(ReviewSourceType::class, $reviewSource); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $reviewSourceRepository->add($reviewSource); + return $this->redirectToRoute('app_review_source_index', [], Response::HTTP_SEE_OTHER); + } + + return $this->render('review_source/new.html.twig', [ + 'review_source' => $reviewSource, + 'form' => $form->createView(), + ]); + } + + /** + * @Route("/{id}/edit", name="app_review_source_edit", methods={"GET", "POST"}) + */ + public function edit(Request $request, ReviewSource $reviewSource, ReviewSourceRepository $reviewSourceRepository): Response + { + $form = $this->createForm(ReviewSourceType::class, $reviewSource); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $reviewSourceRepository->add($reviewSource); + return $this->redirectToRoute('app_review_source_index', [], Response::HTTP_SEE_OTHER); + } + + return $this->render('review_source/edit.html.twig', [ + 'review_source' => $reviewSource, + 'form' => $form->createView(), + ]); + } + + /** + * @Route("/{id}", name="app_review_source_delete", methods={"POST"}) + */ + public function delete(Request $request, ReviewSource $reviewSource, ReviewSourceRepository $reviewSourceRepository): Response + { + if ($this->isCsrfTokenValid('delete'.$reviewSource->getId(), $request->request->get('_token'))) { + $reviewSourceRepository->remove($reviewSource); + } + + return $this->redirectToRoute('app_review_source_index', [], Response::HTTP_SEE_OTHER); + } +} diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php new file mode 100644 index 0000000..c7076f7 --- /dev/null +++ b/src/Controller/SecurityController.php @@ -0,0 +1,405 @@ +csrfTokenManager = $csrfTokenManager; + $this->passwordEncoder = $passwordEncoder; + } + + /** + * @IsGranted("ROLE_USER") + * @Route("/refund", name="security_refund", methods={"GET", "POST"}) + */ + public function refund(Request $request): Response + { + $refundForm = new RefundType(); + $form = $this->createForm(RefundType::class, $refundForm); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + if ($request->request->get('filial') == 3) { + $to = 'info.mmc@sova.clinic, i.cherednichenko@sova.clinic, y.belova@sova.clinic'; + $company_name = 'АО «Многопрофильный медицинский центр»'; + $company_director = 'Бушеневой С.Н.'; + $to_mail = 'vozvrat-vlg@sova.clinic'; + } else { + $to = 'i.cherednichenko@sova.clinic, v.karpova@sova.clinic, n.ermakova@sova.clinic'; + $company_name = 'АО «МЛДК»'; + $company_director = 'Бурлаковой Н.Ф.'; + $to_mail = 'vozvrat@sova.clinic'; + } + + $subject = "Пациент сформировал заявление на возврат средств по онлайн консультации"; + $headers = 'From: ' . $request->request->get('email') . "\r\n" . + 'Reply-To: ' . $request->request->get('email') . "\r\n" . + 'X-Mailer: PHP/' . phpversion(); + $message = "Здравствуйте. Пациент сформировал заявление на возврат средств по онлайн консультации. Ожидаем отправки заявления с почты пациента.\r\n Данные по консультации:\r\n"; + $message .= 'ФИО пациента:' . $form->get('fio')->getData() ."\r\n"; + $message .= 'Телефон пациента:' . $request->request->get('phone') ."\r\n"; + $message .= 'Врач:' . $request->request->get('docName') ."\r\n"; + $message .= 'Индификатор записи:' . $request->request->get('schedident') ."\r\n"; + $message .= 'Сумма возврата:' . $form->get('sum')->getData() ."\r\n"; + $message .= 'Дата платежа:' . $form->get('refund_date')->getData()->format('d.m.Y') ."\r\n"; + + mail($to, $subject, $message, $headers); + + $html = $this->render('security/refund_blank.html.twig', [ + 'template' => Region::getTemplite(), + 'to_email' => $to_mail, + 'title' => 'Заявление на возврат', + 'company_name' => $company_name, + 'company_director' => $company_director, + 'address' => $request->request->get('address'), + 'phone' => $request->request->get('phone'), + 'email' => $request->request->get('email'), + 'current_date' => \date('Y-m-d'), + 'fio' => $form->get('fio')->getData(), + 'passport_serial' => explode(' ', $form->get('passport_serial')->getData())[0], + 'passport_number' => explode(' ', $form->get('passport_serial')->getData())[1], + 'passport_issued' => $form->get('passport_issued')->getData(), + 'passport_date' => $form->get('passport_date')->getData()->format('d.m.Y'), + 'refund_bases' => $form->get('refund_bases')->getData(), + 'sum' => $form->get('sum')->getData(), + 'refund_date' => $form->get('refund_date')->getData()->format('d.m.Y') + ]); + + $mpdf = new \Mpdf\Mpdf(); + $mpdf->WriteHTML($html); + $mpdf->Output(); + } + + return $this->render('security/refund_form.html.twig', [ + 'form' => $form->createView(), + ]); + } + + /** + * @IsGranted("ROLE_USER") + * @Route("/case-history", name="security_case_history") + */ + public function case_history(): Response + { + return $this->render('security/case_history.html.twig', [ + 'template' => Region::getTemplite(), + 'title' => 'Приемы' + ]); + } + + /** + * @IsGranted("ROLE_USER") + * @Route("/referrals", name="security_referrals") + */ + public function referrals(): Response + { + $referrals = []; + + return $this->render('security/referrals.html.twig', [ + 'referrals' => $referrals, + 'template' => Region::getTemplite(), + 'title' => 'Результаты анализов', + ]); + } + + /** + * @IsGranted("ROLE_USER") + * @Route("/security-card", name="security_card") + */ + public function securityCard(): Response + { + return $this->render('security/card.html.twig', [ + 'template' => Region::getTemplite(), + 'title' => 'Медицинская карта', + ]); + } + + /** + * @IsGranted("ROLE_USER") + * @Route("/payment", name="security_payment") + */ + public function payment(): Response + { + return $this->render('security/payment.html.twig', [ + 'template' => Region::getTemplite(), + 'title' => 'Финансы', + ]); + } + + /** + * @IsGranted("ROLE_USER") + * @Route("/setting", name="security_setting") + */ + public function setting( + Request $request, + UserPasswordEncoderInterface $passwordEncoder + ): Response + { + $user = $this->getUser(); + $form = $this->createForm(SettingType::class, $user); + $form->handleRequest($request); + $response = []; + + if ($request->getMethod() == 'POST') { + + $user->setToken($form->get('plainPassword')->getData()); + $user->setPassword( + $passwordEncoder->encodePassword( + $user, + $form->get('plainPassword')->getData() + ) + ); + + $entityManager = $this->getDoctrine()->getManager(); + $entityManager->persist($user); + $entityManager->flush(); + + + return $this->json([ + 'success' => true, + 'redirect' => '/' + ]); + } + + return $this->render('security/setting.html.twig', [ + 'template' => Region::getTemplite(), + 'form' => $form->createView(), + 'setting' => $response, + 'title' => 'Настройки', + ]); + } + + /** + * @Route("/login", name="security_login") + */ + public function login(Request $request, AuthenticationUtils $authenticationUtils): Response + { + $template = preg_match('/sovamed\.ru/m', $request->getHost())? 'login' : 'login_wmtmed'; + + return $this->render('security/' . $template . '.html.twig', [ + 'template' => Region::getTemplite(), + 'alias' => null, + 'last_username' => $authenticationUtils->getLastUsername(), + 'error' => $authenticationUtils->getLastAuthenticationError(), + 'title' => 'Личный кабинет - «СОВА»' + ]); + } + + /** + * @Route("/logout", name="security_logout") + */ + public function logout() + { + + } + + /** + * @Route("/api/usrlog/logout", name="security_usrlog_logout", methods={"POST"}) + */ + public function usrlogLogout(Request $request, UsrlogRepository $usrlogRepository): Response + { + $pcode = null; + $user = $this->getUser(); + + if ($user instanceof User) { + $pcode = (string) $user->getUid(); + } else { + $pcode = trim((string) $request->request->get('pcode', '')); + } + + if ($pcode === '') { + return $this->json(['success' => false, 'message' => 'pcode is required'], 400); + } + + $usrlog = new Usrlog(); + $usrlog + ->setPcode($pcode) + ->setAgent((string) ($request->headers->get('User-Agent') ?? 'unknown')) + ->setClientIp((string) ($request->getClientIp() ?? 'unknown')) + ->setMethod('выход') + ; + + $usrlogRepository->add($usrlog); + + return $this->json(['success' => true]); + } + + /** + * @Route("/registration", name="security_reg", methods={"GET","POST"}) + */ + public function registration( + Request $request, + UserPasswordEncoderInterface $passwordEncoder + ): Response + { + $user = new User(); + $form = $this->createForm(RegistrationFormType::class, $user); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $fullName = $form->get('firstName')->getData(); + $fullName .= ' '; + $fullName .= $form->get('middleName')->getData(); + $user->setFullName($fullName); + $user->setToken($form->get('plainPassword')->getData()); + $user->setRoles(['ROLE_USER']); + $user->setConfirm(0); + $user->setPassword( + $passwordEncoder->encodePassword( + $user, + $form->get('plainPassword')->getData() + ) + ); + + $infoclinica = new Rest(); + $response = $infoclinica->register($request->request->all()); + + if ($response['response']['success'] == true) { + $user->setUid(date('U')); + $entityManager = $this->getDoctrine()->getManager(); + $entityManager->persist($user); + $entityManager->flush(); + + return $this->redirectToRoute('security_confirm', [ + 'id' => $user->getId(), + 'rToken' => $response['response']['data']['rToken'] + ]); + } else { + $this->addFlash( + 'notice', + $response['response']['data']['message'] ?? 'Cервис временно не доступен!' + ); + } + } + + return $this->render('security/register.html.twig', [ + 'title' => 'Регистрация', + 'template' => Region::getTemplite(), + 'form' => $form->createView() + ]); + } + + /** + * @Route("/forget", name="security_forget", methods={"POST"}) + */ + public function forget(Request $request, UserPasswordEncoderInterface $passwordEncoder) + { + $entityManager = $this->getDoctrine()->getManager(); + $email = false; + $uid = $request->request->get('uid'); + + if ($email = $request->request->get('login')) { + $user = $entityManager->getRepository(User::class) + ->findOneBy(['email' => \bin2hex($email)]); + + if ($user) { + $uid = $user->getUid(); + } + + return $this->json(['uid' => $uid]); + }; + + $password = $request->request->get('password'); + + if ($uid && $password) { + $user = $entityManager->getRepository(User::class) + ->findOneBy(['uid' => $uid]); + + if (!$user) { + $user = new User(); + } + + $user->setUid($uid); + $user->setToken($password); + $user->setPassword( + $passwordEncoder->encodePassword( + $user, + $password + ) + ); + + $entityManager = $this->getDoctrine()->getManager(); + $entityManager->persist($user); + $entityManager->flush(); + + return $this->json([ + 'success' => true, + 'redirect' => '/login' + ]); + } + + return $this->json([ + 'success' => false, + 'uid' => $uid + ]); + } + + /** + * @Route("/api/authenticated", name="security_authenticated", methods={"POST"}) + */ + public function authenticated(Request $request, + GuardAuthenticatorHandler $guardHandler, + LoginFormAuthenticator $authenticator, + UserPasswordEncoderInterface $passwordEncoder + ): Response { + $entityManager = $this->getDoctrine()->getManager(); + $userData = $request->request->get('user'); + + $user = $entityManager->getRepository(User::class) + ->findOneBy(['uid' => $userData['id']]); + + if (!$user && $userData) { + $user = new User(); + + $user + ->setFullName($userData['fullName']) + ->setEmail($userData['email']) + ->setPhone($userData['phone']) + ->setUid($userData['id']) + ->setConfirm(1) + ->setRoles(['ROLE_USER']) + ->setToken($userData['id']) + ->setPassword( + $passwordEncoder->encodePassword( + $user, + $userData['id'] + ) + ); + + $entityManager = $this->getDoctrine()->getManager(); + $entityManager->persist($user); + $entityManager->flush(); + $entityManager->clear(); + } + + return $guardHandler->authenticateUserAndHandleSuccess( + $user, + $request, + $authenticator, + 'main' + ); + } + +} diff --git a/src/Controller/SpecialistController.php b/src/Controller/SpecialistController.php new file mode 100644 index 0000000..c6ea098 --- /dev/null +++ b/src/Controller/SpecialistController.php @@ -0,0 +1,217 @@ +cookies->getInt('region'); + $regionId = ($regionId > 0) ? $regionId : null; + + // Получаем значение kinder из запроса для фильтрации специализаций + $kinder = $request->query->get('specialist_search')['kinder'] ?? null; + $kinder = ($kinder == 1) ? 1 : null; + + $searchForm = $this->createForm(SpecialistSearchType::class, new SpecialistView(), [ + 'action' => $this->generateUrl('specialist_index'), + 'method' => 'GET', + 'regionId' => $regionId, + 'kinder' => $kinder, + ]); + + $searchForm->handleRequest($request); + + return $this->render('specialist/_search_form.html.twig', [ + 'searchForm' => $searchForm->createView() + ]); + } + + /** + * @Route("/specialists/{alias?}", name="specialist_index", methods={"GET"}) + */ + public function index( + SpecialistService $specialistService, + Request $request, + string $alias = null + ): Response { + $regionId = $request->cookies->getInt('region'); + $regionId = ($regionId > 0) ? $regionId : null; + + // Получаем значение kinder из запроса для фильтрации специализаций + $kinder = $request->query->get('specialist_search')['kinder'] ?? null; + $kinder = ($kinder == 1) ? 1 : null; + + $searchForm = $this->createForm(SpecialistSearchType::class, new SpecialistView(), [ + 'action' => $this->generateUrl('specialist_index', ['alias' => $alias]), + 'method' => 'GET', + 'regionId' => $regionId, + 'kinder' => $kinder, + ]); + + $searchForm->handleRequest($request); + + $page = $request->query->getInt('page', 1); + + $filters = $request->query->get('specialist_search', ['onlineMode' => 0]); + $filters['depAlias'] = $alias; + + if ($regionId > 0) { + $filters['regionId'] = $regionId; + } + + $pagination = $specialistService->listPaginated($filters, $page, 10); + $view = 'specialist/index.html.twig'; + + if (Region::getTemplite() == 'krasnodar_base') { + $view = 'specialist/krasnodar_index.html.twig'; + } + + if (! empty($request->query->get('specialist_search')['current_date'])) { + $currentDate = $request->query->get('specialist_search')['current_date']; + $dates = explode('-', $currentDate); + $startInterval = $dates[0]; + $endInterval = $dates[1]; + } else { + $startInterval = date("Y-m-d"); + $endInterval = date("Y-m-d", strtotime('+7 day')); + } + + return $this->render($view, [ + 'title' => 'Врачи', + 'alias' => $alias, + 'template' => Region::getTemplite(), + 'pagination' => $pagination, + 'searchForm' => $searchForm->createView(), + 'st' => $startInterval, + 'en' => $endInterval + ]); + } + + /** + * @IsGranted("ROLE_USER") + * @Route("/online-specialists", name="specialist_online_index", methods={"GET"}) + */ + public function onlineIndex( +SpecialistService $specialistService, + Request $request + ): Response { + $regionId = $request->cookies->getInt('region'); + $regionId = ($regionId > 0) ? $regionId : null; + + // Получаем значение kinder из запроса для фильтрации специализаций + $kinder = $request->query->get('specialist_search')['kinder'] ?? null; + $kinder = ($kinder == 1) ? 1 : null; + + $searchForm = $this->createForm(SpecialistSearchType::class, new SpecialistView(), [ + 'action' => $this->generateUrl('specialist_online_index'), + 'method' => 'GET', + 'regionId' => $regionId, + 'kinder' => $kinder, + ]); + + $searchForm->handleRequest($request); + + $filters = $request->query->get('specialist_search', ['onlineMode' => 1]); + $filters['onlineMode'] = 1; + + if ($regionId > 0) { + $filters['regionId'] = $regionId; + } + + $page = $request->query->getInt('page', 1); + + $pagination = $specialistService->listPaginated($filters, $page, 10); + + $view = 'specialist/index.html.twig'; + + if (Region::getTemplite() == 'krasnodar_base') { + $view = 'specialist/krasnodar_index.html.twig'; + } + + if (! empty($request->query->get('specialist_search')['current_date'])) { + $currentDate = $request->query->get('specialist_search')['current_date']; + $dates = explode('-', $currentDate); + $startInterval = $dates[0]; + $endInterval = $dates[1]; + } else { + $startInterval = date("Y-m-d"); + $endInterval = date("Y-m-d", strtotime('+7 day')); + } + + return $this->render($view, [ + 'title' => 'Онлайн консультация', + 'template' => Region::getTemplite(), + 'pagination' => $pagination, + 'searchForm' => $searchForm->createView(), + 'st' => $startInterval, + 'en' => $endInterval + ]); + } + + /** + * @Route("/specialist/{alias}", name="specialist_show", methods={"GET"}) + */ + public function show( + SpecialistService $specialistService, + Request $request, + string $alias + ): Response { + $filters = $request->query->get('specialist_search', ['onlineMode' => 0]); + // $filters['regionId'] = $request->cookies->getInt('region'); + $filters['alias'] = $alias; + + $specialist = $specialistService->show($filters); + + if ($specialist) { + $specialistMoreService = $specialist->getSpecialistMore(); + + if ($defaultLocation = $specialistMoreService->defaultLocation()) { + return $this->render('specialist/show.html.twig', [ + 'title' => 'Врач', + 'st' => date("Y-m-d"), + 'en' => date("Y-m-d", strtotime('+7 day')), + 'template' => Region::getTemplite(), + 'specialist' => $specialist, + 'specialistMore' => $specialistMoreService, + ]); + } + } + + throw $this->createNotFoundException('The page does not exist'); + } + + /** + * @Route("/favorites", name="default_favorites") + */ + public function favorites(SpecialistService $specialistService, Request $request): Response + { + $page = $request->query->getInt('page', 1); + $filters['dcode'] = explode(',', $request->query->get('q')); + + $pagination = $specialistService->listPaginated($filters, $page, 10); + + return $this->render('base/favorites.html.twig', [ + 'st' => date("Y-m-d"), + 'en' => date("Y-m-d", strtotime('+7 day')), + 'pagination' => $pagination, + 'template' => Region::getTemplite(), + 'title' => 'Избранное' + ]); + } +} diff --git a/src/Controller/WidgetController.php b/src/Controller/WidgetController.php new file mode 100644 index 0000000..bfe9e6b --- /dev/null +++ b/src/Controller/WidgetController.php @@ -0,0 +1,159 @@ +findByCity($cityId) as $key => $reviewSource) { + $reviewSources[$key] = $reviewSource; + $reviewSources[$key]['isFloat'] = true; + $f = (float) $reviewSource['rating_total']; + + if (strpos($reviewSource['rating_total'], '.') === false) { + $reviewSources[$key]['isFloat'] = false; + } + } + + return $this->render('widget/review_source.html.twig', [ + 'reviewSources' => $reviewSources, + ]); + } + + /** + * @Route("/reference", name="widget_reference") + */ + public function reference(Request $request): Response + { + $ref = $request->query->get('ref', ''); + $regionId = match (base64_decode($ref, strict: true)) { + 'https://volgograd.sovamed.ru' => 92, + 'https://voronezh.sovamed.ru' => 93, + 'https://wmtmed.ru' => 94, + default => 91, + }; + + $isAuthorized = $this->isGranted('ROLE_USER'); + + $referenceForm = $this->createForm(ReferenceType::class, new User, [ + 'method' => 'GET', + 'isAuthorized' => $isAuthorized, + ]); + + return $this->render('widget/reference.html.twig', [ + 'regionId' => $regionId, + 'referenceForm' => $referenceForm->createView() + ]); + } + + /** + * @Route("/check/{hash}/{id}", name="widget_check", methods={"GET"}) + */ + public function check( + RecordRepository $recordRepository, + FilialRepository $filialRepository, + HttpClientInterface $client, + SpecialistService $specialistService, + $hash, + $id + ): Response { + $record = $recordRepository->findOneBy(['hash' => $hash, 'id' => $id]); + + if ($record) { + $reserve = $record->getReserve(); + $reserve['date'] = \date('d-m-Y', strtotime($reserve['date'])); + + $sms = new SmsManager($client); + $msg = 'Ждем Вас: '; + + $filial = $filialRepository->findOneBy(['fid' => $reserve['filial']]); + + if ($filial) $msg .= $filial->getName() . ' '; + + $msg .= $reserve['date'] . ' в '. $reserve['st']; + + if (Region::getTemplite() == 'base') { + if (!$record->getAlertSms()) { + $response = $sms->sendSmsSova($record->getPhone(), $msg); + + $alertSms = new AlertSms(); + $alertSms + ->setDateCreate(new \DateTime()) + ->setResponse(json_encode($response, JSON_UNESCAPED_UNICODE)) + ->setRecord($record); + + $entityManager = $this->getDoctrine()->getManager(); + $entityManager->persist($alertSms); + $entityManager->flush(); + } + + $html = 'logo
    '; + } else { + if (!$record->getAlertSms()) { + $response = $sms->sendSmsWmt($record->getPhone(), $msg); + + $alertSms = new AlertSms(); + $alertSms + ->setDateCreate(new \DateTime()) + ->setResponse(json_encode($response, JSON_UNESCAPED_UNICODE)) + ->setRecord($record); + + $entityManager = $this->getDoctrine()->getManager(); + $entityManager->persist($alertSms); + $entityManager->flush(); + } + + $html = 'logo
    '; + } + + + $specialist = $specialistService->show(['dcode' => $reserve['dcode']]); + + if ($specialist) { + $html .= '

    ' . $specialist->getName() . '

    '; + } + + $html .= '

    Филиал: ' . $filial->getName(). '

    '; + $html .= '

    Дата приема: '. $reserve['date'] . ' c '. $reserve['st']. ' по ' . $reserve['en'] .'

    '; + $html .= '

    На Ваш номер отправлено смс с информацией о приеме

    '; + + $mpdf = new \Mpdf\Mpdf(); + $mpdf->WriteHTML($html); + $mpdf->Output(); + } + + throw $this->createNotFoundException('The event does not exist'); + } +} diff --git a/src/Controller/WidgetFormController.php b/src/Controller/WidgetFormController.php new file mode 100644 index 0000000..0846a49 --- /dev/null +++ b/src/Controller/WidgetFormController.php @@ -0,0 +1,230 @@ +client = new CachingHttpClient($client, $store); + } + + /** + * @IsGranted("ROLE_ADMIN") + * @Route("/", name="widget_form_index", methods={"GET"}) + */ + public function index(WidgetFormRepository $widgetFormRepository): Response + { + return $this->render('widget_form/index.html.twig', [ + 'widget_forms' => $widgetFormRepository->findAll(), + ]); + } + + /** + * @IsGranted("ROLE_ADMIN") + * @Route("/new", name="widget_form_new", methods={"GET","POST"}) + */ + public function new(Request $request): Response + { + $widgetForm = new WidgetForm(); + $form = $this->createForm(WidgetFormType::class, $widgetForm); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $entityManager = $this->getDoctrine()->getManager(); + $entityManager->persist($widgetForm); + $entityManager->flush(); + + return $this->redirectToRoute('widget_form_index'); + } + + return $this->render('widget_form/new.html.twig', [ + 'widget_form' => $widgetForm, + 'form' => $form->createView(), + ]); + } + + /** + * @IsGranted("ROLE_ADMIN") + * @Route("/{id}/editor", name="widget_form_editor", methods={"GET"}) + */ + public function editor(WidgetForm $widgetForm, Request $request, $id): Response + { + $widgetFormInput = new WidgetFormInput(); + $form = $this->createForm(WidgetFormInputType::class, $widgetFormInput, [ + 'action' => $this->generateUrl('widget_form_input_new', ['id' => $widgetForm->getId()]), + 'method' => 'POST', + ]); + + $form->handleRequest($request); + + return $this->render('widget_form/editor.html.twig', [ + 'form_input' => $widgetForm->getWidgetFormInputs(), + 'widget_form' => $widgetForm, + 'form' => $form->createView(), + ]); + } + + /** + * @Route("/{id}", name="widget_form_show", methods={"GET", "POST"}) + */ + public function show(Request $request, WidgetForm $widgetForm, $id): Response + { + $fields = []; + + switch (base64_decode($request->query->get('ref') ?? $request->request->get('ref'))) { + case 'https://volgograd.sovamed.ru': + $regionId = 92; + $fields['UF_CRM_1539951158'] = 96; + // Волгоград + break; + + case 'https://voronezh.sovamed.ru': + $regionId = 93; + $fields['UF_CRM_1539951158'] = 98; + // Воронеж + break; + + case 'https://wmtmed.ru': + $regionId = 94; + $fields['UF_CRM_1539951158'] = 3018; + // Краснодар + break; + + case 'https://sovenok.sovamed.ru': + $regionId = 95; + $fields['UF_CRM_1539951158'] = 94; + // Совенок + break; + + case 'https://comfort.sovamed.ru': + $regionId = 96; + $fields['UF_CRM_1539951158'] = 94; + // Комфорт + break; + + default: + $regionId = 91; + $fields['UF_CRM_1539951158'] = 94; + // Саратов + break; + } + + if ($request->getMethod() == 'POST' && !empty($request->request->get('fields'))) { + $fields = array_merge($request->request->get('fields'), $fields); + $fields['ASSIGNED_BY_ID'] = 506; + + if (!empty($fields['OPPORTUNITY'])) + $fields['OPPORTUNITY'] = preg_replace('/[^0-9]/', '', $fields['OPPORTUNITY']); + + $this->client->request('POST', $_ENV['BITRIX24_URL'], [ + 'verify_peer' => false, + 'verify_host' => false, + 'base_uri' => 'https://sovamed.bitrix24.ru', + 'headers' => [ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + 'User-Agent' => 'sovamed_bot', + ], + 'query' => ['fields' => $fields] + ]); + + $data = [ + 'requestNumber' => \md5(\time()), + 'requestUrl' => $request->request->get('requestUrl'), + 'requestDate' => \date('d-m-Y H:i:s'), + 'subject' => $fields['TITLE'], + 'sessionId' => $request->query->get('sessionId') ?? $request->request->get('sessionId'), + 'phoneNumber' => $fields['PHONE'][0]['VALUE'], + 'fio' => $fields['NAME'], + 'tag' => str_replace(' ', '_', $fields['TITLE']), + ]; + + if ($request->request->get('utm_source') + && $request->request->get('utm_medium') + && $request->request->get('utm_campaign') + && $request->request->get('utm_content') + && $request->request->get('utm_term')) { + $data['customSources'] = [ + "source" => $request->request->get('utm_source'), + "medium" => $request->request->get('utm_medium'), + "campaign" => $request->request->get('utm_campaign'), + "content" => $request->request->get('utm_content'), + "term" => $request->request->get('utm_term') + ]; + } + + $calltouch = new CalltouchRequest(); + $calltouch->changeRegion($regionId); + $calltouch = $calltouch->create($data); + + return $this->render('widget_form/show.html.twig', [ + 'widget_form' => $widgetForm, + 'renderForm' => false + ]); + } + + return $this->render('widget_form/show.html.twig', [ + 'regionId' => $regionId, + 'widget_form' => $widgetForm, + 'renderForm' => true + ]); + } + + /** + * @IsGranted("ROLE_ADMIN") + * @Route("/{id}/edit", name="widget_form_edit", methods={"GET","POST"}) + */ + public function edit(Request $request, WidgetForm $widgetForm): Response + { + $form = $this->createForm(WidgetFormType::class, $widgetForm); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->getDoctrine()->getManager()->flush(); + + return $this->redirectToRoute('widget_form_index'); + } + + return $this->render('widget_form/edit.html.twig', [ + 'widget_form' => $widgetForm, + 'form' => $form->createView(), + ]); + } + + /** + * @IsGranted("ROLE_ADMIN") + * @Route("/{id}/delete", name="widget_form_delete", methods={"POST"}) + */ + public function delete(Request $request, WidgetForm $widgetForm): Response + { + if ($this->isCsrfTokenValid('delete'.$widgetForm->getId(), $request->request->get('_token'))) { + $entityManager = $this->getDoctrine()->getManager(); + $entityManager->remove($widgetForm); + $entityManager->flush(); + } + + return $this->redirectToRoute('widget_form_index'); + } +} diff --git a/src/Controller/WidgetFormInputController.php b/src/Controller/WidgetFormInputController.php new file mode 100644 index 0000000..d82fb73 --- /dev/null +++ b/src/Controller/WidgetFormInputController.php @@ -0,0 +1,75 @@ +setWidgetForm($widgetForm); + $form = $this->createForm(WidgetFormInputType::class, $widgetFormInput); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $entityManager = $this->getDoctrine()->getManager(); + $entityManager->persist($widgetFormInput); + $entityManager->flush(); + + return $this->redirectToRoute('widget_form_editor', ['id' => $id]); + } + } + + + /** + * @Route("/{id}/edit/{formId}", name="widget_form_input_edit", methods={"GET","POST"}) + */ + public function edit(Request $request, WidgetFormInput $widgetFormInput, $formId): Response + { + $form = $this->createForm(WidgetFormInputType::class, $widgetFormInput); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->getDoctrine()->getManager()->flush(); + + return $this->redirectToRoute('widget_form_editor', ['id' => $formId]); + } + + return $this->render('widget_form_input/edit.html.twig', [ + 'widget_form_input' => $widgetFormInput, + 'form' => $form->createView(), + ]); + } + + /** + * @Route("/{id}/{formId}", name="widget_form_input_delete", methods={"POST"}) + */ + public function delete(Request $request, WidgetFormInput $widgetFormInput, $formId): Response + { + if ($this->isCsrfTokenValid('delete'.$widgetFormInput->getId(), $request->request->get('_token'))) { + $entityManager = $this->getDoctrine()->getManager(); + $entityManager->remove($widgetFormInput); + $entityManager->flush(); + } + + return $this->redirectToRoute('widget_form_editor', ['id' => $formId]); + } +} diff --git a/src/Entity/.gitignore b/src/Entity/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/src/Entity/AlertSms.php b/src/Entity/AlertSms.php new file mode 100644 index 0000000..0458684 --- /dev/null +++ b/src/Entity/AlertSms.php @@ -0,0 +1,75 @@ +id; + } + + public function getRecord(): ?Record + { + return $this->record; + } + + public function setRecord(?Record $record): self + { + $this->record = $record; + + return $this; + } + + public function getDateCreate(): ?\DateTimeInterface + { + return $this->dateCreate; + } + + public function setDateCreate(\DateTimeInterface $dateCreate): self + { + $this->dateCreate = $dateCreate; + + return $this; + } + + public function getResponse(): ?string + { + return $this->response; + } + + public function setResponse(string $response): self + { + $this->response = $response; + + return $this; + } +} diff --git a/src/Entity/Banner.php b/src/Entity/Banner.php new file mode 100644 index 0000000..e07b2e8 --- /dev/null +++ b/src/Entity/Banner.php @@ -0,0 +1,93 @@ +id; + } + + public function getHref(): ?string + { + return $this->href; + } + + public function setHref(string $href): self + { + $this->href = $href; + + return $this; + } + + public function getSrc(): ?string + { + return $this->src; + } + + public function setSrc(string $src): self + { + $this->src = $src; + + return $this; + } + + public function getActive(): ?bool + { + return $this->active; + } + + public function setActive(bool $active): self + { + $this->active = $active; + + return $this; + } + + public function getCity(): ?City + { + return $this->city; + } + + public function setCity(City $city): self + { + $this->city = $city; + + return $this; + } +} diff --git a/src/Entity/CategoryPage.php b/src/Entity/CategoryPage.php new file mode 100644 index 0000000..2916c76 --- /dev/null +++ b/src/Entity/CategoryPage.php @@ -0,0 +1,100 @@ +pages = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function getActive(): ?bool + { + return $this->active; + } + + public function setActive(bool $active): self + { + $this->active = $active; + + return $this; + } + + /** + * @return Collection|Page[] + */ + public function getPages(): Collection + { + return $this->pages; + } + + public function addPage(Page $page): self + { + if (!$this->pages->contains($page)) { + $this->pages[] = $page; + $page->setCategory($this); + } + + return $this; + } + + public function removePage(Page $page): self + { + if ($this->pages->removeElement($page)) { + // set the owning side to null (unless already changed) + if ($page->getCategory() === $this) { + $page->setCategory(null); + } + } + + return $this; + } +} diff --git a/src/Entity/City.php b/src/Entity/City.php new file mode 100644 index 0000000..8c04386 --- /dev/null +++ b/src/Entity/City.php @@ -0,0 +1,175 @@ +filials = new ArrayCollection(); + $this->reviewSources = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function getTimeZone(): ?int + { + return $this->timeZone; + } + + public function setTimeZone(int $timeZone): self + { + $this->timeZone = $timeZone; + + return $this; + } + + public function getRegionId(): ?int + { + return $this->regionId; + } + + public function setRegionId(int $regionId): self + { + $this->regionId = $regionId; + + return $this; + } + + public function getBanner(): ?Banner + { + return $this->banner; + } + + public function setBanner(Banner $banner): self + { + // set the owning side of the relation if necessary + if ($banner->getCity() !== $this) { + $banner->setCity($this); + } + + $this->banner = $banner; + + return $this; + } + + /** + * @return Collection + */ + public function getFilials(): Collection + { + return $this->filials; + } + + public function addFilial(Filial $filial): static + { + if (!$this->filials->contains($filial)) { + $this->filials->add($filial); + $filial->setCity($this); + } + + return $this; + } + + public function removeFilial(Filial $filial): static + { + if ($this->filials->removeElement($filial)) { + // set the owning side to null (unless already changed) + if ($filial->getCity() === $this) { + $filial->setCity(null); + } + } + + return $this; + } + + /** + * @return Collection + */ + public function getReviewSources(): Collection + { + return $this->reviewSources; + } + + public function addReviewSource(ReviewSource $reviewSource): static + { + if (!$this->reviewSources->contains($reviewSource)) { + $this->reviewSources->add($reviewSource); + $reviewSource->setCity($this); + } + + return $this; + } + + public function removeReviewSource(ReviewSource $reviewSource): static + { + if ($this->reviewSources->removeElement($reviewSource)) { + // set the owning side to null (unless already changed) + if ($reviewSource->getCity() === $this) { + $reviewSource->setCity(null); + } + } + + return $this; + } +} diff --git a/src/Entity/Department.php b/src/Entity/Department.php new file mode 100644 index 0000000..23f90e0 --- /dev/null +++ b/src/Entity/Department.php @@ -0,0 +1,159 @@ +id; + } + + public function getDid(): ?int + { + return $this->did; + } + + public function setDid(string $did): self + { + $this->did = $did; + + return $this; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function getGroupName(): ?string + { + return $this->groupName; + } + + public function setGroupName(string $groupName): self + { + $this->groupName = $groupName; + + return $this; + } + + public function getOnlineMode(): ?bool + { + return $this->onlineMode; + } + + public function setOnlineMode(bool $onlineMode): self + { + $this->onlineMode = $onlineMode; + + return $this; + } + + public function getAlias(): ?string + { + return $this->alias; + } + + public function setAlias(string $alias): self + { + $this->alias = $alias; + + return $this; + } + + public function getActive(): ?bool + { + return $this->active; + } + + public function setActive(bool $active): self + { + $this->active = $active; + + return $this; + } + + public function getMiddleName(): ?string + { + return $this->middleName; + } + + public function setMiddleName(?string $middleName): self + { + $this->middleName = $middleName; + + return $this; + } + + public function isOnlineMode(): ?bool + { + return $this->onlineMode; + } + + public function isActive(): ?bool + { + return $this->active; + } + + public function toArray() { + return get_object_vars($this); + } +} diff --git a/src/Entity/DirectCompany.php b/src/Entity/DirectCompany.php new file mode 100644 index 0000000..7c27a92 --- /dev/null +++ b/src/Entity/DirectCompany.php @@ -0,0 +1,75 @@ +id; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function getCompanyId(): ?string + { + return $this->companyId; + } + + public function setCompanyId(string $companyId): self + { + $this->companyId = $companyId; + + return $this; + } + + public function getCity(): ?string + { + return $this->city; + } + + public function setCity(string $city): self + { + $this->city = $city; + + return $this; + } +} diff --git a/src/Entity/DirectReport.php b/src/Entity/DirectReport.php new file mode 100644 index 0000000..025eae4 --- /dev/null +++ b/src/Entity/DirectReport.php @@ -0,0 +1,160 @@ +id; + } + + public function getDate(): ?\DateTimeInterface + { + return $this->date; + } + + public function setDate(\DateTimeInterface $date): self + { + $this->date = $date; + + return $this; + } + + public function getAdGroupId(): ?string + { + return $this->adGroupId; + } + + public function setAdGroupId(string $adGroupId): self + { + $this->adGroupId = $adGroupId; + + return $this; + } + + public function getCampaignId(): ?string + { + return $this->campaignId; + } + + public function setCampaignId(string $campaignId): self + { + $this->campaignId = $campaignId; + + return $this; + } + + public function getAdId(): ?string + { + return $this->adId; + } + + public function setAdId(string $adId): self + { + $this->adId = $adId; + + return $this; + } + + public function getImpressions(): ?string + { + return $this->impressions; + } + + public function setImpressions(string $impressions): self + { + $this->impressions = $impressions; + + return $this; + } + + public function getClicks(): ?int + { + return $this->clicks; + } + + public function setClicks(int $clicks): self + { + $this->clicks = $clicks; + + return $this; + } + + public function getCost(): ?int + { + return $this->cost; + } + + public function setCost(int $cost): self + { + $this->cost = $cost; + + return $this; + } + + public function getConversions(): ?string + { + return $this->conversions; + } + + public function setConversions(string $conversions): self + { + $this->conversions = $conversions; + + return $this; + } +} diff --git a/src/Entity/Filial.php b/src/Entity/Filial.php new file mode 100644 index 0000000..d6aee84 --- /dev/null +++ b/src/Entity/Filial.php @@ -0,0 +1,207 @@ +reviewSources = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getFid(): ?string + { + return $this->fid; + } + + public function setFid(string $fid): self + { + $this->fid = $fid; + + return $this; + } + + public function getSiteId(): ?int + { + return $this->siteId; + } + + public function setSiteId(string $siteId): self + { + $this->siteId = $siteId; + + return $this; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function getAddress(): ?string + { + return $this->address; + } + + public function setAddress(string $address): self + { + $this->address = $address; + + return $this; + } + + public function getAddressName(): ?string + { + return $this->addressName; + } + + public function setAddressName(?string $addressName): self + { + $this->addressName = $addressName; + + return $this; + } + + public function getCity(): ?City + { + return $this->city; + } + + public function setCity(?City $city): self + { + $this->city = $city; + + return $this; + } + + /** + * @return Collection + */ + public function getReviewSources(): Collection + { + return $this->reviewSources; + } + + public function addReviewSource(ReviewSource $reviewSource): self + { + if (!$this->reviewSources->contains($reviewSource)) { + $this->reviewSources[] = $reviewSource; + $reviewSource->setFilial($this); + } + + return $this; + } + + public function removeReviewSource(ReviewSource $reviewSource): self + { + if ($this->reviewSources->removeElement($reviewSource)) { + // set the owning side to null (unless already changed) + if ($reviewSource->getFilial() === $this) { + $reviewSource->setFilial(null); + } + } + + return $this; + } + + public function getActive(): ?bool + { + return $this->active; + } + + public function setActive(bool $active): self + { + $this->active = $active; + + return $this; + } + + public function getCompany(): ?string + { + return $this->company; + } + + public function setCompany(string $company): self + { + $this->company = $company; + + return $this; + } + + public function isActive(): ?bool + { + return $this->active; + } +} diff --git a/src/Entity/LocationView.php b/src/Entity/LocationView.php new file mode 100644 index 0000000..1f5d11b --- /dev/null +++ b/src/Entity/LocationView.php @@ -0,0 +1,96 @@ +id; + } + + public function getDcode(): ?int + { + return $this->dcode; + } + + public function getDepartment(): ?int + { + return $this->department; + } + + public function getFilial(): ?int + { + return $this->filial; + } + + public function getSpecialistId(): ?int + { + return $this->specialistId; + } + + public function isOnlineMode(): ?bool + { + return $this->onlineMode; + } + + public function isActive(): ?bool + { + return $this->active; + } + + public function getNearestDate(): ?\DateTimeInterface + { + return $this->nearestDate; + } +} diff --git a/src/Entity/Page.php b/src/Entity/Page.php new file mode 100644 index 0000000..3339b8e --- /dev/null +++ b/src/Entity/Page.php @@ -0,0 +1,110 @@ +id; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(string $description): self + { + $this->description = $description; + + return $this; + } + + public function getActive(): ?bool + { + return $this->active; + } + + public function setActive(bool $active): self + { + $this->active = $active; + + return $this; + } + + public function getAlias(): ?string + { + return $this->alias; + } + + public function setAlias(string $alias): self + { + $this->alias = $alias; + + return $this; + } + + public function getCategory(): ?CategoryPage + { + return $this->category; + } + + public function setCategory(?CategoryPage $category): self + { + $this->category = $category; + + return $this; + } +} diff --git a/src/Entity/Price.php b/src/Entity/Price.php new file mode 100644 index 0000000..323a341 --- /dev/null +++ b/src/Entity/Price.php @@ -0,0 +1,96 @@ +id; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function getValue(): ?int + { + return $this->value; + } + + public function setValue(int $value): self + { + $this->value = $value; + + return $this; + } + + public function getPropertyValueId(): ?string + { + return $this->value; + } + + public function setPropertyValueId(string $propertyValueId): self + { + $this->propertyValueId = $propertyValueId; + + return $this; + } + + public function toArray() { + return get_object_vars($this); + } + + public function getDateUpdate(): ?\DateTimeInterface + { + return $this->dateUpdate; + } + + public function setDateUpdate(\DateTimeInterface $dateUpdate): self + { + $this->dateUpdate = $dateUpdate; + + return $this; + } +} diff --git a/src/Entity/PriceDepartment.php b/src/Entity/PriceDepartment.php new file mode 100644 index 0000000..771d72f --- /dev/null +++ b/src/Entity/PriceDepartment.php @@ -0,0 +1,116 @@ +id; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(?string $name): self + { + $this->name = $name; + + return $this; + } + + public function getGroupId(): ?int + { + return $this->groupId; + } + + public function setGroupId(?int $groupId): self + { + $this->groupId = $groupId; + + return $this; + } + + public function getGroupName(): ?string + { + return $this->groupName; + } + + public function setGroupName(?string $groupName): self + { + $this->groupName = $groupName; + + return $this; + } + + public function getDoctCount(): ?int + { + return $this->doctCount; + } + + public function setDoctCount(?int $doctCount): self + { + $this->doctCount = $doctCount; + + return $this; + } + + public function getViewInWeb(): ?bool + { + return $this->viewInWeb; + } + + public function setViewInWeb(bool $viewInWeb): self + { + $this->viewInWeb = $viewInWeb; + + return $this; + } + + public function toArray() { + return get_object_vars($this); + } + +} diff --git a/src/Entity/PriceList.php b/src/Entity/PriceList.php new file mode 100644 index 0000000..2e4ea3e --- /dev/null +++ b/src/Entity/PriceList.php @@ -0,0 +1,297 @@ +id; + } + + public function getKodoper(): ?string + { + return $this->kodoper; + } + + public function getSchname(): ?string + { + return $this->schname; + } + + public function getSpecname(): ?string + { + return $this->specname; + } + + public function getSpeccode(): ?string + { + return $this->speccode; + } + + public function getPriceInfo(): ?array + { + return $this->priceInfo; + } + + public function getDiscpercent(): ?string + { + return $this->discpercent; + } + + public function getDiscprice(): ?string + { + return $this->discprice; + } + + public function getStructname(): ?string + { + return $this->structname; + } + + public function getFname(): ?string + { + return $this->fname; + } + + public function getFilial(): ?int + { + return $this->filial; + } + + public function getComment(): ?string + { + return $this->comment; + } + + public function getMediaId(): ?int + { + return $this->mediaId; + } + + public function getDateUpdate(): ?\DateTimeInterface + { + return $this->dateUpdate; + } + + public function getGroupId(): ?int + { + return $this->groupId; + } + + public function getActive(): bool + { + if ($this->dateUpdate === null) { + return false; + } + + $twoDaysAgo = (new \DateTime())->modify('-2 days'); + + return $this->dateUpdate >= $twoDaysAgo; + } + + public function setKodoper(?string $kodoper): static + { + $this->kodoper = $kodoper; + + return $this; + } + + public function setSchname(?string $schname): static + { + $this->schname = $schname; + + return $this; + } + + public function setSpecname(?string $specname): static + { + $this->specname = $specname; + + return $this; + } + + public function setSpeccode(?string $speccode): static + { + $this->speccode = $speccode; + + return $this; + } + + public function setPriceInfo(?array $priceInfo): static + { + $this->priceInfo = $priceInfo; + + return $this; + } + + public function setDiscpercent(?string $discpercent): static + { + $this->discpercent = $discpercent; + + return $this; + } + + public function setDiscprice(?string $discprice): static + { + $this->discprice = $discprice; + + return $this; + } + + public function setStructname(?string $structname): static + { + $this->structname = $structname; + + return $this; + } + + public function setFname(?string $fname): static + { + $this->fname = $fname; + + return $this; + } + + public function setFilial(?int $filial): static + { + $this->filial = $filial; + + return $this; + } + + public function setComment(?string $comment): static + { + $this->comment = $comment; + + return $this; + } + + public function setMediaId(?int $mediaId): static + { + $this->mediaId = $mediaId; + + return $this; + } + + public function setDateUpdate(\DateTimeInterface $dateUpdate): static + { + $this->dateUpdate = $dateUpdate; + + return $this; + } + + public function setGroupId(?int $groupId): static + { + $this->groupId = $groupId; + + return $this; + } + + public function toArray(): array + { + return [ + 'kodoper' => $this->getKodoper(), + 'schname' => $this->getSchname(), + 'specname' => $this->getSpecname(), + 'speccode' => $this->getSpeccode(), + 'priceInfo' => $this->getPriceInfo(), + 'discpercent' => $this->getDiscpercent(), + 'discprice' => $this->getDiscprice(), + 'structname' => $this->getStructname(), + 'fname' => $this->getFname(), + 'filial' => $this->getFilial(), + 'comment' => $this->getComment(), + 'mediaId' => $this->getMediaId(), + 'dateUpdate' => $this->getDateUpdate() ? $this->getDateUpdate()->format('Y-m-d H:i:s') : null, + 'groupId' => $this->getGroupId(), + 'active' => $this->getActive() + ]; + } +} diff --git a/src/Entity/Record.php b/src/Entity/Record.php new file mode 100644 index 0000000..91ccd9a --- /dev/null +++ b/src/Entity/Record.php @@ -0,0 +1,137 @@ +id; + } + + public function getSpecialistId(): ?int + { + return $this->specialistId; + } + + public function setSpecialistId(int $specialistId): self + { + $this->specialistId = $specialistId; + + return $this; + } + + public function getPhone(): ?string + { + return !empty($this->phone)? AES::decrypt($this->phone): $this->phone; + } + + public function setPhone(string $phone): self + { + $this->phone = AES::encrypt($phone); + + return $this; + } + + public function getCreateAt(): ?\DateTimeInterface + { + return $this->createAt; + } + + public function setCreateAt(\DateTimeInterface $createAt): self + { + $this->createAt = $createAt; + + return $this; + } + + public function getHash(): ?string + { + return $this->hash; + } + + public function setHash(string $hash): self + { + $this->hash = md5($hash); + + return $this; + } + + public function getReserve(): ?array + { + return $this->reserve; + } + + public function setReserve(array $reserve): self + { + $this->reserve = $reserve; + + return $this; + } + + public function getAlertSms(): ?AlertSms + { + return $this->alertSms; + } + + public function setAlertSms(?AlertSms $alertSms): self + { + // unset the owning side of the relation if necessary + if ($alertSms === null && $this->alertSms !== null) { + $this->alertSms->setRecord(null); + } + + // set the owning side of the relation if necessary + if ($alertSms !== null && $alertSms->getRecord() !== $this) { + $alertSms->setRecord($this); + } + + $this->alertSms = $alertSms; + + return $this; + } +} diff --git a/src/Entity/Review.php b/src/Entity/Review.php new file mode 100644 index 0000000..38c767a --- /dev/null +++ b/src/Entity/Review.php @@ -0,0 +1,167 @@ +id; + } + + public function getBitrixId(): ?int + { + return $this->externalId; + } + + public function setBitrixId(int $externalId): self + { + $this->externalId = $externalId; + + return $this; + } + + public function getSpecialistId(): ?int + { + return $this->specialistId; + } + + public function setSpecialistId(int $specialistId): self + { + $this->specialistId = $specialistId; + + return $this; + } + + + public function getActive(): ?bool + { + return $this->active; + } + + public function setActive(bool $active): self + { + $this->active = $active; + + return $this; + } + + public function getDateCreate(): ?\DateTimeInterface + { + return $this->dateCreate; + } + + public function setDateCreate(\DateTimeInterface $dateCreate): self + { + $this->dateCreate = $dateCreate; + + return $this; + } + + public function getMessage(): ?string + { + return $this->message; + } + + public function setMessage(string $message): self + { + $this->message = $message; + + return $this; + } + + public function getAuthor(): ?string + { + return $this->author; + } + + public function setAuthor(string $author): self + { + $this->author = $author; + + return $this; + } + + public function getRating(): ?float + { + return $this->rating; + } + + public function setRating(float $rating): self + { + $this->rating = $rating; + + return $this; + } + + public function getSource(): ?string + { + return $this->source; + } + + public function setSource(?string $source): self + { + $this->source = $source; + + return $this; + } + + public function toArray() { + return get_object_vars($this); + } +} diff --git a/src/Entity/ReviewSource.php b/src/Entity/ReviewSource.php new file mode 100644 index 0000000..8a7de62 --- /dev/null +++ b/src/Entity/ReviewSource.php @@ -0,0 +1,150 @@ +id; + } + + public function getCity(): ?City + { + return $this->city; + } + + public function setCity(City $city): self + { + $this->city = $city; + + return $this; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function getCountRow(): ?int + { + return $this->countRow; + } + + public function setCountRow(int $countRow): self + { + $this->countRow = $countRow; + + return $this; + } + + public function getActive(): ?bool + { + return $this->active; + } + + public function setActive(bool $active): self + { + $this->active = $active; + + return $this; + } + + public function getRating(): ?float + { + return $this->rating; + } + + public function setRating(float $rating): self + { + $this->rating = $rating; + + return $this; + } + + public function getDateCreate(): ?\DateTimeInterface + { + return $this->dateCreate; + } + + public function setDateCreate(\DateTimeInterface $dateCreate): self + { + $this->dateCreate = $dateCreate; + + return $this; + } + + public function isActive(): ?bool + { + return $this->active; + } + + public function getFilial(): ?Filial + { + return $this->filial; + } + + public function setFilial(?Filial $filial): static + { + $this->filial = $filial; + + return $this; + } +} diff --git a/src/Entity/SpecialistView.php b/src/Entity/SpecialistView.php new file mode 100644 index 0000000..bf1ebec --- /dev/null +++ b/src/Entity/SpecialistView.php @@ -0,0 +1,233 @@ +updated; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getName(): ?string + { + return $this->name; + } + + public function getFio(): ?array + { + $fio = explode(' ', trim($this->name)); + + if (count($fio) < 3) { + $fio[] = ''; + } + + return $fio; + } + + public function getKinder(): ?string + { + return $this->kinder; + } + + public function getSpeciality(): ?string + { + return $this->speciality; + } + + public function getCategory(): ?string + { + return $this->category; + } + + public function getExperience(): ?string + { + return $this->experience; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function getImg(): ?string + { + return $this->id; + } + + public function getAlias(): ?string + { + return $this->alias; + } + + public function getDcode(): ?string + { + return $this->dcode; + } + + public function getSType(): ?int + { + return $this->sType; + } + + public function isActive(): bool + { + return $this->active; + } + + public function isAcceptsDms(): ?bool + { + return $this->acceptsDms; + } + + public function isInfoclinica(): ?bool + { + return $this->infoclinica; + } + + public function getRegionId(): ?int + { + return $this->regionId; + } + + public function getDegree(): ?string + { + return $this->degree; + } + + public function getKodoper(): ?array + { + return $this->kodoper; + } + + public function addSpecialistMoreService( + SpecialistMoreService $specialistMoreService + ): void { + $this->specialistMoreService = $specialistMoreService; + } + + public function getSpecialistMore(): SpecialistMoreService + { + return $this->specialistMoreService->setSpecialist($this->id, $this->kodoper); + } + + public function toArray() + { + return [ + 'name' => $this->getName(), + 'kinder' => $this->getKinder(), + 'experience' => $this->getExperience(), + 'category' => $this->getCategory(), + 'description' => $this->getDescription(), + 'speciality' => $this->getSpeciality(), + 'alias' => $this->getAlias(), + 'isAcceptsDms' => $this->isAcceptsDms(), + 'degree' => $this->getDegree(), + 'updated' => $this->getUpdated() + ]; + } +} diff --git a/src/Entity/User.php b/src/Entity/User.php new file mode 100644 index 0000000..cf287ee --- /dev/null +++ b/src/Entity/User.php @@ -0,0 +1,268 @@ +calltouches = new ArrayCollection(); + $this->createdAt = new \DateTime(); + $this->lastActivityAt = new \DateTime(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getEmail(): ?string + { + return \hex2bin($this->email); + } + + public function setEmail(string $email): self + { + $this->email = \bin2hex($email); + + return $this; + } + + /** + * A visual identifier that represents this user. + * + * @see UserInterface + */ + public function getUsername(): string + { + return (string) \hex2bin($this->email); + } + + /** + * @see UserInterface + */ + public function getRoles(): array + { + $roles = $this->roles; + // guarantee every user at least has ROLE_USER + $roles[] = 'ROLE_USER'; + + return array_unique($roles); + } + + public function setRoles(array $roles): self + { + $this->roles = $roles; + + return $this; + } + + /** + * Получает реальные роли пользователя без автоматически добавленного ROLE_USER + * + * @return array + */ + public function getActualRoles(): array + { + return $this->roles; + } + + /** + * @see UserInterface + */ + public function getPassword(): string + { + return $this->password ?? ''; + } + + public function setPassword(string $password): self + { + $this->password = $password; + + return $this; + } + + /** + * Returning a salt is only needed, if you are not using a modern + * hashing algorithm (e.g. bcrypt or sodium) in your security.yaml. + * + * @see UserInterface + */ + public function getSalt(): ?string + { + return null; + } + + /** + * @see UserInterface + */ + public function eraseCredentials() + { + // If you store any temporary, sensitive data on the user, clear it here + // $this->plainPassword = null; + } + + public function getUid(): ?int + { + return $this->uid ?? 0; + } + + public function setUid(?int $uid): self + { + $this->uid = $uid; + + return $this; + } + + public function getToken(): ?string + { + return !empty($this->token)? AES::decrypt($this->token): $this->token; + } + + public function setToken(string $token): self + { + $this->token = AES::encrypt($token); + + return $this; + } + + public function getFullName(): ?string + { + return !empty($this->fullName)? AES::decrypt($this->fullName): $this->fullName; + } + + public function setFullName(string $fullName): self + { + $this->fullName = AES::encrypt($fullName); + + return $this; + } + + public function getPhone(): ?string + { + return !empty($this->phone)? AES::decrypt($this->phone): $this->phone; + } + + public function setPhone(string $phone): self + { + $this->phone = AES::encrypt($phone); + + return $this; + } + + public function getConfirm(): ?bool + { + return $this->confirm; + } + + public function setConfirm(bool $confirm): self + { + $this->confirm = $confirm; + + return $this; + } + + public function getCreatedAt(): ?\DateTimeInterface + { + return $this->createdAt; + } + + public function setCreatedAt(?\DateTimeInterface $createdAt): self + { + $this->createdAt = $createdAt; + + return $this; + } + + public function getLastActivityAt(): ?\DateTimeInterface + { + return $this->lastActivityAt; + } + + public function setLastActivityAt(?\DateTimeInterface $lastActivityAt): self + { + $this->lastActivityAt = $lastActivityAt; + + return $this; + } + + /** + * Обновляет время последней активности пользователя + */ + public function updateLastActivity(): self + { + $this->lastActivityAt = new \DateTime(); + return $this; + } +} diff --git a/src/Entity/Usrlog.php b/src/Entity/Usrlog.php new file mode 100644 index 0000000..08f9336 --- /dev/null +++ b/src/Entity/Usrlog.php @@ -0,0 +1,109 @@ +id; + } + + public function getPcode(): ?string + { + return $this->pcode; + } + + public function setPcode(string $pcode): self + { + $this->pcode = $pcode; + + return $this; + } + + public function getAgent(): ?string + { + return $this->agent; + } + + public function setAgent(string $agent): self + { + $this->agent = $agent; + + return $this; + } + + public function getClientIp(): ?string + { + return $this->clientIp; + } + + public function setClientIp(string $clientIp): self + { + $this->clientIp = $clientIp; + + return $this; + } + + public function getMethod(): ?string + { + return $this->method; + } + + public function setMethod(string $method): self + { + $this->method = $method; + + return $this; + } + + public function getCreatedAt(): ?\DateTimeInterface + { + return $this->createdAt; + } + + public function setCreatedAt(\DateTimeInterface $createdAt): self + { + $this->createdAt = $createdAt; + + return $this; + } +} diff --git a/src/Entity/WidgetForm.php b/src/Entity/WidgetForm.php new file mode 100644 index 0000000..34fc03a --- /dev/null +++ b/src/Entity/WidgetForm.php @@ -0,0 +1,88 @@ +widgetFormInputs = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + /** + * @return Collection|WidgetFormInput[] + */ + public function getWidgetFormInputs(): Collection + { + $expr = Criteria::expr(); + $criteria = Criteria::create(); + $criteria->orderBy(['sort' => Criteria::ASC]); + + return $this->widgetFormInputs->matching($criteria); + } + + public function addWidgetFormInput(WidgetFormInput $widgetFormInput): self + { + if (!$this->widgetFormInputs->contains($widgetFormInput)) { + $this->widgetFormInputs[] = $widgetFormInput; + $widgetFormInput->setWidgetForm($this); + } + + return $this; + } + + public function removeWidgetFormInput(WidgetFormInput $widgetFormInput): self + { + if ($this->widgetFormInputs->removeElement($widgetFormInput)) { + // set the owning side to null (unless already changed) + if ($widgetFormInput->getWidgetForm() === $this) { + $widgetFormInput->setWidgetForm(null); + } + } + + return $this; + } +} diff --git a/src/Entity/WidgetFormInput.php b/src/Entity/WidgetFormInput.php new file mode 100644 index 0000000..e55f58c --- /dev/null +++ b/src/Entity/WidgetFormInput.php @@ -0,0 +1,109 @@ +id; + } + + public function getText(): ?string + { + return $this->text; + } + + public function setText(string $text): self + { + $this->text = $text; + + return $this; + } + + public function getType(): ?string + { + return $this->type; + } + + public function setType(string $type): self + { + $this->type = $type; + + return $this; + } + + public function getBitrix24Id(): ?string + { + return $this->bitrix24Id; + } + + public function setBitrix24Id(string $bitrix24Id): self + { + $this->bitrix24Id = $bitrix24Id; + + return $this; + } + + public function getWidgetForm(): ?WidgetForm + { + return $this->widgetForm; + } + + public function setWidgetForm(?WidgetForm $widgetForm): self + { + $this->widgetForm = $widgetForm; + + return $this; + } + + public function getSort(): ?int + { + return $this->sort; + } + + public function setSort(int $sort): self + { + $this->sort = $sort; + + return $this; + } +} diff --git a/src/EventListener/SessionIdQueryCookieListener.php b/src/EventListener/SessionIdQueryCookieListener.php new file mode 100644 index 0000000..58c449b --- /dev/null +++ b/src/EventListener/SessionIdQueryCookieListener.php @@ -0,0 +1,55 @@ + 'onKernelResponse']; + } + + public function onKernelResponse(ResponseEvent $event): void + { + if (!$event->isMainRequest()) { + return; + } + + $request = $event->getRequest(); + if (!$request->query->has(self::PARAM)) { + return; + } + + $value = trim((string) $request->query->get(self::PARAM)); + if ($value === '' || \strlen($value) > self::MAX_LEN) { + return; + } + + $cookie = Cookie::create( + self::PARAM, + $value, + 0, + '/', + null, + $request->isSecure(), + false, + false, + Cookie::SAMESITE_LAX + ); + + $event->getResponse()->headers->setCookie($cookie); + } +} diff --git a/src/EventListener/UserActivityListener.php b/src/EventListener/UserActivityListener.php new file mode 100644 index 0000000..842e8c7 --- /dev/null +++ b/src/EventListener/UserActivityListener.php @@ -0,0 +1,57 @@ +tokenStorage = $tokenStorage; + $this->entityManager = $entityManager; + } + + public static function getSubscribedEvents(): array + { + return [ + KernelEvents::REQUEST => ['onKernelRequest', 0], + ]; + } + + public function onKernelRequest(RequestEvent $event): void + { + if (!$event->isMainRequest()) { + return; + } + + $token = $this->tokenStorage->getToken(); + if (!$token) { + return; + } + + $user = $token->getUser(); + if (!$user instanceof User) { + return; + } + + // Обновляем время последней активности пользователя + // Обновляем не чаще чем раз в минуту, чтобы не нагружать базу данных + $lastActivity = $user->getLastActivityAt(); + $now = new \DateTime(); + + if (!$lastActivity || ($now->getTimestamp() - $lastActivity->getTimestamp()) >= 60) { + $user->updateLastActivity(); + $this->entityManager->persist($user); + $this->entityManager->flush(); + } + } +} diff --git a/src/EventListener/UsrlogAuthListener.php b/src/EventListener/UsrlogAuthListener.php new file mode 100644 index 0000000..c097ff7 --- /dev/null +++ b/src/EventListener/UsrlogAuthListener.php @@ -0,0 +1,63 @@ +usrlogRepository = $usrlogRepository; + $this->logger = $logger; + } + + public static function getSubscribedEvents(): array + { + return [ + SecurityEvents::INTERACTIVE_LOGIN => 'onLoginSuccess', + ]; + } + + public function onLoginSuccess(InteractiveLoginEvent $event): void + { + $token = $event->getAuthenticationToken(); + $user = $token->getUser(); + if (!$user instanceof User) { + return; + } + + $request = $event->getRequest(); + $agent = (string) ($request->headers->get('User-Agent') ?? 'unknown'); + $clientIp = (string) ($request->getClientIp() ?? 'unknown'); + $pcode = (string) $user->getUid(); + + $usrlog = new Usrlog(); + $usrlog + ->setPcode($pcode) + ->setAgent($agent) + ->setClientIp($clientIp) + ->setMethod('вход') + ; + + try { + $this->usrlogRepository->add($usrlog); + } catch (\Throwable $e) { + $this->logger->error('USRLOG: failed to save login log row', [ + 'message' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + ]); + } + } + +} diff --git a/src/Form/BannerType.php b/src/Form/BannerType.php new file mode 100644 index 0000000..31d4bad --- /dev/null +++ b/src/Form/BannerType.php @@ -0,0 +1,61 @@ +add('href') + ->add('file', FileType::class, [ + 'label' => 'image', + 'mapped' => false, + 'required' => true, + 'constraints' => [ + new File([ + 'maxSize' => '5024k', + 'mimeTypes' => [ + 'image/jpeg', + 'image/gif', + 'image/jpg', + 'image/png', + ], + 'mimeTypesMessage' => 'Please upload a valid image', + ]) + ], + ]) + + ->add('city', EntityType::class, [ + 'class' => City::class, + 'query_builder' => function (EntityRepository $er) { + return $er->createQueryBuilder('c'); + }, + 'choice_label' => 'name', + 'choice_value' => 'region_id', + 'mapped' => true, + 'multiple' => false, + 'empty_data' => null, + 'required' => true + ]) + ->add('active') + ; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => Banner::class, + ]); + } +} diff --git a/src/Form/CategoryPageType.php b/src/Form/CategoryPageType.php new file mode 100644 index 0000000..4e20e47 --- /dev/null +++ b/src/Form/CategoryPageType.php @@ -0,0 +1,26 @@ +add('name') + ->add('active') + ; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => CategoryPage::class, + ]); + } +} diff --git a/src/Form/CityType.php b/src/Form/CityType.php new file mode 100644 index 0000000..330b9d6 --- /dev/null +++ b/src/Form/CityType.php @@ -0,0 +1,40 @@ +add('name') + ->add('timeZone') + ->add('filial', EntityType::class, [ + 'class' => Filial::class, + 'query_builder' => function (EntityRepository $er) { + return $er->createQueryBuilder('f'); + }, + 'choice_label' => 'address', + 'mapped' => true, + 'multiple' => false, + 'empty_data' => null, + 'required' => true + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => City::class, + ]); + } +} diff --git a/src/Form/DepartmentType.php b/src/Form/DepartmentType.php new file mode 100644 index 0000000..b685407 --- /dev/null +++ b/src/Form/DepartmentType.php @@ -0,0 +1,25 @@ +add('middleName') + ; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => Department::class, + ]); + } +} diff --git a/src/Form/PageType.php b/src/Form/PageType.php new file mode 100644 index 0000000..c3e000b --- /dev/null +++ b/src/Form/PageType.php @@ -0,0 +1,48 @@ +add('name') + ->add('description', CKEditorType::class) + ->add('active') + ->add('alias') + ->add('category', EntityType::class, [ + 'class' => CategoryPage::class, + 'query_builder' => function (EntityRepository $er) { + return $er + ->createQueryBuilder('c') + ->where('c.active in(:a)') + ->setParameter('a', true) + ->orderBy('c.id', 'ASC'); + }, + 'choice_label' => 'name', + 'choice_value' => 'id', + 'mapped' => false, + 'multiple' => false, + 'empty_data' => [], + 'required' => false + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => Page::class, + ]); + } +} diff --git a/src/Form/PriceListAdminFormType.php b/src/Form/PriceListAdminFormType.php new file mode 100644 index 0000000..8c29f77 --- /dev/null +++ b/src/Form/PriceListAdminFormType.php @@ -0,0 +1,69 @@ +add('schname', TextType::class, [ + 'empty_data' => null, + 'mapped' => true, + 'required' => false + ]) + ->add('kodoper', TextType::class, [ + 'empty_data' => null, + 'mapped' => true, + 'required' => false + ]) + ->add('groupId', EntityType::class, [ + 'class' => PriceDepartment::class, + 'query_builder' => function (EntityRepository $er) { + return $er->createQueryBuilder('d') + ->orderBy('d.name', 'ASC'); + }, + 'choice_value' => 'groupId', + 'choice_label' => 'name', + 'placeholder' => 'Департамент', + 'empty_data' => null, + 'mapped' => false, + 'required' => true + ]) + ->add('filial', EntityType::class, [ + 'class' => Filial::class, + 'query_builder' => function (EntityRepository $er) { + return $er + ->createQueryBuilder('f') + ->where('f.fid <> :fid') + ->setParameter('fid', 0) + ->orderBy('f.address', 'DESC'); + }, + 'choice_label' => 'address', + 'choice_value' => 'fid', + 'placeholder' => 'Все филиалы', + 'mapped' => false, + 'multiple' => false, + 'empty_data' => null, + 'required' => false + ]); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => PriceList::class, + ]); + } +} diff --git a/src/Form/PriceListFormType.php b/src/Form/PriceListFormType.php new file mode 100644 index 0000000..ffccde7 --- /dev/null +++ b/src/Form/PriceListFormType.php @@ -0,0 +1,70 @@ +add('schname', TextType::class, [ + 'empty_data' => null, + 'mapped' => true, + 'required' => false + ]) + ->add('kodoper', TextType::class, [ + 'empty_data' => null, + 'mapped' => true, + 'required' => false + ]) + ->add('groupId', EntityType::class, [ + 'class' => PriceDepartment::class, + 'query_builder' => function (EntityRepository $er) { + return $er->createQueryBuilder('d') + ->orderBy('d.name', 'ASC'); + }, + 'choice_value' => 'groupId', + 'choice_label' => 'name', + 'placeholder' => 'Все специализации', + 'empty_data' => null, + 'mapped' => false, + 'required' => false + ]) + ->add('filial', EntityType::class, [ + 'class' => Filial::class, + 'query_builder' => function (EntityRepository $er) { + return $er + ->createQueryBuilder('f') + ->where('f.fid <> :fid') + ->setParameter('fid', 0) + ->andWhere('f.address LIKE :address') + ->setParameter('address', '%' . Region::getCurrentName() . '%') + ->orderBy('f.address', 'DESC'); + }, + 'choice_label' => 'address', + 'choice_value' => 'fid', + 'mapped' => false, + 'multiple' => false, + 'empty_data' => null, + 'required' => false + ]); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => PriceList::class, + ]); + } +} diff --git a/src/Form/ReferenceType.php b/src/Form/ReferenceType.php new file mode 100644 index 0000000..e3178da --- /dev/null +++ b/src/Form/ReferenceType.php @@ -0,0 +1,105 @@ + 1, + 'Отправка в налоговую инспекцию' => 2, + ] : [ + 'Лично в клинике' => 1, + ]; + + $builder + ->add('autorName', TextType::class, ['mapped' => false]) + ->add('birthDate', TextType::class, ['mapped' => false]) + ->add('inn', TextType::class, [ + 'mapped' => false, + ]) + ->add('phone', TextType::class, [ + 'mapped' => false, + ]) + ->add('periodFirst', DateType::Class, array( + 'mapped' => false, + 'format' => 'yyyy-MM-dd', + 'widget' => 'single_text', + )) + ->add('periodLast', DateType::Class, array( + 'mapped' => false, + 'format' => 'yyyy-MM-dd', + 'widget' => 'single_text', + )) + ->add('responsible', ChoiceType::class, [ + 'mapped' => false, + 'expanded' => false, + 'choices' => [ + 'себя' => 1, + 'другого' => 0, + ] + ]) + ->add('sending', ChoiceType::class, [ + 'mapped' => false, + 'expanded' => false, + 'choices' => $sendingChoices, + 'data' => 1, + ]) + ->add('filial', EntityType::class, [ + 'class' => Filial::class, + 'query_builder' => function (EntityRepository $er) { + return $er + ->createQueryBuilder('f') + ->where('f.fid <> :fid') + ->setParameter('fid', 0) + ->orderBy('f.city', 'ASC'); + }, + 'choice_label' => 'address', + 'choice_value' => 'address', + 'mapped' => false, + 'multiple' => false, + 'empty_data' => [], + 'required' => false + ]) + ->add('filialSending', EntityType::class, [ + 'class' => Filial::class, + 'query_builder' => function (EntityRepository $er) { + return $er + ->createQueryBuilder('f') + ->where('f.fid <> :fid') + ->setParameter('fid', 0) + ->orderBy('f.city', 'ASC'); + }, + 'choice_label' => 'address', + 'choice_value' => 'address', + 'mapped' => false, + 'multiple' => false, + 'empty_data' => [], + 'required' => false + ]); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => User::class, + 'isAuthorized' => false, + ]); + } +} diff --git a/src/Form/RefundType.php b/src/Form/RefundType.php new file mode 100644 index 0000000..f8947a3 --- /dev/null +++ b/src/Form/RefundType.php @@ -0,0 +1,81 @@ +add('fio', TextType::class, [ + 'mapped' => false, + 'required' => true, + 'label' => 'ФИО:' + ]) + ->add('passport_serial', TextType::class, [ + 'mapped' => false, + 'required' => true, + 'label' => 'Паспорт: (серия номер)', + 'attr' => [ + 'data-controller' => 'inputMask', + 'data-mask' => '9999 999999', + ] + ]) + ->add('passport_issued', TextareaType::class, [ + 'mapped' => false, + 'required' => true, + 'label' => 'Выдан:' + ]) + ->add('passport_date', DateType::Class, array( + 'mapped' => false, + 'format' => 'yyyy-MM-dd', + 'widget' => 'single_text', + 'required' => true, + 'label' => 'Дата выдачи:' + )) + ->add('sum', TextType::class, [ + 'mapped' => false, + 'required' => true, + 'label' => 'Сумма возврата:' + ]) + ->add('refund_date', DateType::Class, array( + 'mapped' => false, + 'required' => true, + 'label' => 'Дата платежа:', + 'format' => 'yyyy-MM-dd', + 'widget' => 'single_text', + )) + ->add('refund_bases', ChoiceType::class, [ + 'mapped' => false, + 'required' => true, + 'label' => 'Причина возврата:', + 'expanded' => false, + 'choices' => [ + 'Отказ от консультация до ее начала' => 'Отказ от консультация до ее начала', + 'Консультация не состоялась по техническим причинам' => 'Консультация не состоялась по техническим причинам', + ] + ]); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => NULL, + ]); + } +} diff --git a/src/Form/RegistrationFormType.php b/src/Form/RegistrationFormType.php new file mode 100644 index 0000000..048430b --- /dev/null +++ b/src/Form/RegistrationFormType.php @@ -0,0 +1,71 @@ +add('agreeTerms', CheckboxType::class, [ + 'mapped' => false, + 'constraints' => [ + new IsTrue([ + 'message' => 'You should agree to our terms.', + ]), + ], + ]) + ->add('firstName', TextType::class, ['mapped' => false]) + ->add('lastName', TextType::class, ['mapped' => false]) + ->add('middleName', TextType::class, ['mapped' => false]) + ->add('birthDate', BirthdayType::class, ['mapped' => false]) + ->add('email') + ->add('gender', ChoiceType::class, [ + 'mapped' => false, + 'expanded' => true, + 'choices' => [ + 'male' => 1, + 'famale' => 2, + ] + ]) + ->add('phone') + ->add('plainPassword', PasswordType::class, [ + // instead of being set onto the object directly, + // this is read and encoded in the controller + 'mapped' => false, + 'attr' => ['autocomplete' => 'new-password'], + 'constraints' => [ + new NotBlank([ + 'message' => 'Please enter a password', + ]), + new Length([ + 'min' => 7, + 'minMessage' => 'Your password should be at least {{ limit }} characters', + // max length allowed by Symfony for security reasons + 'max' => 4096, + ]), + ], + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => User::class, + ]); + } +} diff --git a/src/Form/ReportFormType.php b/src/Form/ReportFormType.php new file mode 100644 index 0000000..68a0354 --- /dev/null +++ b/src/Form/ReportFormType.php @@ -0,0 +1,36 @@ +add('filial', EntityType::class, [ + 'class' => Filial::class, + 'query_builder' => function (EntityRepository $er) { + return $er + ->createQueryBuilder('f') + ->groupBy('f.fid') + ->orderBy('f.fid', 'ASC'); + }, + 'choice_label' => function ($data) { + return $data->getAddress(); + }, + 'choice_value' => 'siteId', + 'mapped' => false, + 'multiple' => false, + 'empty_data' => false, + 'required' => true + ]) + ; + // ->add('smsCode',TextType::class, ['mapped' => false]); + } +} diff --git a/src/Form/ReviewSourceType.php b/src/Form/ReviewSourceType.php new file mode 100644 index 0000000..93e9858 --- /dev/null +++ b/src/Form/ReviewSourceType.php @@ -0,0 +1,66 @@ +add('name', ChoiceType::class, [ + 'mapped' => true, + 'expanded' => false, + 'choices' => [ + 'Яндекс карта' => 'YandexMap', + 'Google' => 'Google', + 'ProDoctorov(Клиники)' => 'ProDoctorov', + 'ProDoctorov(Врачи)' => 'ProDoctorovSpecialists', + '2Gis' => '2Gis', + 'Zoon' => 'Zoon', + ] + ]) + ->add('countRow') + ->add('active') + ->add('rating') + ->add('city', EntityType::class, [ + 'class' => City::class, + 'query_builder' => function (EntityRepository $er) { + return $er->createQueryBuilder('f'); + }, + 'choice_label' => 'name', + 'mapped' => true, + 'multiple' => false, + 'empty_data' => null, + 'required' => true + ]) + ->add('filial', EntityType::class, [ + 'class' => Filial::class, + 'query_builder' => function (EntityRepository $er) { + return $er->createQueryBuilder('f'); + }, + 'choice_label' => 'address', + 'mapped' => true, + 'multiple' => false, + 'empty_data' => null, + 'required' => true + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => ReviewSource::class, + ]); + } +} diff --git a/src/Form/SettingType.php b/src/Form/SettingType.php new file mode 100644 index 0000000..a6aa057 --- /dev/null +++ b/src/Form/SettingType.php @@ -0,0 +1,43 @@ +add('plainPassword', PasswordType::class, [ + // instead of being set onto the object directly, + // this is read and encoded in the controller + 'mapped' => false, + 'attr' => ['autocomplete' => 'new-password'], + 'constraints' => [ + new NotBlank([ + 'message' => 'Please enter a password', + ]), + new Length([ + 'min' => 7, + 'minMessage' => 'Your password should be at least {{ limit }} characters', + // max length allowed by Symfony for security reasons + 'max' => 4096, + ]), + ], + ]); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => User::class, + ]); + } +} diff --git a/src/Form/SpecialistAdminSearchType.php b/src/Form/SpecialistAdminSearchType.php new file mode 100644 index 0000000..509e2bd --- /dev/null +++ b/src/Form/SpecialistAdminSearchType.php @@ -0,0 +1,44 @@ +add('name', null, [ + 'required' => false + ]) + ->add('filial', EntityType::class, [ + 'class' => Filial::class, + 'query_builder' => function (EntityRepository $er) { + return $er + ->createQueryBuilder('f') + ->orderBy('f.id', 'ASC'); + }, + 'choice_label' => 'name', + 'mapped' => false, + 'multiple' => true, + 'empty_data' => [], + 'required' => false + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => Specialist::class, + ]); + } +} diff --git a/src/Form/SpecialistSearchType.php b/src/Form/SpecialistSearchType.php new file mode 100644 index 0000000..523a27c --- /dev/null +++ b/src/Form/SpecialistSearchType.php @@ -0,0 +1,121 @@ +add('onlineMode', CheckboxType::class, [ + 'label' => 'Онлайн консультация', + 'mapped' => false, + 'required' => false + ]) + ->add('name', null, [ + 'mapped' => false, + 'required' => false + ]) + ->add('kinder', ChoiceType::class, [ + 'choices' => [ + 'Детский врач' => 1, + ], + 'placeholder' => 'Взрослый врач', + 'mapped' => false, + 'empty_data' => null, + 'required' => false + ]) + ->add('department', EntityType::class, [ + 'class' => Department::class, + 'query_builder' => function (EntityRepository $er) use ($options) { + $qb = $er->createQueryBuilder('d') + ->distinct() + ->innerJoin('App\Entity\LocationView', 'l', 'WITH', 'l.department = d.did AND l.active = true') + ->innerJoin('App\Entity\SpecialistView', 's', 'WITH', 's.id = l.specialistId AND s.active = true') + ->leftJoin('App\Entity\Filial', 'f', 'WITH', 'f.fid = l.filial') + ->where('f.address LIKE :address') + ->andWhere('d.did <> :did') + ->setParameter('did', 0) + ->setParameter('address', '%' . Region::getCurrentName() . '%'); + + // Добавляем фильтр по региону, если он указан + if (!empty($options['regionId']) && $options['regionId'] > 0) { + $qb->andWhere('s.regionId = :regionId') + ->setParameter('regionId', $options['regionId']); + } + + // Добавляем фильтр по детским специализациям, если выбран "Детский врач" + if (!empty($options['kinder']) && $options['kinder'] == 1) { + $qb->andWhere('s.sType = :sType') + ->setParameter('sType', 1); + } + + return $qb->orderBy('d.name', 'ASC'); + }, + 'choice_value' => 'did', + 'choice_label' => 'name', + 'placeholder' => 'Все специализации', + 'empty_data' => null, + 'mapped' => false, + 'required' => false + ]) + ->add('filial', EntityType::class, [ + 'class' => Filial::class, + 'query_builder' => function (EntityRepository $er) { + return $er + ->createQueryBuilder('f') + ->where("f.address LIKE :address and f.active = true and f.fid <> 0") + ->setParameter('address', '%' . Region::getCurrentName() . '%') + ->orderBy('f.id', 'ASC'); + }, + 'choice_value' => 'fid', + 'choice_label' => 'name', + 'mapped' => false, + 'multiple' => true, + 'empty_data' => [], + 'required' => false + ]) + ->add('category', ChoiceType::class, [ + 'choices' => [ + 'Первая' => 'Первая', + 'Вторая' => 'Вторая', + 'Высшая' => 'Высшая' + ], + 'placeholder' => 'Выберите категорию', + 'mapped' => false, + 'multiple' => false, + 'empty_data' => null, + 'required' => false + ]) + ->add('current_date', null, [ + 'mapped' => false + ]) + ->add('order_by', null, [ + 'mapped' => false, + 'required' => false + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => SpecialistView::class, + 'regionId' => null, + 'kinder' => null, + ]); + } +} diff --git a/src/Form/SpecialistType.php b/src/Form/SpecialistType.php new file mode 100644 index 0000000..4f32749 --- /dev/null +++ b/src/Form/SpecialistType.php @@ -0,0 +1,29 @@ +add('sid') + ->add('name') + ->add('onlineMode') + ->add('department') + ->add('filial') + ; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => SpecialistView::class, + ]); + } +} diff --git a/src/Form/UsrlogType.php b/src/Form/UsrlogType.php new file mode 100644 index 0000000..863af5c --- /dev/null +++ b/src/Form/UsrlogType.php @@ -0,0 +1,37 @@ +add('pcode') + ->add('agent', TextareaType::class) + ->add('clientIp', null, [ + 'label' => 'IP клиента', + ]) + ->add('method', ChoiceType::class, [ + 'choices' => [ + 'Вход в систему' => 'вход в систему', + 'Выход' => 'выход', + ], + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Usrlog::class, + ]); + } +} diff --git a/src/Form/WidgetFormInputType.php b/src/Form/WidgetFormInputType.php new file mode 100644 index 0000000..30b3bfd --- /dev/null +++ b/src/Form/WidgetFormInputType.php @@ -0,0 +1,39 @@ +add('text') + ->add('type', ChoiceType::class, [ + 'choices' => [ + 'Строка' => 'text', + 'Телефон' => 'phone', + 'Календаль' => 'date', + 'Коментарий' => 'textarea', + ], + 'empty_data' => null, + 'required' => false + ]) + ->add('bitrix24Id') + ->add('sort') + + ; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => WidgetFormInput::class, + ]); + } +} diff --git a/src/Form/WidgetFormType.php b/src/Form/WidgetFormType.php new file mode 100644 index 0000000..4d71d3a --- /dev/null +++ b/src/Form/WidgetFormType.php @@ -0,0 +1,25 @@ +add('name') + ; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => WidgetForm::class, + ]); + } +} diff --git a/src/Kernel.php b/src/Kernel.php new file mode 100644 index 0000000..655e796 --- /dev/null +++ b/src/Kernel.php @@ -0,0 +1,38 @@ +import('../config/{packages}/*.yaml'); + $container->import('../config/{packages}/'.$this->environment.'/*.yaml'); + + if (is_file(\dirname(__DIR__).'/config/services.yaml')) { + $container->import('../config/services.yaml'); + $container->import('../config/{services}_'.$this->environment.'.yaml'); + } elseif (is_file($path = \dirname(__DIR__).'/config/services.php')) { + (require $path)($container->withPath($path), $this); + } + } + + protected function configureRoutes(RoutingConfigurator $routes): void + { + $routes->import('../config/{routes}/'.$this->environment.'/*.yaml'); + $routes->import('../config/{routes}/*.yaml'); + + if (is_file(\dirname(__DIR__).'/config/routes.yaml')) { + $routes->import('../config/routes.yaml'); + } elseif (is_file($path = \dirname(__DIR__).'/config/routes.php')) { + (require $path)($routes->withPath($path), $this); + } + } +} diff --git a/src/Repository/.gitignore b/src/Repository/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/src/Repository/AlertSmsRepository.php b/src/Repository/AlertSmsRepository.php new file mode 100644 index 0000000..1735161 --- /dev/null +++ b/src/Repository/AlertSmsRepository.php @@ -0,0 +1,21 @@ + + * + * @method City|null find($id, $lockMode = null, $lockVersion = null) + * @method City|null findOneBy(array $criteria, array $orderBy = null) + * @method City[] findAll() + * @method City[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class CityRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, City::class); + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function add(City $entity, bool $flush = true): void + { + $this->_em->persist($entity); + if ($flush) { + $this->_em->flush(); + } + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function remove(City $entity, bool $flush = true): void + { + $this->_em->remove($entity); + if ($flush) { + $this->_em->flush(); + } + } + + public function all() + { + return $this->createQueryBuilder('c') + ->select('c.id, c.name') + ->getQuery() + ->getResult() + ; + } +} diff --git a/src/Repository/DepartmentRepository.php b/src/Repository/DepartmentRepository.php new file mode 100644 index 0000000..2ef2a03 --- /dev/null +++ b/src/Repository/DepartmentRepository.php @@ -0,0 +1,21 @@ + + * + * @method DirectCompany|null find($id, $lockMode = null, $lockVersion = null) + * @method DirectCompany|null findOneBy(array $criteria, array $orderBy = null) + * @method DirectCompany[] findAll() + * @method DirectCompany[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class DirectCompanyRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, DirectCompany::class); + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function add(DirectCompany $entity, bool $flush = true): void + { + $this->_em->persist($entity); + if ($flush) { + $this->_em->flush(); + } + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function remove(DirectCompany $entity, bool $flush = true): void + { + $this->_em->remove($entity); + if ($flush) { + $this->_em->flush(); + } + } +} diff --git a/src/Repository/DirectReportRepository.php b/src/Repository/DirectReportRepository.php new file mode 100644 index 0000000..da5b55b --- /dev/null +++ b/src/Repository/DirectReportRepository.php @@ -0,0 +1,49 @@ + + * + * @method DirectReport|null find($id, $lockMode = null, $lockVersion = null) + * @method DirectReport|null findOneBy(array $criteria, array $orderBy = null) + * @method DirectReport[] findAll() + * @method DirectReport[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class DirectReportRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, DirectReport::class); + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function add(DirectReport $entity, bool $flush = true): void + { + $this->_em->persist($entity); + if ($flush) { + $this->_em->flush(); + } + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function remove(DirectReport $entity, bool $flush = true): void + { + $this->_em->remove($entity); + if ($flush) { + $this->_em->flush(); + } + } +} diff --git a/src/Repository/FilialRepository.php b/src/Repository/FilialRepository.php new file mode 100644 index 0000000..393d406 --- /dev/null +++ b/src/Repository/FilialRepository.php @@ -0,0 +1,31 @@ +createQueryBuilder('filial') + ->where('filial.address LIKE :address') + ->setParameter('address', '%' . $region . '%') + ->orderBy('filial.id') + ->getQuery() + ->getResult(); + } +} diff --git a/src/Repository/LocationViewRepository.php b/src/Repository/LocationViewRepository.php new file mode 100644 index 0000000..8d1e45b --- /dev/null +++ b/src/Repository/LocationViewRepository.php @@ -0,0 +1,108 @@ +createQueryBuilder('l') + ->select('l.id, l.dcode, l.department, l.onlineMode, l.filial, l.active, l.nearestDate, d.name') + ->leftJoin('App\Entity\Department', 'd', 'WITH', 'd.did = l.department') + ->where('l.specialistId = :specialistId') + ->setParameter('specialistId', $specialistId) + ->andWhere('l.active = true') + ->andWhere('l.onlineMode = :onlineMode') + ->setParameter('onlineMode', $onlineMode); + + // Фильтр по отделению (специализации), если указано + if ($department !== null && $department > 0) { + $qb->andWhere('l.department = :department') + ->setParameter('department', $department); + } + + // Фильтр по дате приема (диапазон дат), если указано + if ($startDate !== null) { + $qb->andWhere('l.nearestDate >= :startDate') + ->setParameter('startDate', $startDate); + } + + if ($endDate !== null) { + $qb->andWhere('l.nearestDate <= :endDate') + ->setParameter('endDate', $endDate); + } + + // Сортировка по ближайшему приему: сначала ближайшие даты (ASC), NULL значения в конце + $qb->orderBy('CASE WHEN l.nearestDate IS NULL THEN 1 ELSE 0 END', 'ASC') + ->addOrderBy('l.nearestDate', 'ASC'); + + $mainQuery = $qb->getQuery()->getResult(); + + if (empty($mainQuery)) { + return []; + } + + // Получаем все уникальные filialId из основного запроса + $filialIds = array_filter(array_unique(array_column($mainQuery, 'filial'))); + + if (empty($filialIds)) { + return $mainQuery; + } + + // Запрос для получения данных Filial через EntityManager + $entityManager = $this->getEntityManager(); + $filialQuery = $entityManager->createQueryBuilder() + ->select('f.fid, f.address, f.company, f.addressName') + ->from('App\Entity\Filial', 'f') + ->where('f.fid IN (:filialIds)') + ->setParameter('filialIds', $filialIds) + ->getQuery() + ->getResult(); + + // Создаем мапу для быстрого доступа к данным Filial + $filialMap = []; + foreach ($filialQuery as $filial) { + $filialMap[$filial['fid']] = [ + 'address' => $filial['address'], + 'company' => $filial['company'], + 'addressName' => $filial['addressName'] + ]; + } + + // Объединяем данные + foreach ($mainQuery as &$record) { + $filialId = $record['filial']; + if ($filialId && isset($filialMap[$filialId])) { + $record = array_merge($record, $filialMap[$filialId]); + } else { + // Добавляем пустые значения если filial не найден + $record['address'] = null; + $record['company'] = null; + $record['addressName'] = null; + } + } + + return $mainQuery; + } +} diff --git a/src/Repository/PageRepository.php b/src/Repository/PageRepository.php new file mode 100644 index 0000000..10c3b02 --- /dev/null +++ b/src/Repository/PageRepository.php @@ -0,0 +1,21 @@ +createQueryBuilder('p') + ->addOrderBy('p.filial, p.schname', 'ASC'); + + if (!empty($filters['kodoper'])) { + $kodoper = match (true) { + is_array($filters['kodoper']) => array_filter( + array_map('trim', array_filter($filters['kodoper'], 'is_string')), + function($code) { return $code !== ''; } + ), + is_string($filters['kodoper']) && trim($filters['kodoper']) !== '' => [trim($filters['kodoper'])], + default => [] + }; + + if (!empty($kodoper)) { + // Логируем значения kodoper для отладки + error_log('PriceListRepository: filtering by kodoper=' . json_encode($kodoper)); + + $qb->andWhere('p.kodoper IN (:codes)') + ->setParameter('codes', $kodoper); + } + } + + if (!empty($filters['schname']) && trim($filters['schname']) !== '') { + $qb->andWhere($qb->expr()->like('LOWER(p.schname)', 'LOWER(:schname)')) + ->setParameter('schname', '%' . trim($filters['schname']) . '%'); + } + + if (!empty($filters['filial'])) { + $filialIds = match (true) { + is_array($filters['filial']) => array_filter($filters['filial'], function($id) { + return $id !== null && $id !== ''; + }), + is_string($filters['filial']) && $filters['filial'] !== '' => [$filters['filial']], + is_numeric($filters['filial']) => [(int)$filters['filial']], + default => [] + }; + + if (!empty($filialIds)) { + $qb->andWhere('p.filial IN (:filial)') + ->setParameter('filial', $filialIds); + } + } + + if (!empty($filters['actual'])) { + if ($filters['actual']) { + $qb->andWhere('p.dateUpdate >= :dateUpdate') + ->setParameter('dateUpdate', (new \DateTime()) + ->modify('-2 day') + ->format('Y-m-d 00:00:00') + ); + } + } + + if (!empty($filters['groupId'])) { + $qb->andWhere('p.groupId = :groupId') + ->setParameter('groupId', (int) $filters['groupId']); + } + + return $qb; + } +} diff --git a/src/Repository/PriceRepository.php b/src/Repository/PriceRepository.php new file mode 100644 index 0000000..35f6974 --- /dev/null +++ b/src/Repository/PriceRepository.php @@ -0,0 +1,21 @@ + + * + * @method ReviewSource|null find($id, $lockMode = null, $lockVersion = null) + * @method ReviewSource|null findOneBy(array $criteria, array $orderBy = null) + * @method ReviewSource[] findAll() + * @method ReviewSource[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class ReviewSourceRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, ReviewSource::class); + } + + public function findAll() + { + return $this->findBy([], ['name' => 'ASC']); + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function add(ReviewSource $entity, bool $flush = true): void + { + if (! $entity->getDateCreate()) { + $entity->setDateCreate(new \DateTime()); + } + + $this->_em->persist($entity); + if ($flush) { + $this->_em->flush(); + } + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function remove(ReviewSource $entity, bool $flush = true): void + { + $this->_em->remove($entity); + if ($flush) { + $this->_em->flush(); + } + } + + public function findByCity($cityId) + { + return $this->createQueryBuilder('r') + ->select('r.name, SUM(r.countRow) AS count_row_total, (SUM(r.rating) / COUNT(r.id)) AS rating_total') + ->distinct() + ->where('r.city in (:city)') + ->setParameter('city', $cityId) + ->groupBy('r.name') + ->getQuery() + ->getResult() + ; + } + +} diff --git a/src/Repository/SpecialistViewRepository.php b/src/Repository/SpecialistViewRepository.php new file mode 100644 index 0000000..985f319 --- /dev/null +++ b/src/Repository/SpecialistViewRepository.php @@ -0,0 +1,256 @@ +createQueryBuilder('s') + ->andWhere('s.active = true'); + + $this->applyFilters($qb, $filters); + $this->applyOrderBy($qb, $filters['order_by'] ?? null); + + return $qb; + } + + private function applyFilters(QueryBuilder $qb, array $filters): void + { + foreach ($filters as $key => $value) { + // onlineMode=0 — валидный фильтр; empty(0) в PHP === true, поэтому обрабатываем отдельно. + if ($key === 'onlineMode') { + if ($value === null || $value === '') { + continue; + } + $this->applyOnlineModeFilter($qb, OnlineMode::isOnline($value)); + continue; + } + + if (empty($value)) { + continue; + } + + match ($key) { + 'regionId' => $this->applyRegionFilter($qb, $value), + 'alias' => $this->applyAliasFilter($qb, $value), + 'filial' => $this->applyFilialFilter($qb, $value), + 'dcode' => $this->applyDcodeFilter($qb, $value), + 'kinder' => $this->applyKinderFilter($qb, $value), + 'category' => $this->applyCategoryFilter($qb, $value), + 'department' => $this->applyDepartmentFilter($qb, $value), + 'depAlias' => $this->applyDepAliasFilter($qb, $value), + 'name' => $this->applyNameFilter($qb, $value), + 'current_date' => $this->applyCurrentDateFilter($qb, $value), + default => null // Игнорируем неизвестные фильтры + }; + } + } + + private function applyKinderFilter(QueryBuilder $qb, $sType): void + { + $sType = match (true) { + is_array($sType) => array_map('intval', $sType), + is_string($sType) => [(int) $sType], + is_numeric($sType) => [$sType], + default => [] + }; + + $qb->andWhere('s.sType in (:sType)') + ->setParameter('sType', $sType); + } + + private function applyOnlineModeFilter(QueryBuilder $qb, bool $onlineMode): void + { + $qb->andWhere('EXISTS ( + SELECT 1 FROM App\Entity\LocationView l + WHERE l.specialistId = s.id AND l.onlineMode = :onlineMode + )')->setParameter('onlineMode', $onlineMode); + } + + private function applyRegionFilter(QueryBuilder $qb, $regionId): void + { + $qb->andWhere('s.regionId in (:regionId)') + ->setParameter('regionId', $regionId); + } + + private function applyAliasFilter(QueryBuilder $qb, string $alias): void + { + $qb->andWhere('s.alias = :alias') + ->setParameter('alias', $alias); + } + + private function applyFilialFilter(QueryBuilder $qb, $filial): void + { + $filialIds = array_map('intval', (array)$filial); + + $qb->andWhere('EXISTS ( + SELECT 1 FROM App\Entity\LocationView l2 + WHERE l2.specialistId = s.id AND l2.filial in (:filial) + )')->setParameter('filial', $filialIds); + } + + private function applyDcodeFilter(QueryBuilder $qb, $dcode): void + { + $dcode = match (true) { + is_array($dcode) => array_map('intval', $dcode), + is_string($dcode) => [(int) $dcode], + is_numeric($dcode) => [$dcode], + default => [] + }; + + if (!empty($dcode)) { + $qb->andWhere('EXISTS ( + SELECT 1 FROM App\Entity\LocationView l3 + WHERE l3.specialistId = s.id AND l3.dcode in (:dcode) + )')->setParameter('dcode', $dcode); + } + } + + private function applyCategoryFilter(QueryBuilder $qb, $category): void + { + $qb->andWhere('s.category = :category') + ->setParameter('category', $category); + } + + private function applyDepartmentFilter(QueryBuilder $qb, $department): void + { + $qb->andWhere('EXISTS ( + SELECT 1 FROM App\Entity\LocationView l4 + WHERE l4.specialistId = s.id AND l4.department = :department + )')->setParameter('department', (int) $department); + } + + private function applyDepAliasFilter(QueryBuilder $qb, string $depAlias): void + { + $qb->andWhere('EXISTS ( + SELECT 1 FROM App\Entity\LocationView l5 + JOIN App\Entity\Department d WITH d.did = l5.department + WHERE l5.specialistId = s.id AND d.alias = :depAlias + )')->setParameter('depAlias', $depAlias); + } + + private function applyNameFilter(QueryBuilder $qb, string $name): void + { + $qb->andWhere($qb->expr()->like('LOWER(s.name)', 'LOWER(:search)')) + ->setParameter('search', '%' . trim($name) . '%'); + } + + private function applyCurrentDateFilter(QueryBuilder $qb, $currentDate): void + { + if (empty($currentDate)) { + return; + } + + // Парсим дату: формат "13.01.2026 - 20.01.2026" (с пробелами вокруг дефиса) + // Также поддерживаем варианты: "13.01.2026+-+20.01.2026" или "13.01.2026-20.01.2026" + $dateString = trim($currentDate); + + // Разделяем по дефису с пробелами или без + // Используем регулярное выражение для более точного парсинга + if (preg_match('/^(.+?)\s*[-+]\s*(.+)$/', $dateString, $matches)) { + $startDateStr = trim($matches[1]); + $endDateStr = trim($matches[2]); + } else { + // Если не удалось распарсить, пробуем просто разделить по дефису + $parts = preg_split('/\s*[-+]\s*/', $dateString, 2); + if (count($parts) < 2) { + return; + } + $startDateStr = trim($parts[0]); + $endDateStr = trim($parts[1]); + } + + // Парсим дату из формата "dd.mm.yyyy" или "yyyy-mm-dd" + $startDate = $this->parseDate($startDateStr); + $endDate = $this->parseDate($endDateStr); + + if (!$startDate || !$endDate) { + return; + } + + // Фильтруем специалистов, у которых есть локации с nearestDate в указанном диапазоне + $qb->andWhere('EXISTS ( + SELECT 1 FROM App\Entity\LocationView l_date + WHERE l_date.specialistId = s.id + AND l_date.active = true + AND l_date.nearestDate >= :startDate + AND l_date.nearestDate <= :endDate + )') + ->setParameter('startDate', $startDate) + ->setParameter('endDate', $endDate); + } + + private function parseDate(string $dateStr): ?\DateTime + { + $dateStr = trim($dateStr); + + // Пробуем формат "dd.mm.yyyy" + if (preg_match('/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/', $dateStr, $matches)) { + $day = (int)$matches[1]; + $month = (int)$matches[2]; + $year = (int)$matches[3]; + try { + return new \DateTime(sprintf('%04d-%02d-%02d', $year, $month, $day)); + } catch (\Exception $e) { + return null; + } + } + + // Пробуем формат "yyyy-mm-dd" + if (preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2})$/', $dateStr, $matches)) { + $year = (int)$matches[1]; + $month = (int)$matches[2]; + $day = (int)$matches[3]; + try { + return new \DateTime(sprintf('%04d-%02d-%02d', $year, $month, $day)); + } catch (\Exception $e) { + return null; + } + } + + // Пробуем стандартный формат + try { + return new \DateTime($dateStr); + } catch (\Exception $e) { + return null; + } + } + + private function applyOrderBy(QueryBuilder $qb, ?string $orderBy): void + { + $orderConfig = match ($orderBy) { + 'sort-abc.asc' => ['field' => 's.name', 'direction' => 'ASC'], + 'sort-abc.desc' => ['field' => 's.name', 'direction' => 'DESC'], + 'sort-time.asc' => $this->createTimeOrderConfig('ASC'), + 'sort-time.desc' => $this->createTimeOrderConfig('DESC'), + default => ['field' => 's.name', 'direction' => 'ASC'] + }; + + if (isset($orderConfig['join'])) { + $qb->leftJoin('App\Entity\LocationView', 'l_sort', 'WITH', 'l_sort.specialistId = s.id'); + } + + $qb->addOrderBy($orderConfig['field'], $orderConfig['direction']); + } + + private function createTimeOrderConfig(string $direction): array + { + return [ + 'field' => 'l_sort.nearestDate', + 'direction' => $direction, + 'join' => true + ]; + } +} \ No newline at end of file diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php new file mode 100644 index 0000000..0c7f38f --- /dev/null +++ b/src/Repository/UserRepository.php @@ -0,0 +1,62 @@ +setPassword($newEncodedPassword); + $this->_em->persist($user); + $this->_em->flush(); + } + + /** + * Находит неактивных пользователей с только ROLE_USER для удаления + * Удаляются пользователи, у которых последняя активность (или дата создания, если активности нет) была до указанной даты + * + * @param \DateTimeInterface $beforeDate Дата, до которой должна быть последняя активность + * @return User[] + */ + public function findUsersToCleanup(\DateTimeInterface $beforeDate): array + { + // Ищем пользователей, у которых либо lastActivityAt < beforeDate, либо (lastActivityAt IS NULL AND createdAt < beforeDate) + $users = $this->createQueryBuilder('u') + ->where('(u.lastActivityAt IS NOT NULL AND u.lastActivityAt < :beforeDate) OR (u.lastActivityAt IS NULL AND u.createdAt IS NOT NULL AND u.createdAt < :beforeDate)') + ->setParameter('beforeDate', $beforeDate) + ->getQuery() + ->getResult(); + + // Фильтруем пользователей, у которых только ROLE_USER + return array_filter($users, function (User $user) { + // Если массив ролей пуст, значит у пользователя только ROLE_USER (который добавляется автоматически) + return empty($user->getActualRoles()); + }); + } + +} diff --git a/src/Repository/UsrlogRepository.php b/src/Repository/UsrlogRepository.php new file mode 100644 index 0000000..4b7b344 --- /dev/null +++ b/src/Repository/UsrlogRepository.php @@ -0,0 +1,53 @@ + + * + * @method Usrlog|null find($id, $lockMode = null, $lockVersion = null) + * @method Usrlog|null findOneBy(array $criteria, array $orderBy = null) + * @method Usrlog[] findAll() + * @method Usrlog[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class UsrlogRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Usrlog::class); + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function add(Usrlog $entity, bool $flush = true): void + { + if (!$entity->getCreatedAt()) { + $entity->setCreatedAt(new \DateTime()); + } + + $this->_em->persist($entity); + if ($flush) { + $this->_em->flush(); + } + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function remove(Usrlog $entity, bool $flush = true): void + { + $this->_em->remove($entity); + if ($flush) { + $this->_em->flush(); + } + } +} diff --git a/src/Repository/WidgetFormInputRepository.php b/src/Repository/WidgetFormInputRepository.php new file mode 100644 index 0000000..57b66d2 --- /dev/null +++ b/src/Repository/WidgetFormInputRepository.php @@ -0,0 +1,22 @@ +entityManager = $entityManager; + $this->urlGenerator = $urlGenerator; + $this->csrfTokenManager = $csrfTokenManager; + $this->passwordEncoder = $passwordEncoder; + } + + public function supports(Request $request) + { + return self::LOGIN_ROUTE === $request->attributes->get('_route') + && $request->isMethod('POST'); + } + + public function getCredentials(Request $request) + { + $credentials = [ + 'email' => $request->request->get('email'), + 'password' => $request->request->get('password'), + 'csrf_token' => $request->request->get('_csrf_token'), + ]; + $request->getSession()->set( + Security::LAST_USERNAME, + $credentials['email'] + ); + + return $credentials; + } + + public function getUser($credentials, UserProviderInterface $userProvider) + { + $token = new CsrfToken('authenticate', $credentials['csrf_token']); + + if (!$this->csrfTokenManager->isTokenValid($token)) { + throw new InvalidCsrfTokenException(); + } + + $user = $this->entityManager->getRepository(User::class) + ->findOneBy(['email' => bin2hex($credentials['email'])]); + + if (!$user) { + throw new UsernameNotFoundException('Email could not be found.'); + } + + return $user; + } + + public function checkCredentials($credentials, UserInterface $user) + { + return $this->passwordEncoder->isPasswordValid($user, $credentials['password']); + } + + /** + * Used to upgrade (rehash) the user's password automatically over time. + */ + public function getPassword($credentials): ?string + { + return $credentials['password']; + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey) + { + if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) { + // return new RedirectResponse($targetPath); + return $this->responseJson([ + 'success' => true, + 'redirect' => $request->request->get('redirectFrom') ?? $targetPath + ]); + } + + return $this->responseJson([ + 'success' => true, + 'redirect' => $request->request->get('redirectFrom') ?? $this->urlGenerator->generate('default_index') + ]); + + // For example : return new RedirectResponse($this->urlGenerator->generate('some_route')); + // return new RedirectResponse($this->urlGenerator->generate('default_index')); + } + + protected function getLoginUrl() + { + return $this->urlGenerator->generate(self::LOGIN_ROUTE); + } + + private function responseJson($data) + { + $response = new Response(); + $response->headers->set('Content-Type', 'application/json'); + return $response->setContent(json_encode(['data' => $data ])); + } + +} diff --git a/src/Service/PriceListService.php b/src/Service/PriceListService.php new file mode 100644 index 0000000..87d5c5a --- /dev/null +++ b/src/Service/PriceListService.php @@ -0,0 +1,50 @@ +priceListRepository = $priceListRepository; + $this->filialRepository = $filialRepository; + } + + public function getFilteredPriceListQuery(array $filters): QueryBuilder + { + $filters['actual'] = true; + + if (empty($filters['filial'])) { + $filters['filial'] = $this->getCurrentFilialIds(); + } + + $priceListQuery = $this->priceListRepository->createFilteredQueryBuilder($filters); + + return $priceListQuery; + } + + public function getPriceListQuery(array $filters): QueryBuilder + { + $priceListQuery = $this->priceListRepository->createFilteredQueryBuilder($filters); + + return $priceListQuery; + } + + private function getCurrentFilialIds(): array + { + $filials = $this->filialRepository->findByRegion(Region::getCurrentName()); + + return array_map(function ($filial) { + return $filial->getFid(); + }, $filials); + } +} diff --git a/src/Service/SpecialistMoreService.php b/src/Service/SpecialistMoreService.php new file mode 100644 index 0000000..4b73aff --- /dev/null +++ b/src/Service/SpecialistMoreService.php @@ -0,0 +1,204 @@ +onlineMode = $onlineMode; + } + + public function setSpecialist(int $specialistId, ?array $kodopers): static + { + $this->specialistId = $specialistId; + $this->kodopers = $kodopers; + + return $this; + } + + public function getLocations(): array + { + if (empty($this->locations)) { + $this->locations = $this + ->locationViewRepository + ->findBySpecialistId($this->specialistId, $this->onlineMode); + } + + return $this->locations; + } + + public function locationsCount(): int + { + return count($this->getLocations()); + } + + public function defaultLocation(): ?array + { + $this->getLocations(); + + usort($this->locations, function($a, $b) { + return $a['nearestDate'] <=> $b['nearestDate']; + }); + + return !empty($this->locations) ? $this->locations[0] : null; + } + + public function hasPrice(): bool + { + return !empty($this->getPrices()); + } + + public function getPrices(): ?array + { + if (empty($this->prices)) { + if ($this->kodopers) { + // Логируем входные данные + Logger::send([ + 'method' => 'SpecialistMoreService::getPrices()', + 'action' => 'Input kodopers', + 'kodopers' => $this->kodopers + ], 'dev.log'); + + // Фильтр по актуальным записям (обновленным за последние 2 дня) + $filters['actual'] = true; + $filters['kodoper'] = $this->kodopers; + $filters['filial'] = []; + + // Убеждаемся, что locations загружены перед использованием + $locations = $this->getLocations(); + + foreach ($locations as $location) { + if (isset($location['filial']) && $location['filial'] !== null) { + $filters['filial'][] = $location['filial']; + } + } + + // Убираем дубликаты филиалов + $filters['filial'] = array_values(array_unique($filters['filial'])); + + // Если филиалы не найдены, не применяем фильтр по филиалам + // чтобы показать все цены для данного специалиста + if (empty($filters['filial'])) { + unset($filters['filial']); + } + + $queryBuilder = $this->priceListRepository->createFilteredQueryBuilder($filters); + + // Логируем SQL запрос и параметры для отладки + $query = $queryBuilder->getQuery(); + $sql = $query->getSQL(); + $params = []; + foreach ($query->getParameters() as $param) { + $params[$param->getName()] = $param->getValue(); + } + + Logger::send([ + 'method' => 'SpecialistMoreService::getPrices()', + 'action' => 'PriceList SQL params', + 'params' => $params + ], 'dev.log'); + + // Выполняем запрос + $allPrices = $queryBuilder->getQuery()->getResult(); + + // Убираем дубликаты по комбинации kodoper + filial + schname + $uniquePrices = []; + $seenKeys = []; + foreach ($allPrices as $price) { + $kodoper = $price->getKodoper(); + $filial = $price->getFilial(); + $schname = $price->getSchname(); + // Создаем уникальный ключ + $key = md5($kodoper . '|' . $filial . '|' . $schname); + + if (!isset($seenKeys[$key])) { + $seenKeys[$key] = true; + $uniquePrices[] = $price; + } + } + + $this->prices = $uniquePrices; + + // Логируем детальную информацию о найденных записях + $foundKodopers = []; + $foundRecords = []; + foreach ($this->prices as $price) { + $kodoper = $price->getKodoper(); + $foundKodopers[] = $kodoper; + $foundRecords[] = [ + 'id' => $price->getId(), + 'kodoper' => $kodoper, + 'schname' => $price->getSchname(), + 'filial' => $price->getFilial(), + 'price' => $price->getPriceInfo()['price'] ?? null, + ]; + } + + Logger::send([ + 'method' => 'SpecialistMoreService::getPrices()', + 'action' => 'PriceList query result', + 'kodoper' => $filters['kodoper'], + 'filial' => $filters['filial'] ?? 'not set', + 'found_records' => count($this->prices), + 'before_dedup' => count($allPrices), + 'found_kodopers' => $foundKodopers, + 'records_details' => $foundRecords + ], 'dev.log'); + } + } + + return $this->prices; + } + + public function minPrice(): ?PriceList + { + $minPriceRecord = null; + $minPrice = PHP_INT_MAX; + + foreach ($this->getPrices() as $record) { + $currentPrice = $record->getPriceInfo()['price']; + if ($currentPrice < $minPrice) { + $minPrice = $currentPrice; + $minPriceRecord = $record; + } + } + + return $minPriceRecord; + } + + public function getReviews(): ?array + { + if (empty($this->reviews)) { + $this->reviews = $this + ->reviewRepository + ->findBy(['specialistId' => $this->specialistId]); + } + + return $this->reviews; + } + + public function hasReviews(): bool + { + return !empty($this->getReviews()); + } +} diff --git a/src/Service/SpecialistService.php b/src/Service/SpecialistService.php new file mode 100644 index 0000000..d8294ea --- /dev/null +++ b/src/Service/SpecialistService.php @@ -0,0 +1,94 @@ +specialistViewRepository + ->createFilteredQueryBuilder($filters) + ->getQuery() + ; + + $paginatedSpecialists = $this->paginator->paginate($query, $page, $limit); + + foreach ($paginatedSpecialists as $key => $specialist) { + $specialist->addSpecialistMoreService( + new SpecialistMoreService( + $this->locationViewRepository, + $this->reviewRepository, + $this->priceListRepository, + $filters['onlineMode'] ?? false + ) + ); + } + + return $paginatedSpecialists; + } + + public function list(array $filters = []): array + { + $specialists = $this->specialistViewRepository + ->createFilteredQueryBuilder($filters) + ->getQuery() + ->getResult() + ; + + foreach ($specialists as $key => $specialist) { + $specialist->addSpecialistMoreService( + new SpecialistMoreService( + $this->locationViewRepository, + $this->reviewRepository, + $this->priceListRepository, + $filters['onlineMode'] ?? false + ) + ); + } + + return $specialists; + } + + public function show(array $filters = []): ?SpecialistView + { + $specialist = $this->specialistViewRepository + ->createFilteredQueryBuilder($filters) + ->setMaxResults(1) + ->getQuery() + ->getOneOrNullResult(); + ; + + if ($specialist) { + $specialist->addSpecialistMoreService( + new SpecialistMoreService( + $this->locationViewRepository, + $this->reviewRepository, + $this->priceListRepository, + $filters['onlineMode'] ?? false + ) + ); + } + + return $specialist; + } +} \ No newline at end of file diff --git a/src/Service/UserCleanupService.php b/src/Service/UserCleanupService.php new file mode 100644 index 0000000..ded3ffb --- /dev/null +++ b/src/Service/UserCleanupService.php @@ -0,0 +1,318 @@ +userRepository = $userRepository; + $this->entityManager = $entityManager; + $this->logger = $logger; + $this->cookieLifetime = $sessionStorageOptions['cookie_lifetime'] ?? 0; + $this->sessionHandler = $sessionHandler; + } + + /** + * Удаляет неактивных пользователей с только ROLE_USER после истечения времени жизни кукисов + * + * @return int Количество удаленных пользователей + */ + public function cleanupExpiredUsers(): int + { + // Если cookie_lifetime = 0, используем время жизни сессии по умолчанию (1 час) + // Иначе используем указанное время жизни кукисов + $sessionLifetime = $this->cookieLifetime > 0 + ? $this->cookieLifetime + : 3600; // 1 час по умолчанию для session cookies + + // Вычисляем дату, до которой должна быть последняя активность пользователя + $beforeDate = new \DateTime(); + $beforeDate->modify("-{$sessionLifetime} seconds"); + + $usersToDelete = $this->userRepository->findUsersToCleanup($beforeDate); + $deletedCount = 0; + + $this->logger->info(sprintf( + 'Найдено пользователей для проверки: %d (время жизни сессии: %d секунд, дата отсечки: %s)', + count($usersToDelete), + $sessionLifetime, + $beforeDate->format('Y-m-d H:i:s') + )); + + foreach ($usersToDelete as $user) { + // Проверяем, что у пользователя только ROLE_USER + // Если массив ролей пуст, значит у пользователя только ROLE_USER (который добавляется автоматически) + $actualRoles = $user->getActualRoles(); + $hasOnlyUserRole = empty($actualRoles); + + $this->logger->debug(sprintf( + 'Проверка пользователя ID %d: роли=%s, только ROLE_USER=%s', + $user->getId(), + json_encode($actualRoles), + $hasOnlyUserRole ? 'да' : 'нет' + )); + + if ($hasOnlyUserRole) { + // Определяем дату для логирования + $activityDate = $user->getLastActivityAt() ?? $user->getCreatedAt(); + $activityDateStr = $activityDate ? $activityDate->format('Y-m-d H:i:s') : 'N/A'; + + try { + // Удаляем сессии пользователя из Redis перед удалением пользователя + $this->cleanupUserSessions($user); + + $this->deleteUserAndRelatedData($user); + $deletedCount++; + $this->logger->info(sprintf( + 'Удален неактивный пользователь с ID %d (uid: %d, email: %s, последняя активность: %s)', + $user->getId(), + $user->getUid(), + $user->getEmail() ?? 'N/A', + $activityDateStr + )); + } catch (\Exception $e) { + $this->logger->error(sprintf( + 'Ошибка при удалении пользователя с ID %d: %s', + $user->getId(), + $e->getMessage() + )); + } + } + } + + // Очищаем старые сессии из Redis + $this->cleanupOldRedisSessions($sessionLifetime); + + return $deletedCount; + } + + /** + * Удаляет пользователя и все связанные данные + * + * @param User $user + */ + private function deleteUserAndRelatedData(User $user): void + { + // Удаляем связанные данные Calltouch (если есть) + $this->deleteCalltouchData($user); + + // Удаляем сам пользователя + $this->entityManager->remove($user); + $this->entityManager->flush(); + } + + /** + * Очищает сессии пользователя из Redis + * + * @param User $user + */ + private function cleanupUserSessions(User $user): void + { + if (!$this->sessionHandler) { + return; + } + + try { + $redis = $this->getRedisClient(); + if (!$redis) { + return; + } + + // Ищем все сессии, которые могут принадлежать пользователю + // В Symfony сессии обычно хранятся с префиксом PHPSESSID или sess_ + $patterns = ['PHPSESSID_*', 'sess_*', '*_symfony_*']; + $deletedSessions = 0; + + foreach ($patterns as $pattern) { + $keys = $redis->keys($pattern); + if (!$keys) { + continue; + } + + foreach ($keys as $key) { + try { + $sessionData = $redis->get($key); + if ($sessionData) { + // Пытаемся найти user_id в данных сессии + // Сессии Symfony обычно содержат сериализованные данные + if (strpos($sessionData, '_security_main') !== false || + strpos($sessionData, (string)$user->getId()) !== false || + strpos($sessionData, (string)$user->getUid()) !== false) { + $redis->del($key); + $deletedSessions++; + } + } + } catch (\Exception $e) { + // Игнорируем ошибки при проверке отдельных ключей + } + } + } + + if ($deletedSessions > 0) { + $this->logger->info(sprintf( + 'Удалено %d сессий из Redis для пользователя %d', + $deletedSessions, + $user->getId() + )); + } + } catch (\Exception $e) { + $this->logger->warning(sprintf( + 'Ошибка при очистке сессий пользователя %d из Redis: %s', + $user->getId(), + $e->getMessage() + )); + } + } + + /** + * Очищает старые сессии из Redis + * + * @param int $sessionLifetime Время жизни сессии в секундах + */ + private function cleanupOldRedisSessions(int $sessionLifetime): void + { + if (!$this->sessionHandler) { + return; + } + + try { + $redis = $this->getRedisClient(); + if (!$redis) { + return; + } + + $patterns = ['PHPSESSID_*', 'sess_*', '*_symfony_*']; + $deletedCount = 0; + $now = time(); + + foreach ($patterns as $pattern) { + $keys = $redis->keys($pattern); + if (!$keys) { + continue; + } + + foreach ($keys as $key) { + try { + $ttl = $redis->ttl($key); + // Если TTL отрицательный (ключ не имеет TTL) или больше времени жизни сессии, пропускаем + if ($ttl < 0 || $ttl > $sessionLifetime) { + continue; + } + + // Проверяем, не истекла ли сессия + $lastAccess = $now - $ttl; + if ($lastAccess > $sessionLifetime) { + $redis->del($key); + $deletedCount++; + } + } catch (\Exception $e) { + // Игнорируем ошибки при проверке отдельных ключей + } + } + } + + if ($deletedCount > 0) { + $this->logger->info(sprintf( + 'Удалено %d старых сессий из Redis', + $deletedCount + )); + } + } catch (\Exception $e) { + $this->logger->warning(sprintf( + 'Ошибка при очистке старых сессий из Redis: %s', + $e->getMessage() + )); + } + } + + /** + * Получает Redis клиент из session handler + * + * @return \Redis|\Predis\Client|null + */ + private function getRedisClient() + { + if (!$this->sessionHandler) { + return null; + } + + try { + // Пытаемся получить Redis клиент из различных типов session handlers + if ($this->sessionHandler instanceof \Redis) { + return $this->sessionHandler; + } + + if ($this->sessionHandler instanceof \Predis\Client) { + return $this->sessionHandler; + } + + // Для RedisSessionHandler из Symfony + if ($this->sessionHandler instanceof RedisSessionHandler) { + $reflection = new \ReflectionClass($this->sessionHandler); + if ($reflection->hasProperty('redis')) { + $property = $reflection->getProperty('redis'); + $property->setAccessible(true); + return $property->getValue($this->sessionHandler); + } + } + + // Пытаемся вызвать метод getRedis, если он существует + if (method_exists($this->sessionHandler, 'getRedis')) { + return $this->sessionHandler->getRedis(); + } + + return null; + } catch (\Exception $e) { + $this->logger->warning(sprintf( + 'Не удалось получить Redis клиент: %s', + $e->getMessage() + )); + return null; + } + } + + /** + * Удаляет данные Calltouch, связанные с пользователем + * + * @param User $user + */ + private function deleteCalltouchData(User $user): void + { + try { + // Проверяем наличие связи с Calltouch через миграции + // Если есть таблица calltouch с внешним ключом на users.id + $connection = $this->entityManager->getConnection(); + + // Удаляем записи calltouch, связанные с пользователем + $connection->executeStatement( + 'DELETE FROM calltouch WHERE user_id = :userId', + ['userId' => $user->getId()] + ); + } catch (\Exception $e) { + // Игнорируем ошибки, если таблица не существует или нет связи + $this->logger->warning(sprintf( + 'Не удалось удалить данные Calltouch для пользователя %d: %s', + $user->getId(), + $e->getMessage() + )); + } + } +} diff --git a/src/Support/OnlineMode.php b/src/Support/OnlineMode.php new file mode 100644 index 0000000..03e237f --- /dev/null +++ b/src/Support/OnlineMode.php @@ -0,0 +1,46 @@ +num2str($num); + } + + public function priceSearch($schid, $filial ,$haystack) { + foreach ($haystack as $pricelist) { + foreach($pricelist as $key => $value) { + if (array_search($schid, $value['priceInfo']) != false) { + if ($filial == $value['filial']) { + return $value; + } + } + } + + } + + return false; + } + + public function rFloat($value) + { + return preg_replace('/(\..{1}).*/', '$1', $value); + } + + public function getLicenseLink($id = null) + { + if (empty($id)) $id = Region::getId(); + + switch ($id) { + case '92': + return 'https://volgograd.sovamed.ru/o-sove/dokumenty-i-licenzii/'; + // Волгоград + break; + + case '93': + return 'https://voronezh.sovamed.ru/o-sove/dokumenty-i-licenzii/'; + // Воронеж + break; + + case '94': + return 'https://wmtmed.ru/about/law-map/'; + // Краснодар + break; + + default: + return 'https://sovamed.ru/o-sove/dokumenty-i-licenzii/'; + // Саратов + break; + } + } + + public function getPoliticaLink($id = null) + { + if (empty($id)) $id = Region::getId(); + + switch ($id) { + case '92': + return 'https://volgograd.sovamed.ru/o-sove/dokumenty-i-licenzii/politika-konfidencialnosti/'; + // Волгоград + break; + + case '93': + return 'https://voronezh.sovamed.ru/o-sove/dokumenty-i-licenzii/politika-konfidencialnosti/'; + // Воронеж + break; + + case '94': + return 'https://wmtmed.ru/about/confidentiality_policy.php'; + // краснодар + break; + + case '95': + return 'https://sovenok.sovamed.ru/o-sove/dokumenty-i-licenzii/politika-konfidencialnosti/'; + // совенок + break; + + case '96': + return 'https://sovamed.ru/docs/comfort_politika.pdf'; + // комфорт + break; + + default: + return 'https://sovamed.ru/o-sove/dokumenty-i-licenzii/politika-konfidencialnosti/'; + // Саратов + break; + + } + } + + public function isShowInRegion($regions = []) + { + if (in_array(Region::getId(), $regions)) { + return true; + } + + return false; + } + + + public function ceil($value) + { + return (int) ceil($value); + } + + public function float($value) + { + return (float) $value; + } + + public function floor($value) + { + return floor($value); + } + + public function minText($value, $size = 150) + { + if ($crop = (mb_strlen($value) > $size)) { + $value = mb_substr(strip_tags($value), 0, $size) . '...'; + } + + return [ + 'text' => $value, + 'crop' => $crop + ]; + } + + public function textYear($year, $exp = true) + { + $t1 = 0; + $t2 = 0; + $year = abs($year); + $t1 = $year % 10; + $t2 = $year % 100; + if ($exp) { + return ($t1 == 1 && $t2 != 11 ? "год" : ($t1 >= 2 && $t1 <= 4 && ($t2 < 10 || $t2 >= 20) ? "года" : "лет")); + } else { + return ($t1 == 1 ? "года" : "лет"); + } + } + + public function isMobile() { + $useragent = $_SERVER['HTTP_USER_AGENT'] ?? ''; + + if ($useragent === '') { + return false; + } + + return preg_match('/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i',$useragent) + || preg_match('/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i',substr($useragent,0,4)); + } + + +} \ No newline at end of file diff --git a/templates/_calltouch.html.twig b/templates/_calltouch.html.twig new file mode 100644 index 0000000..5bfe16e --- /dev/null +++ b/templates/_calltouch.html.twig @@ -0,0 +1,20 @@ +{% set regionId = app.request.cookies.get('region')|default(94) %} +{% set modId = '' %} + +{% if regionId == 91 or regionId == '91' %} + {% set modId = '95271cfe' %} +{% elseif regionId == 92 or regionId == '92' %} + {% set modId = '886a1412' %} +{% elseif regionId == 93 or regionId == '93' %} + {% set modId = 'ce64c813' %} +{% elseif regionId == 94 or regionId == '94' %} + {% set modId = 'yde3clnb' %} +{% endif %} + +{% if modId %} + + + +{% endif %} diff --git a/templates/_change_region.html.twig b/templates/_change_region.html.twig new file mode 100644 index 0000000..1495764 --- /dev/null +++ b/templates/_change_region.html.twig @@ -0,0 +1,32 @@ + \ No newline at end of file diff --git a/templates/_metrika.html.twig b/templates/_metrika.html.twig new file mode 100644 index 0000000..f637d6c --- /dev/null +++ b/templates/_metrika.html.twig @@ -0,0 +1,33 @@ +{% if isShowInRegion([91,92,93]) %} + + + + +{% else %} + + + + +{% endif %} diff --git a/templates/admin_base.html.twig b/templates/admin_base.html.twig new file mode 100644 index 0000000..3eebdb3 --- /dev/null +++ b/templates/admin_base.html.twig @@ -0,0 +1,80 @@ + + + + + {% block title %}{% endblock %} + + {% if app.debug == false %} + {{ include('/_metrika.html.twig') }} + {{ include('/_calltouch.html.twig') }} + {% endif %} + + {% block stylesheets %} + + {{ encore_entry_link_tags('app_sovamed') }} + {% endblock %} + + {% block javascripts %} + {{ encore_entry_script_tags('app_sovamed') }} + {% endblock %} + + {% block js %}{% endblock %} + + + +
    + + +
    +
    + {% for message in app.flashes('notice') %} + + {% endfor %} + {% for message in app.flashes('success') %} + + {% endfor %} +
    + + {% block body %}{% endblock %} +
    + +
    + +
    + + + + {{ include('_change_region.html.twig') }} + + diff --git a/templates/banner/_delete_form.html.twig b/templates/banner/_delete_form.html.twig new file mode 100644 index 0000000..31440cc --- /dev/null +++ b/templates/banner/_delete_form.html.twig @@ -0,0 +1,4 @@ +
    + + +
    diff --git a/templates/banner/_form.html.twig b/templates/banner/_form.html.twig new file mode 100644 index 0000000..92fe2a2 --- /dev/null +++ b/templates/banner/_form.html.twig @@ -0,0 +1,5 @@ +{% form_theme form 'bootstrap_3_layout.html.twig' %} +{{ form_start(form) }} + {{ form_widget(form) }} + +{{ form_end(form) }} diff --git a/templates/banner/edit.html.twig b/templates/banner/edit.html.twig new file mode 100644 index 0000000..0cb3884 --- /dev/null +++ b/templates/banner/edit.html.twig @@ -0,0 +1,13 @@ +{% extends 'admin_base.html.twig' %} + +{% block title %}Edit Banner{% endblock %} + +{% block body %} +

    Edit Banner

    + + {{ include('banner/_form.html.twig', {'button_label': 'Update'}) }} + + back to list + + {{ include('banner/_delete_form.html.twig') }} +{% endblock %} diff --git a/templates/banner/index.html.twig b/templates/banner/index.html.twig new file mode 100644 index 0000000..f93594f --- /dev/null +++ b/templates/banner/index.html.twig @@ -0,0 +1,40 @@ +{% extends 'admin_base.html.twig' %} + +{% block title %}Banner index{% endblock %} + +{% block body %} +

    Banner index

    + + + + + + + + + + + + + + {% for banner in banners %} + + + + + + + + + {% else %} + + + + {% endfor %} + +
    IdHrefSrcCityActiveactions
    {{ banner.id }}{{ banner.href }}banner {{ banner.city.name }}{{ banner.active? 'on' : 'off' }} + edit +
    no records found
    + + Create new +{% endblock %} diff --git a/templates/banner/new.html.twig b/templates/banner/new.html.twig new file mode 100644 index 0000000..0f7921d --- /dev/null +++ b/templates/banner/new.html.twig @@ -0,0 +1,11 @@ +{% extends 'admin_base.html.twig' %} + +{% block title %}New Banner{% endblock %} + +{% block body %} +

    Create new Banner

    + + {{ include('banner/_form.html.twig') }} + + back to list +{% endblock %} diff --git a/templates/base.html.twig b/templates/base.html.twig new file mode 100644 index 0000000..4252699 --- /dev/null +++ b/templates/base.html.twig @@ -0,0 +1,149 @@ + + + + + {% block title %}{% endblock %} + + {% if app.debug == false %} + {{ include('/_metrika.html.twig') }} + {{ include('/_calltouch.html.twig') }} + {% endif %} + + {% block stylesheets %} + + + {{ encore_entry_link_tags('app_sovamed') }} + {% endblock %} + + {% block javascripts %} + {{ encore_entry_script_tags('app_sovamed') }} + {% endblock %} + + {% block js %}{% endblock %} + + + + {#
    #} +
    + + +
    + {% block top %} +
    +

    + + {% if app.request.get('_route') in ['security_card'] and app.request.get('tab') == 'allTest' %} + Результаты анализов + {% else %} + {{ title }} + {% endif %} + +

    + +
    + + +
    +
    + + +
    +
    +
    + {% endblock %} +
    + {% for message in app.flashes('notice') %} + + {% endfor %} + {% for message in app.flashes('success') %} + + {% endfor %} +
    + + {% block body %}{% endblock %} +
    + + {% block sidebar %} + +
    + {% if (isMobile() == false) %} + + {% endif %} + + {% block bonusWidget %} + {% endblock %} + +
    + {% block filter %} +
    Найти специалиста
    + {{ render(path('specialist_filter')) }} + {% endblock %} +
    + + +
    + {% endblock %} + +
    + +
    + + + + + + {{ include('_change_region.html.twig') }} + + + + diff --git a/templates/base/_search_form_price.html.twig b/templates/base/_search_form_price.html.twig new file mode 100644 index 0000000..b2579dc --- /dev/null +++ b/templates/base/_search_form_price.html.twig @@ -0,0 +1,38 @@ + + + + + + +{{ form_start(searchForm) }} +
    + {{ form_widget(searchForm.schname, {'attr': { + 'class': 'form-control input-castom', + 'placeholder': 'Название услуги' + }}) }} +
    +
    + {{ form_widget(searchForm.kodoper, {'attr': { + 'class': 'form-control input-castom', + 'placeholder': 'Код услуги' + }}) }} +
    +
    + {{ form_widget(searchForm.groupId, {'attr': { + 'class': 'filter__select', + 'data-live-search': 'true', + 'data-controller' : 'selectpicker' + }}) }} +
    +
    + {{ form_widget(searchForm.filial, {'attr': { + 'class': 'filter__select selectpicker', + 'data-controller' : 'selectpicker', + 'data-placeholder': 'Все клиники' + }}) }} +
    + {{ form_row(searchForm._token) }} +
    + +
    +{{ form_end(searchForm) }} \ No newline at end of file diff --git a/templates/base/doc.html.twig b/templates/base/doc.html.twig new file mode 100644 index 0000000..4834eb2 --- /dev/null +++ b/templates/base/doc.html.twig @@ -0,0 +1,25 @@ +{% extends template ~ '.html.twig' %} + +{% block title %}{{ title }}{% endblock %} + +{% block body %} + +
    + +
    +
    +

    Пациентам

    +
    + {% include '/base/patient.html.twig' %} +
    +
    + + +
    + + +{% endblock %} diff --git a/templates/base/doc_your_home.html.twig b/templates/base/doc_your_home.html.twig new file mode 100644 index 0000000..8da1c2d --- /dev/null +++ b/templates/base/doc_your_home.html.twig @@ -0,0 +1,25 @@ +{% extends template ~ '.html.twig' %} + +{% block title %}{{ title }}{% endblock %} + +{% block body %} +
    +
    + +
    +
    +
    +

    Вызов врача на дом – это популярная услуга, ведь каждый сталкивался с ситуацией, когда срочно необходима медицинская помощь, а попасть в больницу нет возможности. Причины могут быть разные: плохое самочувствие, ограничение подвижности, обострение болезни. Кому-то не с кем оставить ребенка или хочется облегчить жизнь пожилых людей, для которых визит к специалисту стал трудновыполнимой задачей. В условиях распространения коронавируса многие сознательно ограничивают посещение публичных мест, опасаясь инфекции.

    +

    Оптимальное решение проблемы – оформить заявку по нужному адресу в удобное для вас время. В комфортной обстановке будет проведен осмотр и назначено эффективное лечение. При необходимости составляется план дальнейшего обследования.

    +

    Преимущества обращения в клинику «СОВА»:

    +
      +
    • Соблюдение повышенных мер безопасности.
    • +
    • Современное портативное диагностическое оборудование.
    • +
    • Высококвалифицированные доктора и внимательное отношение к пациентам.
    • +
    +

    Время платного выезда врача на дом оговаривается индивидуально и зависит от штатного расписания того или иного специалиста. +
    Отправьте заявку и и мы свяжемся с Вами в ближайшее время.

    +
    +
    +
    +{% endblock %} diff --git a/templates/base/favorites.html.twig b/templates/base/favorites.html.twig new file mode 100644 index 0000000..f5855e0 --- /dev/null +++ b/templates/base/favorites.html.twig @@ -0,0 +1,40 @@ +{% extends template ~ '.html.twig' %} + +{% block title %}{{ title }}{% endblock %} + +{% block js %} + +{% endblock %} +{% block sidebar %} +
    + +
    + + {% block filter %} +
    Найти специалиста
    + {{ render(path('specialist_filter')) }} + {% endblock %} +
    + + +
    +{% endblock %} +{% block body %} +
    +
    + + {% for specialist in pagination %} + {% include '/specialist/_item.html.twig' with {'link': true} %} + + {% else %} +

    Вы пока не добавили врачей в избранное

    + {% endfor %} +
    +
    + {{ knp_pagination_render(pagination) }} +
    +
    + +{% include '/specialist/_calendar.html.twig' %} + +{% endblock %} \ No newline at end of file diff --git a/templates/base/help.html.twig b/templates/base/help.html.twig new file mode 100644 index 0000000..f11af09 --- /dev/null +++ b/templates/base/help.html.twig @@ -0,0 +1,127 @@ +{% extends template ~ '.html.twig' %} + +{% block title %}{{ title }}{% endblock %} + +{% block body %} +
    + {% if isShowInRegion([91,92,93]) %} +
    + +
    +
    +

    Онлайн консультация проводится через личный кабинет, никаких приложений устанавливать не нужно.

    +

    Если у Вас есть логин и пароль – выберите удобный интервал, авторизуйтесь и оплатите запись. Читать инструкцию.

    +

    Если логина и пароля нет – то рекомендуем войти через Госуслуги и затем вернутся в раздел Онлайн консультации. Читать инструкцию.

    +

    Возврат средств при несостоявшейся консультации производится в разделе приемы – история записей. Инструкция по возврату средств

    +
    +
    +
    + {% endif %} + + +
    + +
    +
    +

    Здесь Вы сможете:

    +
      +
    • найти необходимого специалиста
    • +
    • записаться на прием к врачу или записать своих близких или родственников
    • +
    • получить результаты исследований
    • +
    • получить историю посещений специалистов
    • + + {% if isShowInRegion([91,92,93]) %} +
    • получить информацию о бонусных балах
    • + {% endif %} + +
    • получить информацию о платежах
    • +
    • оплатить онлайн услуги
    • +
    • запросить оформление
    • +
    • ознакомится с ценами на услуги
    • +
    • запросить получение документов на налоговый вычет
    • +
    +
    +
    +
    + +
    + +
    +
    + {% if isShowInRegion([91,92,93]) %} +

    Самостоятельная регистрация на сайте предоставляет ограниченный доступ в Личный кабинет – доступна только запись на прием.
    Для получения полного доступа необходимо обратиться в регистратуру любой клиники сети «Сова» с паспортом и подписать соглашение на использование Личного кабинета пациента.

    + {% else %} +

    Самостоятельная регистрация на сайте предоставляет ограниченный доступ в Личный кабинет – доступна только запись на прием.
    Для получения полного доступа необходимо обратиться в регистратуру любой клиники сети «WMT» с паспортом и подписать соглашение на использование Личного кабинета пациента.

    + {% endif %} +
    +
    +
    + + {% if isShowInRegion([0]) %} +
    + +
    +
    +

    Пациент осуществляет предоплату онлайн консультации через Личный кабинет в процессе записи на консультацию.
    Предусмотрены несколько вариантов осуществления оплаты.
    Обычно оплата поступает в течение 3-5 минут.
    Подтверждение оплаты и резервирования времени консультации осуществляется в течение 5-10 минут.
    Пациент получит подтверждение на электронную почту, указанную при регистрации

    +
    +
    +
    + {% endif %} + +
    + + + +{% endblock %} diff --git a/templates/base/index.html.twig b/templates/base/index.html.twig new file mode 100644 index 0000000..ab276e2 --- /dev/null +++ b/templates/base/index.html.twig @@ -0,0 +1,72 @@ +{% extends template ~ '.html.twig' %} + +{% block title %}Личный кабинет{% endblock %} + +{% block body %} + +
    + {% if isMobile() %} + + {% endif %} + +
    +
    + {# /security-card #} + Медицинская карта +
    +
    + Загрузка данных.. +
    +
    + +
    + +
    +
    + Загрузка данных.. +
    +
    +
    + +
    + {# /bonus #} + +
    + {% if isShowInRegion([91,92,93]) %} + + {% endif %} +
    бонусов на счете: 0
    +
    + + +
    +
    +
    + Загрузка данных.. +
    +
    +
    +
    + +
    + {# /favorites #} +
    Мои врачи
    +
    +
    +
    +
    + +
    + +{% endblock %} diff --git a/templates/base/paginator.html.twig b/templates/base/paginator.html.twig new file mode 100644 index 0000000..9ee55de --- /dev/null +++ b/templates/base/paginator.html.twig @@ -0,0 +1,46 @@ +{# +/** + * @file + * Twitter Bootstrap v4 Sliding pagination control implementation. + * + * View that can be used with the pagination module + * from the Twitter Bootstrap v4 CSS Toolkit + * https://v4-alpha.getbootstrap.com/components/pagination/ + * + * @author Carlos Delgado + */ +#} + +{% if pageCount > 1 %} + +{% endif %} diff --git a/templates/base/patient.html.twig b/templates/base/patient.html.twig new file mode 100644 index 0000000..5e2b85f --- /dev/null +++ b/templates/base/patient.html.twig @@ -0,0 +1,10 @@ +
    Социальный налоговый вычет по расходам на лечение
    +

    Социальный налоговый вычет на лечение может получить физлицо, оплатившее медицинские услуги, в том числе дорогостоящие, оказанные ему самому, его супругу (супруге), родителям, а также детям (в том числе усыновленным) в возрасте до 18 лет (до 24 лет, если дети (в том числе усыновленные) являются обучающимися по очной форме обучения в организациях, осуществляющих образовательную деятельность), подопечным в возрасте до 18 лет. +

    +

    В наших клиниках вы можете получить справку об оплате медицинских услуг для предоставления в налоговую инспекцию. Для получения такой справки вам необходимо заполнить заявку на нашем сайте или обратиться к администраторам в регистратуру клиники. +

    +

    Срок подготовки справки - до 14 рабочих дней с момента обращения.После этого вам нужно будет обратиться на ресепшен к администратору клиники, указанной в вашей заявке, имея при себе паспорт.

    +

    Обращаем Ваше внимание, что в соответствии с Налоговым кодексом РФ, суммы, уплаченные за пребывание, уход и наблюдение в палатах стационара, в итоговую сумму справки не входят.

    +

    Перечень медицинских услуг и видов дорогостоящего лечения утвержден Постановлением Правительства РФ от 08.04.2020 N 458 «Об утверждении перечней медицинских услуг и дорогостоящих видов лечения в медицинских организациях, у индивидуальных предпринимателей, осуществляющих медицинскую деятельность, суммы оплаты которых за счет собственных средств налогоплательщика учитываются при определении суммы социального налогового вычета».

    + + diff --git a/templates/base/price.html.twig b/templates/base/price.html.twig new file mode 100644 index 0000000..1a52f41 --- /dev/null +++ b/templates/base/price.html.twig @@ -0,0 +1,65 @@ +{% extends template ~ '.html.twig' %} + +{% block title %}{{ title }}{% endblock %} + +{% block top %} +
    +

    {{ title }}

    +
    + + +
    +
    + + +
    +
    +
    +{% endblock %} + +{% block filter %} +
    Найти услугу
    + {{ include('base/_search_form_price.html.twig', {'searchForm': searchForm}) }} +{% endblock %} + +{% block body %} +
    + {% for item in pagination %} + {% if item.priceInfo.price != 0 %} +
    +
    +
    +
    {{ item.schname }}
    +

    Код услуги: {{ item.kodoper }}

    +
    +
    +

    {{ item.priceInfo.price }} ₽

    +
    +
    +
    +
    +

    {{ item.fname }}

    +
    +
    +
    + +
    +
    +
    +
    + {% endif %} + {% endfor %} + +
    + {{ knp_pagination_render(pagination) }} +
    +
    +{% endblock %} \ No newline at end of file diff --git a/templates/base/price_list.html.twig b/templates/base/price_list.html.twig new file mode 100644 index 0000000..17c96b6 --- /dev/null +++ b/templates/base/price_list.html.twig @@ -0,0 +1,88 @@ +{% extends 'admin_base.html.twig' %} + +{% block title %}{{ title }}{% endblock %} + +{% block body %} +

    + Для того чтобы цены подгрузились с инфоклиники, выберите департамент и нажмите на кнопку "Search".
    + Не найдено - данная услуга в Инфоклиници отсутствует, либо не загружена.
    + active = true - данная услуга отображается в кабинете. +

    +
    +
    +

    {{ title }}

    + {{ include('specialist/admin/_form.html.twig') }} +
    +
    +

    Обновить цены

    +
    +
    + +
    +
    +
    + +
    +
    +
    + + + + + + + + + + + + + + + {% for item in pagination %} + {% set list = priceSearch(item.priceInfo.schid, item.filial, priceList) %} + + + + + + + + {% if list.priceInfo.price is defined %} + {% if list.priceInfo.price == item.priceInfo.price %} + + {% else %} + + {% endif %} + {% else %} + + {% endif %} + + + + + {% if item.dateUpdate|date('U') >= dateActive|date('U') %} + {% if item.priceInfo.price == 0 %} + + {% else %} + + {% endif %} + {% else %} + + {% endif %} + + + + + {% else %} + + + + {% endfor %} + +
    schnamespecnamekodoperprice cabinetprice infoclinicadate updateactivefilial
    {{ item.schname }}{{ item.specname }}{{ item.kodoper }}{{ item.priceInfo.price }}{{ list.priceInfo.price }}{{ list.priceInfo.price }}Не найдено{{ item.dateUpdate|date('d.m.Y') }}falsetruefalse{{ item.fname }}
    Записи не найдены или отсутствуют
    + +
    + {{ knp_pagination_render(pagination) }} +
    +{% endblock %} diff --git a/templates/base_pdf.html.twig b/templates/base_pdf.html.twig new file mode 100644 index 0000000..85e272d --- /dev/null +++ b/templates/base_pdf.html.twig @@ -0,0 +1,17 @@ +{% block head %}{% endblock %} + + + + + + +
    + + + + + +
    +
    + +{% block body %}{% endblock %} diff --git a/templates/base_widget.html.twig b/templates/base_widget.html.twig new file mode 100644 index 0000000..133d809 --- /dev/null +++ b/templates/base_widget.html.twig @@ -0,0 +1,36 @@ + + + + + {% block title %}{% endblock %} + + {% block stylesheets %} + + {{ encore_entry_link_tags('app_widget') }} + {% endblock %} + + {% block javascripts %} + {{ encore_entry_script_tags('app_widget') }} + {% endblock %} + + {% block js %}{% endblock %} + + + + {% block body %}{% endblock %} + + + + \ No newline at end of file diff --git a/templates/base_wmtmed.html.twig b/templates/base_wmtmed.html.twig new file mode 100644 index 0000000..74700e4 --- /dev/null +++ b/templates/base_wmtmed.html.twig @@ -0,0 +1,138 @@ + + + + + {% block title %}{% endblock %} + + {% if app.debug == false %} + {{ include('/_metrika.html.twig') }} + {{ include('/_calltouch.html.twig') }} + {% endif %} + + {% block stylesheets %} + + + {{ encore_entry_link_tags('app_wmtmed') }} + {% endblock %} + + {% block javascripts %} + {{ encore_entry_script_tags('app_wmtmed') }} + {% endblock %} + + {% block js %}{% endblock %} + + + + {#
    #} +
    + + +
    + {% block top %} +
    +

    + + {% if app.request.get('_route') in ['security_card'] and app.request.get('tab') == 'allTest' %} + Результаты анализов + {% else %} + {{ title }} + {% endif %} + +

    + +
    + + +
    +
    +
    + {% endblock %} +
    + {% for message in app.flashes('notice') %} + + {% endfor %} + {% for message in app.flashes('success') %} + + {% endfor %} +
    + + {% block body %}{% endblock %} +
    + + {% block sidebar %} + +
    + {% if (isMobile() == false) %} + + {% endif %} + + {% block bonusWidget %} + {% endblock %} + +
    + {% block filter %} +
    Найти специалиста
    + {{ render(path('specialist_filter')) }} + {% endblock %} +
    + + +
    + {% endblock %} + +
    + +
    + + + + + + {{ include('_change_region.html.twig') }} + + + + diff --git a/templates/category_page/_delete_form.html.twig b/templates/category_page/_delete_form.html.twig new file mode 100644 index 0000000..46724b2 --- /dev/null +++ b/templates/category_page/_delete_form.html.twig @@ -0,0 +1,4 @@ +
    + + +
    diff --git a/templates/category_page/_form.html.twig b/templates/category_page/_form.html.twig new file mode 100644 index 0000000..e8b0e10 --- /dev/null +++ b/templates/category_page/_form.html.twig @@ -0,0 +1,5 @@ +{% form_theme form 'bootstrap_4_layout.html.twig' %} +{{ form_start(form) }} + {{ form_widget(form) }} + +{{ form_end(form) }} diff --git a/templates/category_page/edit.html.twig b/templates/category_page/edit.html.twig new file mode 100644 index 0000000..e9d4d03 --- /dev/null +++ b/templates/category_page/edit.html.twig @@ -0,0 +1,13 @@ +{% extends 'admin_base.html.twig' %} + +{% block title %}Edit CategoryPage{% endblock %} + +{% block body %} +

    Edit CategoryPage

    + + {{ include('category_page/_form.html.twig', {'button_label': 'Update'}) }} + + back to list + + {{ include('category_page/_delete_form.html.twig') }} +{% endblock %} diff --git a/templates/category_page/index.html.twig b/templates/category_page/index.html.twig new file mode 100644 index 0000000..3b196d8 --- /dev/null +++ b/templates/category_page/index.html.twig @@ -0,0 +1,37 @@ +{% extends 'admin_base.html.twig' %} + +{% block title %}CategoryPage index{% endblock %} + +{% block body %} +

    CategoryPage index

    + + + + + + + + + + + + {% for category_page in category_pages %} + + + + + + + {% else %} + + + + {% endfor %} + +
    IdNameActiveactions
    {{ category_page.id }}{{ category_page.name }}{{ category_page.active ? 'Yes' : 'No' }} + show + edit +
    no records found
    + + Create new +{% endblock %} diff --git a/templates/category_page/new.html.twig b/templates/category_page/new.html.twig new file mode 100644 index 0000000..5f1538b --- /dev/null +++ b/templates/category_page/new.html.twig @@ -0,0 +1,11 @@ +{% extends 'admin_base.html.twig' %} + +{% block title %}New CategoryPage{% endblock %} + +{% block body %} +

    Create new CategoryPage

    + + {{ include('category_page/_form.html.twig') }} + + back to list +{% endblock %} diff --git a/templates/category_page/show.html.twig b/templates/category_page/show.html.twig new file mode 100644 index 0000000..bfe565f --- /dev/null +++ b/templates/category_page/show.html.twig @@ -0,0 +1,30 @@ +{% extends 'admin_base.html.twig' %} + +{% block title %}CategoryPage{% endblock %} + +{% block body %} +

    CategoryPage

    + + + + + + + + + + + + + + + + +
    Id{{ category_page.id }}
    Name{{ category_page.name }}
    Active{{ category_page.active ? 'Yes' : 'No' }}
    + + back to list + + edit + + {{ include('category_page/_delete_form.html.twig') }} +{% endblock %} diff --git a/templates/department/_form.html.twig b/templates/department/_form.html.twig new file mode 100644 index 0000000..e8b0e10 --- /dev/null +++ b/templates/department/_form.html.twig @@ -0,0 +1,5 @@ +{% form_theme form 'bootstrap_4_layout.html.twig' %} +{{ form_start(form) }} + {{ form_widget(form) }} + +{{ form_end(form) }} diff --git a/templates/department/edit.html.twig b/templates/department/edit.html.twig new file mode 100644 index 0000000..065c93d --- /dev/null +++ b/templates/department/edit.html.twig @@ -0,0 +1,11 @@ +{% extends 'admin_base.html.twig' %} + +{% block title %}Edit Department{% endblock %} + +{% block body %} +

    Edit Department

    + + {{ include('department/_form.html.twig', {'button_label': 'Update'}) }} + + back to list +{% endblock %} diff --git a/templates/department/index.html.twig b/templates/department/index.html.twig new file mode 100644 index 0000000..7fefcc9 --- /dev/null +++ b/templates/department/index.html.twig @@ -0,0 +1,38 @@ +{% extends 'admin_base.html.twig' %} + +{% block title %}Department index{% endblock %} + +{% block body %} +

    Department index

    + + + + + + + + + + + + + + {% for department in departments %} + + + + + + + + + {% else %} + + + + {% endfor %} + +
    DcodeNameAliasActiveMiddleNameactions
    {{ department.did }}{{ department.name }}{{ department.alias }}{{ department.active ? 'Yes' : 'No' }}{{ department.middleName }} + edit +
    no records found
    +{% endblock %} diff --git a/templates/internal_api/swagger.html.twig b/templates/internal_api/swagger.html.twig new file mode 100644 index 0000000..ec29f3b --- /dev/null +++ b/templates/internal_api/swagger.html.twig @@ -0,0 +1,9 @@ +{% extends 'base_widget.html.twig' %} + +{% block title %}Open API sovamed{% endblock %} + +{% block body %} + +
    + +{% endblock %} diff --git a/templates/menu.html.twig b/templates/menu.html.twig new file mode 100644 index 0000000..8be82f6 --- /dev/null +++ b/templates/menu.html.twig @@ -0,0 +1,100 @@ +{% set route = app.request.get('_route') %} + + +{% if (isMobile() == false) %} +
    +{% endif %} + +{% if app.user %} + + + +{% endif %} + + +{% if (isMobile() == false) %} + +{% endif %} diff --git a/templates/page/_delete_form.html.twig b/templates/page/_delete_form.html.twig new file mode 100644 index 0000000..41fbf6c --- /dev/null +++ b/templates/page/_delete_form.html.twig @@ -0,0 +1,4 @@ +
    + + +
    diff --git a/templates/page/_form.html.twig b/templates/page/_form.html.twig new file mode 100644 index 0000000..e8b0e10 --- /dev/null +++ b/templates/page/_form.html.twig @@ -0,0 +1,5 @@ +{% form_theme form 'bootstrap_4_layout.html.twig' %} +{{ form_start(form) }} + {{ form_widget(form) }} + +{{ form_end(form) }} diff --git a/templates/page/edit.html.twig b/templates/page/edit.html.twig new file mode 100644 index 0000000..ecebf9b --- /dev/null +++ b/templates/page/edit.html.twig @@ -0,0 +1,13 @@ +{% extends 'admin_base.html.twig' %} + +{% block title %}Edit Page{% endblock %} + +{% block body %} +

    Edit Page

    + + {{ include('page/_form.html.twig', {'button_label': 'Update'}) }} + + back to list + + {{ include('page/_delete_form.html.twig') }} +{% endblock %} diff --git a/templates/page/index.html.twig b/templates/page/index.html.twig new file mode 100644 index 0000000..6d50904 --- /dev/null +++ b/templates/page/index.html.twig @@ -0,0 +1,43 @@ +{% extends 'admin_base.html.twig' %} + +{% block title %}Page index{% endblock %} + +{% block body %} +

    Page index

    + + + + + + + + + + + + + + {% for page in pages %} + + + + + + + + + {% else %} + + + + {% endfor %} + +
    IdNameDescriptionActiveAliasactions
    {{ page.id }}{{ page.name }}{{ page.description }}{{ page.active ? 'Yes' : 'No' }}{{ page.alias }} + show + edit +
    no records found
    + + Create new +
    + Все категории +{% endblock %} diff --git a/templates/page/new.html.twig b/templates/page/new.html.twig new file mode 100644 index 0000000..dcfbb4b --- /dev/null +++ b/templates/page/new.html.twig @@ -0,0 +1,11 @@ +{% extends 'admin_base.html.twig' %} + +{% block title %}New Page{% endblock %} + +{% block body %} +

    Create new Page

    + + {{ include('page/_form.html.twig') }} + + back to list +{% endblock %} diff --git a/templates/page/show.html.twig b/templates/page/show.html.twig new file mode 100644 index 0000000..d168633 --- /dev/null +++ b/templates/page/show.html.twig @@ -0,0 +1,7 @@ +{% extends 'base_widget.html.twig' %} + +{% block title %}{{ page.name }}{% endblock %} + +{% block body %} + {{ page.description | raw}} +{% endblock %} diff --git a/templates/report/_form.html.twig b/templates/report/_form.html.twig new file mode 100644 index 0000000..a0a2bd6 --- /dev/null +++ b/templates/report/_form.html.twig @@ -0,0 +1,3 @@ +{% form_theme form 'bootstrap_4_layout.html.twig' %} +{{ form_start(form) }} +{{ form_end(form) }} \ No newline at end of file diff --git a/templates/report/index.html.twig b/templates/report/index.html.twig new file mode 100644 index 0000000..7d2c4ea --- /dev/null +++ b/templates/report/index.html.twig @@ -0,0 +1,10 @@ +{% extends 'base.html.twig' %} + +{% block title %}{{ title }}{% endblock %} + +{% block body %} +
    + + +
    +{% endblock %} diff --git a/templates/review_source/_delete_form.html.twig b/templates/review_source/_delete_form.html.twig new file mode 100644 index 0000000..280091c --- /dev/null +++ b/templates/review_source/_delete_form.html.twig @@ -0,0 +1,4 @@ +
    + + +
    diff --git a/templates/review_source/_form.html.twig b/templates/review_source/_form.html.twig new file mode 100644 index 0000000..e8b0e10 --- /dev/null +++ b/templates/review_source/_form.html.twig @@ -0,0 +1,5 @@ +{% form_theme form 'bootstrap_4_layout.html.twig' %} +{{ form_start(form) }} + {{ form_widget(form) }} + +{{ form_end(form) }} diff --git a/templates/review_source/edit.html.twig b/templates/review_source/edit.html.twig new file mode 100644 index 0000000..ba2a62d --- /dev/null +++ b/templates/review_source/edit.html.twig @@ -0,0 +1,13 @@ +{% extends 'admin_base.html.twig' %} + +{% block title %}Edit ReviewSource{% endblock %} + +{% block body %} +

    Edit ReviewSource

    + + {{ include('review_source/_form.html.twig', {'button_label': 'Update'}) }} + + back to list + + {{ include('review_source/_delete_form.html.twig') }} +{% endblock %} diff --git a/templates/review_source/index.html.twig b/templates/review_source/index.html.twig new file mode 100644 index 0000000..afc58ff --- /dev/null +++ b/templates/review_source/index.html.twig @@ -0,0 +1,42 @@ +{% extends 'admin_base.html.twig' %} + +{% block title %}ReviewSource index{% endblock %} + +{% block body %} +

    ReviewSource index

    + + + + + + + + + + + + + + + {% for review_source in review_sources %} + + + + + + + + + + {% else %} + + + + {% endfor %} + +
    IdNameCountRowRatingFilialActiveactions
    {{ review_source.id }}{{ review_source.name }}{{ review_source.countRow }}{{ review_source.rating }}{{ review_source.filial.address }}{{ review_source.active }} + edit +
    no records found
    + + Create new +{% endblock %} diff --git a/templates/review_source/new.html.twig b/templates/review_source/new.html.twig new file mode 100644 index 0000000..1156164 --- /dev/null +++ b/templates/review_source/new.html.twig @@ -0,0 +1,11 @@ +{% extends 'admin_base.html.twig' %} + +{% block title %}New ReviewSource{% endblock %} + +{% block body %} +

    Create new ReviewSource

    + + {{ include('review_source/_form.html.twig') }} + + back to list +{% endblock %} diff --git a/templates/security/card.html.twig b/templates/security/card.html.twig new file mode 100644 index 0000000..699f398 --- /dev/null +++ b/templates/security/card.html.twig @@ -0,0 +1,46 @@ +{% extends template ~ '.html.twig' %} + +{% block title %} + + {% if app.request.get('tab') == 'allTest' %} + Результаты анализов + {% else %} + {{ title }} + {% endif %} + +{% endblock %} + +{% block body %} + +
    +
    +
    + {% if(isMobile()) %} +
    + +
    + + {% else %} + + + + {% endif %} + +
    Загрузка данных
    +
    +
    +
    +
    +
    + +{% endblock %} + + diff --git a/templates/security/case_history.html.twig b/templates/security/case_history.html.twig new file mode 100644 index 0000000..3022ada --- /dev/null +++ b/templates/security/case_history.html.twig @@ -0,0 +1,81 @@ +{% extends template ~ '.html.twig' %} + +{% block title %}{{ title }}{% endblock %} + +{% block body %} + +
    +
    +
    + {% if(isMobile()) %} +
    + +
    + {% else %} + + {% endif %} +
    + +
    Загрузка данных
    +
    +
    +
    + +
    + +
    +
    Приём детей с
    +
    Опыт работы
    +  {# Отзывы #} +
    +
    + +
    +
    +
    Ваша запись:
    ,
    + Отменить +
    +
    +
    +
    +
    + + + +
    + Чтобы активировать кнопку оплаты, подтвердите согласие. +
    +
    +
    + Повторить запись + {#
    добавить в календарь
    #} +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    + +{% endblock %} diff --git a/templates/security/confirm.html.twig b/templates/security/confirm.html.twig new file mode 100644 index 0000000..b3cd09f --- /dev/null +++ b/templates/security/confirm.html.twig @@ -0,0 +1,27 @@ +{% extends template ~ '.html.twig' %} + +{% block title %}Confirm{% endblock %} + +{% block body %} + {% for flashError in app.flashes('verify_email_error') %} + + {% endfor %} + +

    Confirm

    + + {{ form_start(form) }} + {{ form_errors(form) }} + {{ form_row(form._token) }} + +
    + {{ form_label(form.smsCode, 'smsCode') }} + {{ form_widget(form.smsCode, {'attr': {'class': 'form-control'}}) }} +
    + {{ form_errors(form.smsCode) }} +
    +
    + + + {{ form_end(form) }} + +{% endblock %} diff --git a/templates/security/login.html.twig b/templates/security/login.html.twig new file mode 100644 index 0000000..0f211d2 --- /dev/null +++ b/templates/security/login.html.twig @@ -0,0 +1,105 @@ + + + + + {% block title %}{{ title }}{% endblock %} + + {% block js %} + + {% endblock %} + + {% block stylesheets %} + + + {{ encore_entry_link_tags('app_sovamed') }} + {% endblock %} + + {% block javascripts %} + {{ encore_entry_script_tags('app_sovamed') }} + {% endblock %} + + + + + + + + {{ include('_change_region.html.twig') }} + + \ No newline at end of file diff --git a/templates/security/login_wmtmed.html.twig b/templates/security/login_wmtmed.html.twig new file mode 100644 index 0000000..127d541 --- /dev/null +++ b/templates/security/login_wmtmed.html.twig @@ -0,0 +1,104 @@ + + + + + {% block title %}{{ title }}{% endblock %} + + {% block js %} + + {% endblock %} + + {% block stylesheets %} + + + {{ encore_entry_link_tags('app_wmtmed') }} + {% endblock %} + + {% block javascripts %} + {{ encore_entry_script_tags('app_wmtmed') }} + {% endblock %} + + + + + + + {{ include('_change_region.html.twig') }} + + \ No newline at end of file diff --git a/templates/security/payment.html.twig b/templates/security/payment.html.twig new file mode 100644 index 0000000..bef2a51 --- /dev/null +++ b/templates/security/payment.html.twig @@ -0,0 +1,53 @@ +{% extends template ~ '.html.twig' %} + +{% block title %}{{ title }}{% endblock %} + +{% block bonusWidget %} +
    + {% if isShowInRegion([91,92,93]) %} + + {% endif %} +
    бонусов на счете: 0
    +
    +{% endblock %} + +{% block body %} + {% if app.request.get('pay') is not null %} + {% if app.request.get('pay') is defined and app.request.get('pay') == 'true' %} + + {% else %} + + {% endif %} + {% endif %} + +
    +
    +
    +
    ,
    +
    +
    + Счет № + от +
    +
    +
    Специалист:
    +
    +
    + +
    Оплачено 0 ₽
    +
    +
    +
    +
    +{% endblock %} diff --git a/templates/security/referrals.html.twig b/templates/security/referrals.html.twig new file mode 100644 index 0000000..4e48f8d --- /dev/null +++ b/templates/security/referrals.html.twig @@ -0,0 +1,28 @@ +{% extends template ~ '.html.twig' %} + +{% block title %}{{ title }}{% endblock %} + +{% block body %} +
    +
    +
    +
    +
    +
    +

    ,

    +

    +

    +
    +
    +

    +

    Специалист:
    + +

    +

    +
    +
    +
    +
    +
    +
    +{% endblock %} diff --git a/templates/security/refund_blank.html.twig b/templates/security/refund_blank.html.twig new file mode 100644 index 0000000..9f4caf8 --- /dev/null +++ b/templates/security/refund_blank.html.twig @@ -0,0 +1,76 @@ +{% extends 'base_pdf.html.twig' %} +{% block head %} + + + + + + +
    Распечатайте, подпишите и пришлите фото со своей почты на почту: {{ to_email }}
    +{% endblock %} +{% block body %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
     
    Генеральному директору {{ company_name }}
    {{ company_director }}
    От: {{ fio }}
    Паспорт: серия {{ passport_serial }} номер {{ passport_number }}
    Выдан: {{ passport_issued }}
    Дата выдачи: {{ passport_date }} г.
    Проживающий по адресу: {{ address }}
    Телефон: {{ phone }}
    Email: {{ email }}
    + Заявление
    о возврате денежных средств +
    + Прошу вернуть мне денежные средства в размере {{ sum }} руб. ({{ amountInWords(sum) }}) + оплаченные {{ refund_date }} г. за онлайн-консультацию. +
    + Основание возврата: {{ refund_bases }} +
    + Дата {{ current_date|date('d.m.Y') }} г. +
    + + + + + + +
    Распечатайте, подпишите и пришлите фото со своей почты на почту: {{ to_email }}
    +{% endblock %} \ No newline at end of file diff --git a/templates/security/refund_form.html.twig b/templates/security/refund_form.html.twig new file mode 100644 index 0000000..e5cafbd --- /dev/null +++ b/templates/security/refund_form.html.twig @@ -0,0 +1,38 @@ +{% extends 'base_widget.html.twig' %} + +{% block title %}refund{% endblock %} + +{% block body %} + +
    + + + {% form_theme form 'bootstrap_4_layout.html.twig' %} + +
    + + + + + + + + {{ form_widget(form) }} + +
    + +
    + +{% endblock %} diff --git a/templates/security/register.html.twig b/templates/security/register.html.twig new file mode 100644 index 0000000..7d76de9 --- /dev/null +++ b/templates/security/register.html.twig @@ -0,0 +1,141 @@ +{% extends template ~ '.html.twig' %} + +{% block title %}Регистрация пациента{% endblock %} + +{% block js %} + +{% endblock %} + +{% block top %} +
    +

    {{ title }}

    +
    +{% endblock %} + +{% block body %} +
    + +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    + +
    + +
    +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    + +
    указывайте действующий e-mail, к которому привязан ваш аккаунт
    на сайте Госуслуги
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    +
    + + +
    +
    + + +
    +
    +
    + +
    + +
    +
    +
    +
    + + +
    + +
    +
    +
    + +
    + +
    + +
    +
    +
    +
    +
    + +
    + +
    + +
    +
    +
    + +
    + +
    + +
    не менее 7 символов
    +
    +
    + +
    + + +
    +
    +{% endblock %} diff --git a/templates/security/setting.html.twig b/templates/security/setting.html.twig new file mode 100644 index 0000000..a4d8795 --- /dev/null +++ b/templates/security/setting.html.twig @@ -0,0 +1,42 @@ +{% extends template ~ '.html.twig' %} + +{% block title %}{{ title }}{% endblock %} + +{% block body %} + +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    + + {{ form_start(form) }} + {{ form_errors(form) }} + {{ form_row(form._token) }} + +
    + {{ form_label(form.plainPassword, 'Пароль') }} + {{ form_widget(form.plainPassword, {'attr': {'class': 'form-control'}}) }} + +
    + {{ form_errors(form.plainPassword) }} +
    +
    +
    + + {{ form_end(form) }} +
    +
    +
    + +{% endblock %} diff --git a/templates/specialist/_calendar.html.twig b/templates/specialist/_calendar.html.twig new file mode 100644 index 0000000..a35b32b --- /dev/null +++ b/templates/specialist/_calendar.html.twig @@ -0,0 +1,36 @@ + diff --git a/templates/specialist/_item.html.twig b/templates/specialist/_item.html.twig new file mode 100644 index 0000000..0c86dda --- /dev/null +++ b/templates/specialist/_item.html.twig @@ -0,0 +1,208 @@ +{% set onlineMode = 0 %} +{% set href = path('specialist_show', {'alias': specialist.alias}) %} + +{% if app.request.attributes.get('_route') in ['specialist_online_index'] %} + {% set onlineMode = 1 %} + {% set href = path('specialist_show', {'alias': specialist.alias, 'specialist_search' : { 'onlineMode': onlineMode }}) %} +{% endif %} + +{% if app.request.get('specialist_search')['onlineMode'] is defined and app.request.get('specialist_search')['onlineMode'] == 1 %} + {% set onlineMode = 1 %} + {% set href = path('specialist_show', {'alias': specialist.alias, 'specialist_search' : { 'onlineMode': onlineMode }}) %} +{% endif %} +{% set specialistMore = specialist.specialistMore %} + +
    +
    + {% if link %} + + {% else %} + + {% endif %} + +
    + {% if link %} + + {{ specialist.fio[0] }}
    + {{ specialist.fio[1] }}  + + {% if specialist.fio[2] is defined %} + {{ specialist.fio[2] }} + {% endif %} +
    + {% else %} +

    + {{ specialist.fio[0] }}
    + {{ specialist.fio[1] }}  + + {% if specialist.fio[2] is defined %} + {{ specialist.fio[2] }} + {% endif %} +

    + {% endif %} + + {% autoescape %} +
    {{ specialist.speciality }}
    + + {% if specialist.degree is defined and specialist.degree is not null %} +
    + {{ specialist.degree }} +
    + {% endif %} + + {% if specialist.kinder is defined and specialist.kinder is not null %} +
    + Приём детей с {{ specialist.kinder }} {{ textYear(specialist.kinder, false) }} +
    + {% endif %} + + {% if specialist.experience is defined and specialist.experience is not null %} +
    + Опыт работы: {{ specialist.experience }} {{ textYear(specialist.experience, true) }} +
    + {% endif %} + + {% if specialist.category is defined and specialist.category is not null %} +
    + Категория: {{ specialist.category }} +
    + {% endif %} + + {% if specialist.acceptsDms is defined and specialist.acceptsDms is not null %} +
    + {{ specialist.acceptsDms ? 'Принимает по ДМС' : 'Не принимает по ДМС' }} +
    + {% endif %} + + {% endautoescape %} + + {% if specialistMore.hasReviews %} + + Отзывы + + {% endif %} +
    +
    +
    +
    + + {% set render = true %} + + {% if specialistMore.locationsCount > 1 %} +
    + {% if onlineMode and render %} + {% for location in specialistMore.locations %} + {% if location.onlineMode == 1 and render %} + {% set render = false %} +
    + + Онлайн консультация + +
    + {% endif %} + + {% endfor %} + {% else %} + + {% endif %} +
    + {% else %} +
    + {% if specialistMore.defaultLocation.onlineMode|default(false) == true and onlineMode and render %} + {% set render = false %} +
    + + Онлайн консультация + +
    + {% else %} +
    + + {{ specialistMore.defaultLocation.addressName|default('null') }} + +
    + {% endif %} +
    + {% endif %} +
    + {% if specialist.infoclinica %} +

    Удобное время для записи: загружается

    + {% else %} +

    Вы можете записаться, оставив заявку

    + {% endif %} + +
    + {% if specialist.infoclinica %} +
    +
    + Все даты +
    + {% else %} +
    + +
    + {% endif %} +
    +
    + {% set route = app.request.get('_route') %} + {% if isShowInRegion([91,92,93]) %} + {% if specialistMore.minPrice is not null and route != 'specialist_online_index' and onlineMode != 1 %} + Прием от {{ specialistMore.minPrice.priceInfo.price }} ₽ + {% else %} +   + {% endif %} + {% endif %} +
    + + +
    +
    +
    +
    \ No newline at end of file diff --git a/templates/specialist/_reviews.html.twig b/templates/specialist/_reviews.html.twig new file mode 100644 index 0000000..8f05b2c --- /dev/null +++ b/templates/specialist/_reviews.html.twig @@ -0,0 +1,43 @@ +{% if specialistMore.hasReviews %} +{% set reviews = specialistMore.reviews %} + +{% endif %} \ No newline at end of file diff --git a/templates/specialist/_search_form.html.twig b/templates/specialist/_search_form.html.twig new file mode 100644 index 0000000..f7a76fa --- /dev/null +++ b/templates/specialist/_search_form.html.twig @@ -0,0 +1,74 @@ + + + + + + +{{ form_start(searchForm, {'attr': {'data-controller': 'kinderFilter'}}) }} +
    + {{ form_widget(searchForm.kinder, {'attr': { + 'class': 'filter__select', + 'data-controller' : 'selectpicker' + }}) }} +
    +
    + {{ form_widget(searchForm.order_by, {'attr': {'class': 'filter__select d-none'}}) }} +
    +
    + {{ form_widget(searchForm.name, {'attr': {'class': 'filter__select d-none'}}) }} +
    +
    + {{ form_widget(searchForm.department, {'attr': { + 'class': 'filter__select', + 'data-live-search': 'true', + 'data-controller' : 'selectpicker' + }}) }} +
    + +
    + {{ form_widget(searchForm.category, {'attr': { + 'class': 'filter__select', + 'data-live-search': 'true', + 'data-controller' : 'selectpicker', + 'data-placeholder': 'Категория' + }}) }} +
    +
    + {{ form_widget(searchForm.filial, {'attr': { + 'class': 'filter__select selectpicker', + 'data-controller' : 'selectpicker', + 'data-placeholder': 'Все клиники' + }}) }} +
    +
    + {{ form_widget(searchForm.current_date, {'attr': { + 'class': 'filter__input filter__input--date', + 'data-controller': 'datePicker', + 'data-quickDateRange-target': 'input', + 'range': 'true', + 'placeholder': 'Сегодня', + 'autocomplete': 'off' + }}) }} +
    + + +
    +
    +
    + {{ form_widget(searchForm.onlineMode, {'attr': {'class': 'filter__checkbox'}}) }} + {{ form_label(searchForm.onlineMode, 'Онлайн консультация', {'label_attr': {'class': 'filter__checkbox-label'}}) }} +
    + + {{ form_row(searchForm._token) }} +
    + +
    +{{ form_end(searchForm) }} \ No newline at end of file diff --git a/templates/specialist/admin/_form.html.twig b/templates/specialist/admin/_form.html.twig new file mode 100644 index 0000000..b6a9449 --- /dev/null +++ b/templates/specialist/admin/_form.html.twig @@ -0,0 +1,5 @@ +{% form_theme searchForm 'bootstrap_4_layout.html.twig' %} +{{ form_start(searchForm) }} + {{ form_widget(searchForm) }} + +{{ form_end(searchForm) }} diff --git a/templates/specialist/admin/_toggle_form.html.twig b/templates/specialist/admin/_toggle_form.html.twig new file mode 100644 index 0000000..e118f41 --- /dev/null +++ b/templates/specialist/admin/_toggle_form.html.twig @@ -0,0 +1,4 @@ +
    + + +
    diff --git a/templates/specialist/admin/index.html.twig b/templates/specialist/admin/index.html.twig new file mode 100644 index 0000000..6ee2f1f --- /dev/null +++ b/templates/specialist/admin/index.html.twig @@ -0,0 +1,113 @@ +{% extends 'admin_base.html.twig' %} + +{% block title %}{{ title }}{% endblock %} + +{% block body %} +

    {{ title }}

    +
    + {{ include('specialist/admin/_form.html.twig') }} +
    + + + + + + + + + + + + + + + + + + {% for specialist in pagination %} + + + + + + {% if specialist.locationAllCount > 0 %} + + {% else %} + + {% endif %} + + {% if specialist.locationAllCount > 0 %} + + {% else %} + + {% endif %} + + {% if specialist.locationAllCount > 0 %} + + {% else %} + + {% endif %} + + {% if specialist.locationAllCount > 0 %} + + {% else %} + + {% endif %} + + {% if specialist.locationAllCount == 0 %} + + {% else %} + + {% endif %} + + {% if specialist.locationAllCount > 0 %} + + {% else %} + + {% endif %} + + {% if specialist.updated is defined %} + + {% else %} + + {% endif %} + + + {% else %} + + + + {% endfor %} + +
    IDFIOPictureAddressDepartmentDcode or bitrixIDActiveActionsUpdated LocationUpdate
    {{ specialist.id }}{{ specialist.name }}show picture + {% for location in specialist.locationAll %} +

    {{ location.filial.address }}

    + {% endfor %} +
    there is no data + {% for location in specialist.locationAll %} +

    {{ location.department.did }}

    + {% endfor %} +
    there is no data + {% for location in specialist.locationAll %} +

    {{ location.dcode }}

    + {% endfor %} +
    there is no data + {% for location in specialist.locationAll %} +

    {{ location.active ? 'on' : 'off' }}

    + {% endfor %} +
    there is no datadisabled + {% for location in specialist.locationAll %} + {{ include('specialist/admin/_toggle_form.html.twig') }} + {% endfor %} + + {% for location in specialist.locationAll %} +

    {{ location.updated|date('d.m.Y') }}

    + {% endfor %} +
    there is no data +

    {{ specialist.updated|date('d.m.Y') }}

    +
    there is no data
    no records found
    + +
    + {{ knp_pagination_render(pagination) }} +
    +{% endblock %} diff --git a/templates/specialist/index.html.twig b/templates/specialist/index.html.twig new file mode 100644 index 0000000..293bf6a --- /dev/null +++ b/templates/specialist/index.html.twig @@ -0,0 +1,73 @@ +{% extends template ~ '.html.twig' %} + +{% block title %} {{ title }} {% endblock %} + +{% block js %} + +{% endblock %} + +{% block filter %} +
    Найти специалиста
    + {{ include('specialist/_search_form.html.twig', {'searchForm': searchForm}) }} +{% endblock %} + +{% block body %} +
    + {% if(isMobile() == false) %} +
    +
    + Не нашли нужного врача?  + Оставьте заявку для записи +
    +
    + + + + по времени приема + + {# + + + по стоимости приема + #} + + + + по афавиту + +
    +
    + {% else %} +
    +
    + + +
    + + + + + фильтр + +
    + {% endif %} + +
    + + {% for specialist in pagination %} + {% include '/specialist/_item.html.twig' with {'link': true} %} + {% else %} +

    Не найдено, повторите поиск

    + {% endfor %} +
    +
    + {{ knp_pagination_render(pagination) }} +
    +
    + +{% include '/specialist/_calendar.html.twig' %} + +{% endblock %} \ No newline at end of file diff --git a/templates/specialist/show.html.twig b/templates/specialist/show.html.twig new file mode 100644 index 0000000..1690b7a --- /dev/null +++ b/templates/specialist/show.html.twig @@ -0,0 +1,42 @@ +{% extends template ~ '.html.twig' %} + +{% block title %}{{ title }} - {{ specialist.name }}{% endblock %} + +{% block js %} + +{% endblock %} + +{% block body %} +
    +
    +
    {{ specialistMore.defaultLocation.address }}
    +
    + {% include '/specialist/_item.html.twig' with {'link': false} %} +
    +
    + {% if isShowInRegion([91,92,93]) %} + {% if specialistMore.hasPrice %} +
    +
    +

    Стоимость

    +
      + {% for price in specialistMore.prices %} +
    • {{ price.schname }} — {{ price.priceInfo.price }} ₽
    • + {% endfor %} +
    +
    + {% endif %} + {% endif %} +
    +
    + {% autoescape 'html' %} + {{ specialist.description|raw }} + {% endautoescape %} +
    + + {% include '/specialist/_reviews.html.twig' %} +
    + + {% include '/specialist/_calendar.html.twig' %} + +{% endblock %} diff --git a/templates/widget/reference.html.twig b/templates/widget/reference.html.twig new file mode 100644 index 0000000..d1ebd0d --- /dev/null +++ b/templates/widget/reference.html.twig @@ -0,0 +1,100 @@ +{% extends 'base_widget.html.twig' %} + +{% block title %}Reference{% endblock %} + +{% block body %} + {% set referer = app.request.get('ref')|default('/') %} + +
    + {% form_theme referenceForm 'bootstrap_4_layout.html.twig' %} + {{ form_start(referenceForm) }} + {{ form_row(referenceForm._token) }} +
    + {{ form_label(referenceForm.autorName, 'ФИО налогоплательщика:', {'label_attr': {'class': 'mb-0'}}) }} + {{ form_widget(referenceForm.autorName, {'attr': {'class': 'form-control'}}) }} +
    +
    +
    + {{ form_label(referenceForm.phone, 'Телефон:', {'label_attr': {'class': 'mb-0'}}) }} + {{ form_widget(referenceForm.phone, {'attr': { + 'class': 'form-control', + 'data-controller': 'inputMask' + }}) }} +
    +
    +
    + {{ form_label(referenceForm.birthDate, 'Дата рождения налогоплательщика:', {'label_attr': {'class': 'mb-0'}}) }} + {{ form_widget(referenceForm.birthDate, {'attr': { + 'class': 'form-control', + 'data-controller': 'datePicker', + 'range': 'false' + }}) }} +
    +
    +
    + {{ form_label(referenceForm.inn, 'ИНН налогоплательщика:', {'label_attr': {'class': 'mb-0'}}) }} + {{ form_widget(referenceForm.inn, {'attr': {'class': 'form-control'}}) }} +
    +
    + +
    +
    + {{ form_label(referenceForm.responsible, 'Получаю документы за:', {'label_attr': {'class': 'mb-0'}}) }} + {{ form_widget(referenceForm.responsible, {'attr': { + 'class': 'reference-responsible form-control', + 'data-count' : '0', + }}) }} +
    +
    +
    + {{ form_label(referenceForm.filial, 'Клиника, где оказывались услуги:', {'label_attr': {'class': 'mb-0'}}) }} + {{ form_widget(referenceForm.filial, {'attr': { + 'class': 'reference-filial form-control', + }}) }} +
    +
    +
    + {{ form_label(referenceForm.periodFirst, 'Начало периода:', {'label_attr': {'class': 'mb-0'}}) }} + {{ form_widget(referenceForm.periodFirst, {'attr': { + 'class': 'reference-period-first', + 'type' : 'date', + 'value' : "now"|date_modify("-1 year")|date("Y-01-01") + }}) }} +
    +
    +
    + {{ form_label(referenceForm.periodLast, 'Конец периода:', {'label_attr': {'class': 'mb-0'}}) }} + {{ form_widget(referenceForm.periodLast, {'attr': { + 'class': 'reference-period-last', + 'type' : 'date', + 'value' : "now"|date_modify("-1 year")|date("Y-12-31") + }}) }} +
    +
    +
    +
    + {{ form_label(referenceForm.sending, 'Пакет документов прошу выдать:', {'label_attr': {'class': 'mb-0'}}) }} + {{ form_widget(referenceForm.sending, {'attr': { + 'class': 'reference-sending', + }}) }} +
    +
    +
    +
    + {{ form_label(referenceForm.filialSending, 'Клиника, где получить справку:', {'label_attr': {'class': 'mb-0'}}) }} + {{ form_widget(referenceForm.filialSending, {'attr': {'class': 'form-control'}}) }} +
    +
    +
    +
    +
    + + + + {{ form_end(referenceForm) }} +
    +{% endblock %} diff --git a/templates/widget/review_source.html.twig b/templates/widget/review_source.html.twig new file mode 100644 index 0000000..c1ac036 --- /dev/null +++ b/templates/widget/review_source.html.twig @@ -0,0 +1,40 @@ +{% extends 'base_widget.html.twig' %} + +{% block title %}WidgetController{% endblock %} + +{% block body %} +
    +
    + {% for item in reviewSources %} +
    +
    +
    + {{item.name}} +
    +
    +
    + {% for i in 1..5 %} + {% if item.rating_total|ceil >= i %} + {% if item.isFloat and item.rating_total|ceil == i %} + + {% else %} + + {% endif %} + {% else %} + + {% endif %} + {% endfor %} +
    +

    + {% if item.count_row_total > 0 %} + {{ item.count_row_total }} оценок. + {% endif %} + В среднем - {{ rFloat(item.rating_total) }} +

    +
    +
    +
    + {% endfor %} +
    +
    +{% endblock %} diff --git a/templates/widget_form/_delete_form.html.twig b/templates/widget_form/_delete_form.html.twig new file mode 100644 index 0000000..6997312 --- /dev/null +++ b/templates/widget_form/_delete_form.html.twig @@ -0,0 +1,4 @@ +
    + + +
    diff --git a/templates/widget_form/_form.html.twig b/templates/widget_form/_form.html.twig new file mode 100644 index 0000000..a80b680 --- /dev/null +++ b/templates/widget_form/_form.html.twig @@ -0,0 +1,5 @@ +{% form_theme form 'bootstrap_4_layout.html.twig' %} +{{ form_start(form) }} + {{ form_widget(form) }} + +{{ form_end(form) }} diff --git a/templates/widget_form/edit.html.twig b/templates/widget_form/edit.html.twig new file mode 100644 index 0000000..138bc87 --- /dev/null +++ b/templates/widget_form/edit.html.twig @@ -0,0 +1,13 @@ +{% extends 'admin_base.html.twig' %} + +{% block title %}Edit WidgetForm{% endblock %} + +{% block body %} +

    Edit WidgetForm

    + + {{ include('widget_form/_form.html.twig', {'button_label': 'Update'}) }} + + back to list + + {{ include('widget_form/_delete_form.html.twig') }} +{% endblock %} diff --git a/templates/widget_form/editor.html.twig b/templates/widget_form/editor.html.twig new file mode 100644 index 0000000..44b8002 --- /dev/null +++ b/templates/widget_form/editor.html.twig @@ -0,0 +1,34 @@ +{% extends 'admin_base.html.twig' %} + +{% block title %}WidgetForm{% endblock %} + +{% block body %} +

    {{ widget_form.name }}

    +
    +
    +
    + {% for input in form_input %} +
    + + Изменить + {% if input.type == 'text' %} + + {% elseif input.type == 'phone' %} + + {% elseif input.type == 'date' %} + + {% elseif input.type == 'textarea' %} + + {% endif %} + +
    + {% endfor %} +
    +
    +
    +

    Добавить элемент

    + {{ include('widget_form/_form.html.twig', {'button_label': 'Добавить поле'}) }} +
    +
    + back to list +{% endblock %} diff --git a/templates/widget_form/index.html.twig b/templates/widget_form/index.html.twig new file mode 100644 index 0000000..0f23e8d --- /dev/null +++ b/templates/widget_form/index.html.twig @@ -0,0 +1,37 @@ +{% extends 'admin_base.html.twig' %} + +{% block title %}WidgetForm index{% endblock %} + +{% block body %} +

    Виджет формы

    + + + + + + + + + + + + {% for widget_form in widget_forms %} + + + + + + + {% else %} + + + + {% endfor %} + +
    IdNameLinkactions
    {{ widget_form.id }}{{ widget_form.name }}{{ absolute_url(path('widget_form_show', {'id': widget_form.id})) }} + настроить поля формы
    + редактировать форму +
    no records found
    + + Create new +{% endblock %} diff --git a/templates/widget_form/new.html.twig b/templates/widget_form/new.html.twig new file mode 100644 index 0000000..ba74dd9 --- /dev/null +++ b/templates/widget_form/new.html.twig @@ -0,0 +1,11 @@ +{% extends 'admin_base.html.twig' %} + +{% block title %}New WidgetForm{% endblock %} + +{% block body %} +

    Create new WidgetForm

    + + {{ include('widget_form/_form.html.twig') }} + + back to list +{% endblock %} diff --git a/templates/widget_form/show.html.twig b/templates/widget_form/show.html.twig new file mode 100644 index 0000000..80693fb --- /dev/null +++ b/templates/widget_form/show.html.twig @@ -0,0 +1,72 @@ +{% extends 'base_widget.html.twig' %} + +{% block title %}WidgetForm index{% endblock %} + +{% block js %} + +{% endblock %} + +{% block body %} + {% if renderForm %} +
    + {% set query = app.request.query.all %} + {% for key, queryParam in query %} + {% if key == 'fields' %} + {% for field, val in queryParam %} + + {% endfor %} + {% endif %} + {% if key != 'hidden' and key != 'fields' %} + + {% endif %} + {% endfor %} + + + + {% for input in widget_form.widgetFormInputs %} + {% set hiddenVal = false %} + {% if query.hidden is defined and query.hidden[input.bitrix24Id] is defined %} + {% set hiddenVal = query.hidden[input.bitrix24Id] %} + {% endif %} +
    + {% if input.type == 'text' %} + {% if hiddenVal == false %} + {# #} + + {% else %} + + {% endif %} + {% elseif input.type == 'phone' %} + {# #} + + + {% elseif input.type == 'date' %} + + + {% elseif input.type == 'textarea' %} + {% if hiddenVal == false %} + {# #} + + {% else %} + + {% endif %} + {% endif %} +
    + {% endfor %} + + +
    +
    + +
    +
    + {% else %} +
    + ok +

    Ваша заявка отправлена!
    Мы свяжемся с Вами в ближайшее время.

    +
    + {% endif %} + +{% endblock %} diff --git a/templates/widget_form_input/_delete_form.html.twig b/templates/widget_form_input/_delete_form.html.twig new file mode 100644 index 0000000..eadd5d7 --- /dev/null +++ b/templates/widget_form_input/_delete_form.html.twig @@ -0,0 +1,4 @@ +
    + + +
    diff --git a/templates/widget_form_input/_form.html.twig b/templates/widget_form_input/_form.html.twig new file mode 100644 index 0000000..2fc8735 --- /dev/null +++ b/templates/widget_form_input/_form.html.twig @@ -0,0 +1,6 @@ +{% form_theme form 'bootstrap_4_layout.html.twig' %} +{{ form_start(form) }} + {{ form_widget(form) }} + + +{{ form_end(form) }} diff --git a/templates/widget_form_input/edit.html.twig b/templates/widget_form_input/edit.html.twig new file mode 100644 index 0000000..a5c663d --- /dev/null +++ b/templates/widget_form_input/edit.html.twig @@ -0,0 +1,13 @@ +{% extends 'admin_base.html.twig' %} + +{% block title %}Edit WidgetFormInput{% endblock %} + +{% block body %} +

    Edit WidgetFormInput

    + + {{ include('widget_form_input/_form.html.twig', {'button_label': 'Update'}) }} + + back to list + + {{ include('widget_form_input/_delete_form.html.twig', {'id': widget_form_input.id, 'form_id': widget_form_input.widgetForm.id}) }} +{% endblock %} diff --git a/templates/widget_form_input/new.html.twig b/templates/widget_form_input/new.html.twig new file mode 100644 index 0000000..7c60749 --- /dev/null +++ b/templates/widget_form_input/new.html.twig @@ -0,0 +1,11 @@ +{% extends 'admin_base.html.twig' %} + +{% block title %}New WidgetFormInput{% endblock %} + +{% block body %} +

    Create new WidgetFormInput

    + + {{ include('widget_form_input/_form.html.twig') }} + + back to list +{% endblock %} diff --git a/tests/Controller/OnlineSpecialistsControllerTest.php b/tests/Controller/OnlineSpecialistsControllerTest.php new file mode 100644 index 0000000..0ebec92 --- /dev/null +++ b/tests/Controller/OnlineSpecialistsControllerTest.php @@ -0,0 +1,37 @@ + 'PHPUnit/OnlineConsultationTest']; + + public function testOnlineSpecialistsRequiresAuthentication(): void + { + $client = static::createClient(); + $client->request('GET', '/online-specialists', [], [], self::BROWSER_HEADERS); + + $this->assertTrue( + $client->getResponse()->isRedirect() || $client->getResponse()->getStatusCode() === 401, + 'Online specialists page must require authentication' + ); + } + + public function testOfflineSpecialistsListIsPublic(): void + { + $client = static::createClient(); + $client->request('GET', '/specialists', [], [], self::BROWSER_HEADERS); + + $this->assertResponseIsSuccessful(); + } + + public function testIntervalApiRequiresParameters(): void + { + $client = static::createClient(); + $client->request('GET', '/api/interval', [], [], self::BROWSER_HEADERS); + + $this->assertGreaterThanOrEqual(400, $client->getResponse()->getStatusCode()); + } +} diff --git a/tests/Unit/Support/OnlineModeTest.php b/tests/Unit/Support/OnlineModeTest.php new file mode 100644 index 0000000..d56cfd6 --- /dev/null +++ b/tests/Unit/Support/OnlineModeTest.php @@ -0,0 +1,36 @@ +assertSame($expected, OnlineMode::isOnline($input)); + $this->assertSame($expected ? 1 : 0, OnlineMode::toInt($input)); + } + + public function onlineValuesProvider(): array + { + return [ + 'int 1' => [1, true], + 'int 0' => [0, false], + 'string 1' => ['1', true], + 'string 0' => ['0', false], + 'true bool' => [true, true], + 'false bool' => [false, false], + 'true string' => ['true', true], + 'false string' => ['false', false], + 'yes' => ['yes', true], + 'empty' => ['', false], + 'null' => [null, false], + 'garbage' => ['maybe', false], + ]; + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..469dcce --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,11 @@ +bootEnv(dirname(__DIR__).'/.env'); +} diff --git a/translations/.gitignore b/translations/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/translations/KnpPaginatorBundle.ru.yaml b/translations/KnpPaginatorBundle.ru.yaml new file mode 100644 index 0000000..4ecc720 --- /dev/null +++ b/translations/KnpPaginatorBundle.ru.yaml @@ -0,0 +1,2 @@ +label_previous: "Назад" +label_next: "Вперед" \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..79cbcf4 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,83 @@ +const Encore = require('@symfony/webpack-encore'); + +// Manually configure the runtime environment if not already configured yet by the "encore" command. +// It's useful when you use tools that rely on webpack.config.js file. +if (!Encore.isRuntimeEnvironmentConfigured()) { + Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev'); +} + +Encore + // directory where compiled assets will be stored + .setOutputPath('public/build/') + // public path used by the web server to access the output path + .setPublicPath('/build') + // only needed for CDN's or sub-directory deploy + //.setManifestKeyPrefix('build/') + + /* + * ENTRY CONFIG + * + * Each entry will result in one JavaScript file (e.g. app.js) + * and one CSS file (e.g. app.css) if your JavaScript imports CSS. + */ + .addEntry('app_sovamed', './assets/loader_sovamed.js') + .addEntry('app_wmtmed', './assets/loader_wmtmed.js') + .addEntry('app_widget', './assets/loader_widget.js') + .addEntry('app_bitrix', './assets/loader_bitrix.js') + + // enables the Symfony UX Stimulus bridge (used in assets/bootstrap.js) + .enableStimulusBridge('./assets/controllers.json') + + // When enabled, Webpack "splits" your files into smaller pieces for greater optimization. + .splitEntryChunks() + + // will require an extra script tag for runtime.js + // but, you probably want this, unless you're building a single-page app + .enableSingleRuntimeChunk() + + /* + * FEATURE CONFIG + * + * Enable & configure other features below. For a full + * list of features, see: + * https://symfony.com/doc/current/frontend.html#adding-more-features + */ + .cleanupOutputBeforeBuild() + .enableBuildNotifications() + .enableSourceMaps(!Encore.isProduction()) + // enables hashed filenames (e.g. app.abc123.css) + .enableVersioning(Encore.isProduction()) + + .configureBabel((config) => { + config.plugins.push('@babel/plugin-proposal-class-properties'); + }) + + // enables @babel/preset-env polyfills + .configureBabelPresetEnv((config) => { + config.useBuiltIns = 'usage'; + config.corejs = 3; + }) + + // enables Sass/SCSS support + .enableSassLoader() + + // uncomment if you use TypeScript + //.enableTypeScriptLoader() + + // uncomment if you use React + //.enableReactPreset() + + // uncomment to get integrity="..." attributes on your script & link tags + // requires WebpackEncoreBundle 1.4 or higher + //.enableIntegrityHashes(Encore.isProduction()) + + // uncomment if you're having problems with a jQuery plugin + //.autoProvidejQuery() + .autoProvideVariables({ + '$': 'jquery', + 'jQuery': 'jquery', + 'window.jQuery': 'jquery' + }) +; + +module.exports = Encore.getWebpackConfig();