0 php /** * Поля сущностей «Дель Кам» — регистрация в коде. * * Почему в коде, а не кликами в админке: группы полей, нарисованные мышкой, живут * в базе. Их нельзя ни просмотреть в диффе, ни перенести со staging на прод иначе * как выгрузкой базы, ни восстановить, если кто-то случайно удалил группу. * acf_add_local_field_group кладёт схему в git и делает её частью кода. * * Лицензия ACF Pro тут ни при чём: локальные группы работают и без активной лицензии, * лицензия нужна только для обновлений плагина. * * @package delkam-core */ defined( 'ABSPATH' ) || exit; /** * Локация «этот тип записи». * * @param string $post_type Слаг типа записи. * @return array */ function delkam_acf_location( $post_type ) { return array( array( array( 'param' => 'post_type', 'operator' => '==', 'value' => $post_type, ), ), ); } add_action( 'acf/init', function () { if ( ! function_exists( 'acf_add_local_field_group' ) ) { return; } $has_pro = function_exists( 'acf_get_setting' ) && acf_get_setting( 'pro' ); // ------------------------------------------------------------------ // 0. Страница настроек и общие файлы // ------------------------------------------------------------------ if ( function_exists( 'acf_add_options_page' ) ) { acf_add_options_page( array( 'page_title' => 'Дель Кам — настройки', 'menu_title' => 'Настройки Дель Кам', 'menu_slug' => 'delkam-settings', 'capability' => 'manage_options', 'position' => '20.4', 'icon_url' => 'dashicons-admin-settings', 'redirect' => false, ) ); acf_add_local_field_group( array( 'key' => 'group_dk_settings', 'title' => 'Файлы и общие данные', 'location' => array( array( array( 'param' => 'options_page', 'operator' => '==', 'value' => 'delkam-settings', ), ), ), 'fields' => array( array( 'key' => 'field_dk_price_file', 'label' => 'Файл прейскуранта', 'name' => 'price_file', 'type' => 'file', 'return_format' => 'array', 'instructions' => 'Файл для кнопки «Скачать полный прейскурант». ' . 'Собирается командой tools/export-price-file.php из позиций прайса — ' . 'только базовые цены. Исходный xlsx клиники выкладывать нельзя: ' . 'в нём колонки ДМС и внутренние наценки.', ), array( 'key' => 'field_dk_price_file_date', 'label' => 'Прейскурант актуален на', 'name' => 'price_file_date', 'type' => 'text', 'instructions' => 'Дата, которую показываем рядом с кнопкой.', ), array( 'key' => 'field_dk_installment_months', 'label' => 'Рассрочка: срок, месяцев', 'name' => 'installment_months', 'type' => 'number', 'default_value' => 36, 'min' => 1, 'max' => 120, 'step' => 1, 'required' => 1, 'wrapper' => array( 'width' => '33' ), ), array( 'key' => 'field_dk_installment_percent', 'label' => 'Рассрочка: процент', 'name' => 'installment_percent', 'type' => 'number', 'default_value' => 0, 'min' => 0, 'max' => 100, 'step' => 0.1, 'required' => 1, 'instructions' => 'Помесячный расчёт на сайте включается только при 0%. Для иной ставки сначала нужна согласованная финансовая формула.', 'wrapper' => array( 'width' => '33' ), ), array( 'key' => 'field_dk_installment_threshold', 'label' => 'Рассрочка: порог цены, ₽', 'name' => 'installment_threshold', 'type' => 'number', 'default_value' => 50000, 'min' => 0, 'step' => 1000, 'required' => 1, 'instructions' => 'Подсказка «от N ₽/мес.» появляется только у текущей цены строго выше этого значения.', 'wrapper' => array( 'width' => '34' ), ), array( 'key' => 'field_dk_price_benefits', 'label' => 'Преимущества рядом с ценами', 'name' => 'price_benefits', 'type' => 'repeater', 'layout' => 'block', 'min' => 3, 'max' => 3, 'button_label' => 'Добавить преимущество', 'instructions' => 'Единый набор из трёх карточек для всех блоков цен: на страницах услуг, главной и полном прейскуранте.', 'sub_fields' => array( array( 'key' => 'field_dk_price_benefit_badge', 'label' => 'Короткий акцент', 'name' => 'benefit_badge', 'type' => 'text', 'required' => 1, ), array( 'key' => 'field_dk_price_benefit_title', 'label' => 'Заголовок', 'name' => 'benefit_title', 'type' => 'text', 'required' => 1, ), array( 'key' => 'field_dk_price_benefit_text', 'label' => 'Пояснение', 'name' => 'benefit_text', 'type' => 'textarea', 'rows' => 3, 'required' => 1, ), ), ), array( 'key' => 'field_dk_price_promotions', 'label' => 'Акции на позиции прайса', 'name' => 'price_promotions', 'type' => 'repeater', 'layout' => 'block', 'button_label' => 'Добавить акцию', 'instructions' => 'Без дат акция действует бессрочно. Можно заполнить только одну границу периода.', 'sub_fields' => array( array( 'key' => 'field_dk_price_promotion_item', 'label' => 'Позиция прайса', 'name' => 'promotion_item', 'type' => 'post_object', 'post_type' => array( 'dk_price' ), 'post_status' => array( 'publish' ), 'return_format' => 'id', 'ui' => 1, 'required' => 1, 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_price_promotion_value', 'label' => 'Цена по акции, ₽', 'name' => 'promotion_value', 'type' => 'number', 'min' => 1, 'step' => 1, 'required' => 1, 'wrapper' => array( 'width' => '25' ), ), array( 'key' => 'field_dk_price_promotion_show_old', 'label' => 'Показать прежнюю цену', 'name' => 'promotion_show_old', 'type' => 'true_false', 'ui' => 1, 'default_value' => 0, 'wrapper' => array( 'width' => '25' ), ), array( 'key' => 'field_dk_price_promotion_conditions', 'label' => 'Условия акции', 'name' => 'promotion_conditions', 'type' => 'textarea', 'rows' => 2, 'required' => 1, ), array( 'key' => 'field_dk_price_promotion_start', 'label' => 'Начало акции', 'name' => 'promotion_start', 'type' => 'date_picker', 'display_format' => 'd.m.Y', 'return_format' => 'Ymd', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_price_promotion_end', 'label' => 'Окончание акции', 'name' => 'promotion_end', 'type' => 'date_picker', 'display_format' => 'd.m.Y', 'return_format' => 'Ymd', 'wrapper' => array( 'width' => '50' ), ), ), ), array( 'key' => 'field_dk_price_popular', 'label' => 'Популярные позиции прайса', 'name' => 'price_popular', 'type' => 'post_object', 'post_type' => array( 'dk_price' ), 'post_status' => array( 'publish' ), 'return_format' => 'id', 'multiple' => 1, 'ui' => 1, 'instructions' => 'Выбранные позиции получают отдельную оранжевую плашку «Популярно».', ), array( 'key' => 'field_dk_founded', 'label' => 'Год основания клиники', 'name' => 'founded', 'type' => 'number', 'min' => 1900, 'max' => 2100, 'instructions' => 'Четыре цифры. Из них считается «N лет работы» на главной ' . 'и в блоке «О клинике». Пока поле пустое, плашка не выводится: ' . 'год, вписанный в текст руками, придётся переписывать каждый январь, ' . 'и однажды его забудут.', ), array( 'key' => 'field_dk_hero_photo', 'label' => 'Фото первого экрана главной', 'name' => 'hero_photo', 'type' => 'image', 'return_format' => 'id', 'preview_size' => 'medium', 'instructions' => 'Живое фото клиники или команды. Пусто — первый экран ' . 'обходится без снимка, заглушку не подставляем.', ), array( 'key' => 'field_dk_phone', 'label' => 'Телефон клиники', 'name' => 'phone', 'type' => 'text', 'instructions' => 'Единственное место, где живёт номер. В шаблонах его нет: ' . 'если поле пустое, кнопка с телефоном просто не выводится.', ), array( 'key' => 'field_dk_phone_2', 'label' => 'Второй телефон', 'name' => 'phone_2', 'type' => 'text', 'instructions' => 'Городской, если он есть. Пусто — не выводится.', ), array( 'key' => 'field_dk_address', 'label' => 'Адрес', 'name' => 'address', 'type' => 'text', 'instructions' => 'Как пишем на сайте: г. Казань, ул. Спартаковская, д. 88б.', ), array( 'key' => 'field_dk_hours', 'label' => 'Часы работы', 'name' => 'hours', 'type' => 'text', 'instructions' => 'Пн-Пт 8:00–20:00 · Сб-Вс 8:00–17:00. Это первое, что проверяют ' . 'перед визитом: в старой шапке стояло «Сб», хотя воскресенье рабочее.', ), array( 'key' => 'field_dk_email', 'label' => 'Почта для пациентов', 'name' => 'email', 'type' => 'text', ), array( 'key' => 'field_dk_map_url', 'label' => 'Ссылка на карту', 'name' => 'map_url', 'type' => 'url', 'instructions' => 'Карточка клиники на Яндекс Картах — для кнопки «как проехать».', ), array( 'key' => 'field_dk_social', 'label' => 'Соцсети и мессенджеры', 'name' => 'social', 'type' => 'textarea', 'rows' => 4, 'instructions' => 'По одной на строку в виде «Название|ссылка». ' . 'Например: ВКонтакте|https://vk.com/stomdelkam', ), array( 'key' => 'field_dk_cta_url', 'label' => 'Куда ведёт кнопка записи', 'name' => 'cta_url', 'type' => 'text', 'instructions' => 'Сейчас — попап «Formzapis» Elementor. После снятия конструктора ' . 'меняется здесь одной строкой, шаблоны трогать не нужно.', 'default_value' => '#elementor-action%3Aaction%3Dpopup%3Aopen%26settings%3DeyJpZCI6IjE5OSIsInRvZ2dsZSI6ZmFsc2V9', ), ), ) ); } // ------------------------------------------------------------------ // 1. Услуга // ------------------------------------------------------------------ $service_fields = array( array( 'key' => 'field_dk_service_parent', 'label' => 'Родительская услуга', 'name' => 'service_parent', 'type' => 'post_object', 'post_type' => array( 'dk_service' ), 'return_format' => 'id', 'ui' => 1, 'allow_null' => 1, 'instructions' => 'Логический хаб для дочерней страницы. Публичный URL остаётся плоским: /uslugi/{slug}/.', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_service_related', 'label' => 'Связанные услуги', 'name' => 'service_related', 'type' => 'relationship', 'post_type' => array( 'dk_service' ), 'filters' => array( 'search', 'taxonomy' ), 'return_format' => 'id', 'instructions' => 'Необязательный ручной порядок. Текущая страница и её родитель исключаются автоматически.', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_service_prices', 'label' => 'Позиции прайса (ручной порядок)', 'name' => 'service_prices', 'type' => 'relationship', 'post_type' => array( 'dk_price' ), 'filters' => array( 'search', 'taxonomy' ), 'return_format' => 'id', 'instructions' => 'Пусто — действует прежняя связь price_service, затем подбор по направлению. По умолчанию выводятся только основные позиции.', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_service_prices_include_non_main', 'label' => 'Ручной прайс: показывать дополнительные позиции', 'name' => 'service_prices_include_non_main', 'type' => 'true_false', 'ui' => 1, 'default_value' => 0, 'instructions' => 'Действует только для точного ручного списка «Позиции прайса». Включено — список может показывать опубликованные позиции без флага «Основная». Автоподбор по price_service и направлению всегда остаётся только по основным позициям.', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_service_reviews_manual', 'label' => 'Отзывы (ручная подборка)', 'name' => 'service_reviews_manual', 'type' => 'relationship', 'post_type' => array( 'dk_review' ), 'filters' => array( 'search', 'taxonomy' ), 'return_format' => 'id', 'instructions' => 'Необязательная редакторская подборка в ручном порядке. Считается подборкой по направлению, а не отзывами именно об этой услуге. Негативные и не прошедшие рекламный стоп-лист записи не выводятся.', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_service_reviews_exact_only', 'label' => 'Отзывы: только точная связь', 'name' => 'service_reviews_exact_only', 'type' => 'true_false', 'ui' => 1, 'default_value' => 0, 'instructions' => 'Включено — после ручной подборки допускаются только отзывы с review_service, привязанным к этой странице. Если обоих источников нет, автоматический fallback по направлению запрещён. Ручная подборка имеет приоритет и не получает Review JSON-LD.', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_service_eyebrow', 'label' => 'Надзаголовок', 'name' => 'service_eyebrow', 'type' => 'text', 'instructions' => 'Короткая строка над H1, капсом. Например: ИМПЛАНТАЦИЯ ЗУБОВ В КАЗАНИ.', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_service_subtitle', 'label' => 'Подзаголовок', 'name' => 'service_subtitle', 'type' => 'text', 'instructions' => 'Одна строка под H1.', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_service_image', 'label' => 'Изображение услуги', 'name' => 'service_image', 'type' => 'image', 'return_format' => 'id', 'preview_size' => 'medium', 'instructions' => 'Визуал направления в шапке страницы: 4:5, от 800×1000. ' . 'Без лиц пациентов и без клинических снимков — 152-ФЗ. ' . 'Если пусто, шапка выводится в одну колонку, как сейчас.', ), array( 'key' => 'field_dk_service_lead', 'label' => 'Лид', 'name' => 'service_lead', 'type' => 'textarea', 'rows' => 3, 'instructions' => '1–2 предложения: что за услуга и кому она нужна. Без «гарантий» и «лучших» — ФЗ-38 ст. 24.', ), array( 'key' => 'field_dk_service_price_from', 'label' => 'Цена от, ₽', 'name' => 'service_price_from', 'type' => 'number', 'instructions' => 'Только число. Если оставить пустым, шаблон возьмёт минимум из привязанных позиций прайса.', 'min' => 0, 'step' => 100, 'wrapper' => array( 'width' => '33' ), ), array( 'key' => 'field_dk_service_duration', 'label' => 'Сколько занимает', 'name' => 'service_duration', 'type' => 'text', 'instructions' => 'Например: приём 40 минут, лечение 3–6 месяцев.', 'wrapper' => array( 'width' => '33' ), ), array( 'key' => 'field_dk_service_anesthesia', 'label' => 'Обезболивание', 'name' => 'service_anesthesia', 'type' => 'text', 'instructions' => 'Как проводится. Формулировка нейтральная: «под местной анестезией», не «без боли».', 'wrapper' => array( 'width' => '34' ), ), array( 'key' => 'field_dk_service_indications', 'label' => 'Показания', 'name' => 'service_indications', 'type' => 'textarea', 'rows' => 4, 'instructions' => 'По одному пункту на строку.', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_service_contra', 'label' => 'Ограничения', 'name' => 'service_contra', 'type' => 'textarea', 'rows' => 4, 'instructions' => 'По одному пункту на строку. Обязательный блок: закон требует предупреждать о противопоказаниях.', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_service_price_note', 'label' => 'Оговорка к блоку цен', 'name' => 'service_price_note', 'type' => 'textarea', 'rows' => 3, 'instructions' => 'То, что человек обязан узнать здесь, а не у кассы: ' . 'что не входит в цену, куда ехать за снимком, что считается отдельно. ' . 'По одному абзацу на строку. Пусто — блок просто не выводится.', ), array( 'key' => 'field_dk_service_doctors', 'label' => 'Врачи (ручной порядок)', 'name' => 'service_doctors', 'type' => 'relationship', 'post_type' => array( 'dk_doctor' ), 'filters' => array( 'search', 'taxonomy' ), 'return_format' => 'id', 'instructions' => 'Заполнять не обязательно. Пусто — шаблон сам подтянет врачей по направлению.', ), array( 'key' => 'field_dk_service_cta_text', 'label' => 'Текст главной кнопки', 'name' => 'service_cta_text', 'type' => 'text', 'default_value' => 'Записаться на консультацию', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_service_cta_note', 'label' => 'Сноска под кнопкой', 'name' => 'service_cta_note', 'type' => 'text', 'default_value' => 'План лечения составляется после консультации.', 'wrapper' => array( 'width' => '50' ), ), ); if ( $has_pro ) { $service_fields[] = array( 'key' => 'field_dk_service_comparison_image', 'label' => 'Сравнение: изображение', 'name' => 'service_comparison_image', 'type' => 'image', 'return_format' => 'id', 'preview_size' => 'medium', 'instructions' => 'Необязательное поясняющее изображение. Использовать только материал с подтверждёнными правами; узнаваемые пациенты — только при подтверждённом согласии.', ); $service_fields[] = array( 'key' => 'field_dk_service_comparison_heading', 'label' => 'Сравнение: заголовок', 'name' => 'service_comparison_heading', 'type' => 'text', 'instructions' => 'Нейтральный заголовок без превосходных степеней и обещаний результата.', 'wrapper' => array( 'width' => '50' ), ); $service_fields[] = array( 'key' => 'field_dk_service_comparison_lead', 'label' => 'Сравнение: вводный текст', 'name' => 'service_comparison_lead', 'type' => 'textarea', 'rows' => 3, 'instructions' => 'Коротко объясняет критерии сравнения и подчёркивает, что выбор зависит от клинической задачи.', 'wrapper' => array( 'width' => '50' ), ); $service_fields[] = array( 'key' => 'field_dk_service_comparison_items', 'label' => 'Сравнение: варианты', 'name' => 'service_comparison_items', 'type' => 'repeater', 'layout' => 'block', 'button_label' => 'Добавить вариант', 'sub_fields' => array( array( 'key' => 'field_dk_service_comparison_item_title', 'label' => 'Название', 'name' => 'comparison_title', 'type' => 'text', 'wrapper' => array( 'width' => '35' ), ), array( 'key' => 'field_dk_service_comparison_item_text', 'label' => 'Пояснение', 'name' => 'comparison_text', 'type' => 'textarea', 'rows' => 2, 'wrapper' => array( 'width' => '65' ), ), ), ); $service_fields[] = array( 'key' => 'field_dk_service_price_cards', 'label' => 'Плашки цен (три штуки)', 'name' => 'service_price_cards', 'type' => 'repeater', 'layout' => 'table', 'max' => 3, 'button_label' => 'Добавить плашку', 'instructions' => 'Какие три позиции показать крупно над прайсом. Порядок — как здесь. ' . 'Автоподбор «минимум / середина / максимум» этого не заменяет: он не знает, ' . 'что важно пациенту, и однажды поставил синус-лифтинг как «чаще всего выбирают». ' . 'Пусто — сработает автоподбор.', 'sub_fields' => array( array( 'key' => 'field_dk_service_card_price', 'label' => 'Позиция прайса', 'name' => 'card_price', 'type' => 'post_object', 'post_type' => array( 'dk_price' ), 'return_format' => 'id', 'ui' => 1, 'allow_null' => 0, ), array( 'key' => 'field_dk_service_card_badge', 'label' => 'Ярлык', 'name' => 'card_badge', 'type' => 'text', 'instructions' => 'Например «Чаще всего выбирают». Ставится вручную и только там, где это правда.', ), ), ); $service_fields[] = array( 'key' => 'field_dk_service_price_tiers', 'label' => 'Вилка цены в шапке', 'name' => 'service_price_tiers', 'type' => 'repeater', 'layout' => 'table', 'max' => 3, 'button_label' => 'Добавить уровень', 'instructions' => 'Два-три типичных случая с разной ценой: например «один зуб» и ' . '«челюсть на четырёх имплантах». Одна цифра в шапке вводит в заблуждение там, ' . 'где случаи различаются в разы. Пусто — шапка покажет одну цену «от», как раньше. ' . 'Автоподбора здесь нет и быть не может: какие случаи типичны, знает клиника.', 'sub_fields' => array( array( 'key' => 'field_dk_service_tier_label', 'label' => 'Случай', 'name' => 'tier_label', 'type' => 'text', 'wrapper' => array( 'width' => '25' ), ), array( 'key' => 'field_dk_service_tier_value', 'label' => 'Цена, ₽', 'name' => 'tier_value', 'type' => 'number', 'min' => 0, 'step' => 100, 'wrapper' => array( 'width' => '15' ), ), array( 'key' => 'field_dk_service_tier_is_from', 'label' => '«от»', 'name' => 'tier_is_from', 'type' => 'true_false', 'ui' => 1, 'wrapper' => array( 'width' => '10' ), ), array( 'key' => 'field_dk_service_tier_unit', 'label' => 'За что', 'name' => 'tier_unit', 'type' => 'select', 'choices' => array( '' => '—', 'usluga' => 'за услугу', 'zub' => 'за зуб', 'chelyust' => 'за челюсть', '2chelyusti' => 'за две челюсти', 'kurs' => 'за курс', ), 'wrapper' => array( 'width' => '20' ), ), array( 'key' => 'field_dk_service_tier_note', 'label' => 'Что в эту цену входит', 'name' => 'tier_note', 'type' => 'text', 'instructions' => 'Коротко и по факту прейскуранта.', 'wrapper' => array( 'width' => '30' ), ), ), ); $service_fields[] = array( 'key' => 'field_dk_service_steps', 'label' => 'Этапы лечения', 'name' => 'service_steps', 'type' => 'repeater', 'layout' => 'block', 'button_label' => 'Добавить этап', 'sub_fields' => array( array( 'key' => 'field_dk_service_step_title', 'label' => 'Этап', 'name' => 'step_title', 'type' => 'text', 'wrapper' => array( 'width' => '30' ), ), array( 'key' => 'field_dk_service_step_term', 'label' => 'Сколько занимает', 'name' => 'step_term', 'type' => 'text', 'instructions' => 'Например «1 приём», «от 3 месяцев». Человек хочет знать ' . 'не что будет, а когда закончится. Не знаете точно — оставьте пустым: ' . 'срок лечения это медицинский факт, придуманный он хуже отсутствующего.', 'wrapper' => array( 'width' => '20' ), ), array( 'key' => 'field_dk_service_step_text', 'label' => 'Что происходит', 'name' => 'step_text', 'type' => 'textarea', 'rows' => 2, 'wrapper' => array( 'width' => '50' ), ), ), ); $service_fields[] = array( 'key' => 'field_dk_service_faq', 'label' => 'Вопросы и ответы', 'name' => 'service_faq', 'type' => 'repeater', 'layout' => 'block', 'button_label' => 'Добавить вопрос', 'instructions' => 'Идут в блок «Вопросы-ответы» и в разметку FAQPage.', 'sub_fields' => array( array( 'key' => 'field_dk_service_faq_q', 'label' => 'Вопрос', 'name' => 'faq_q', 'type' => 'text', ), array( 'key' => 'field_dk_service_faq_a', 'label' => 'Ответ', 'name' => 'faq_a', 'type' => 'textarea', 'rows' => 3, ), ), ); } acf_add_local_field_group( array( 'key' => 'group_dk_service', 'title' => 'Услуга — содержание страницы', 'location' => delkam_acf_location( 'dk_service' ), 'position' => 'normal', 'style' => 'default', 'fields' => $service_fields, ) ); // ------------------------------------------------------------------ // 2. Врач // ------------------------------------------------------------------ acf_add_local_field_group( array( 'key' => 'group_dk_doctor', 'title' => 'Врач — карточка', 'location' => delkam_acf_location( 'dk_doctor' ), 'fields' => array( array( 'key' => 'field_dk_doctor_position', 'label' => 'Должность', 'name' => 'doctor_position', 'type' => 'text', 'instructions' => 'Как пишем на сайте: «Врач стоматолог-ортодонт».', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_doctor_degree', 'label' => 'Категория, степень', 'name' => 'doctor_degree', 'type' => 'text', 'instructions' => 'Если есть. Например: к. м. н., высшая категория.', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_doctor_experience_since', 'label' => 'Практикует с (год)', 'name' => 'doctor_experience_since', 'type' => 'number', 'instructions' => 'Год, а не стаж. Стаж шаблон посчитает сам — иначе через год цифра на сайте врёт.', 'min' => 1950, 'max' => 2100, 'wrapper' => array( 'width' => '25' ), ), array( 'key' => 'field_dk_doctor_price_consult', 'label' => 'Консультация, ₽', 'name' => 'doctor_price_consult', 'type' => 'number', 'min' => 0, 'step' => 50, 'wrapper' => array( 'width' => '25' ), ), array( 'key' => 'field_dk_doctor_rating', 'label' => 'Рейтинг', 'name' => 'doctor_rating', 'type' => 'number', 'instructions' => 'Средний по площадкам, от 0 до 5.', 'min' => 0, 'max' => 5, 'step' => 0.1, 'wrapper' => array( 'width' => '25' ), ), array( 'key' => 'field_dk_doctor_reviews_count', 'label' => 'Отзывов, шт.', 'name' => 'doctor_reviews_count', 'type' => 'number', 'instructions' => 'Пусто — шаблон посчитает по привязанным отзывам.', 'min' => 0, 'wrapper' => array( 'width' => '25' ), ), array( 'key' => 'field_dk_doctor_education', 'label' => 'Образование и повышение квалификации', 'name' => 'doctor_education', 'type' => 'textarea', 'rows' => 5, 'instructions' => 'По одному пункту на строку: год — учреждение — программа.', ), array( 'key' => 'field_dk_doctor_focus', 'label' => 'На чём специализируется', 'name' => 'doctor_focus', 'type' => 'textarea', 'rows' => 4, 'instructions' => 'По одному пункту на строку. Это то, с чем врач работает чаще всего, а не полный список умений.', ), array( 'key' => 'field_dk_doctor_takes_kids', 'label' => 'Принимает детей', 'name' => 'doctor_takes_kids', 'type' => 'true_false', 'ui' => 1, 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_doctor_is_lead', 'label' => 'Ведущий по направлению', 'name' => 'doctor_is_lead', 'type' => 'true_false', 'ui' => 1, 'instructions' => 'Такие врачи идут первыми в блоке на странице услуги.', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_doctor_prodoctorov_url', 'label' => 'Профиль на ПроДокторов', 'name' => 'doctor_prodoctorov_url', 'type' => 'url', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_doctor_photo_legacy', 'label' => 'Внешнее фото (временно)', 'name' => 'doctor_photo_legacy', 'type' => 'url', 'instructions' => 'Ссылка на Tilda CDN на время миграции. Этап 4.3 — перенести файл в «Медиафайлы» и очистить это поле: зависимость от чужого CDN в продакшене недопустима.', ), ), ) ); // ------------------------------------------------------------------ // 3. Отзыв // ------------------------------------------------------------------ acf_add_local_field_group( array( 'key' => 'group_dk_review', 'title' => 'Отзыв — источник и связи', 'location' => delkam_acf_location( 'dk_review' ), 'fields' => array( array( 'key' => 'field_dk_review_author', 'label' => 'Автор', 'name' => 'review_author', 'type' => 'text', 'instructions' => 'Как подписан на площадке. Только имя. Фамилию, телефон, диагноз не переносим — 152-ФЗ.', 'wrapper' => array( 'width' => '40' ), ), array( 'key' => 'field_dk_review_rating', 'label' => 'Оценка', 'name' => 'review_rating', 'type' => 'number', 'min' => 1, 'max' => 5, 'step' => 1, 'wrapper' => array( 'width' => '20' ), ), array( 'key' => 'field_dk_review_date', 'label' => 'Дата отзыва', 'name' => 'review_date', 'type' => 'date_picker', 'display_format' => 'd.m.Y', 'return_format' => 'Ymd', 'first_day' => 1, 'wrapper' => array( 'width' => '20' ), ), array( 'key' => 'field_dk_review_is_negative', 'label' => 'Негативный', 'name' => 'review_is_negative', 'type' => 'true_false', 'ui' => 1, 'instructions' => 'Служебная метка. Такие на сайт не выводятся.', 'wrapper' => array( 'width' => '20' ), ), array( 'key' => 'field_dk_review_url', 'label' => 'Ссылка на отзыв', 'name' => 'review_url', 'type' => 'url', 'instructions' => 'Первоисточник на площадке. Нужен для проверяемости.', 'wrapper' => array( 'width' => '60' ), ), array( 'key' => 'field_dk_review_external_id', 'label' => 'ID на площадке', 'name' => 'review_external_id', 'type' => 'text', 'instructions' => 'Чтобы повторный импорт не наплодил дублей.', 'wrapper' => array( 'width' => '40' ), ), array( 'key' => 'field_dk_review_doctor', 'label' => 'О каком враче', 'name' => 'review_doctor', 'type' => 'post_object', 'post_type' => array( 'dk_doctor' ), 'return_format' => 'id', 'ui' => 1, 'allow_null' => 1, 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_review_service', 'label' => 'О какой услуге', 'name' => 'review_service', 'type' => 'post_object', 'post_type' => array( 'dk_service' ), 'return_format' => 'id', 'ui' => 1, 'allow_null' => 1, 'instructions' => 'Не обязательно: связь по направлению обычно достаточна.', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_review_response', 'label' => 'Ответ клиники', 'name' => 'review_response', 'type' => 'textarea', 'rows' => 3, ), ), ) ); // ------------------------------------------------------------------ // 4. Позиция прайса // ------------------------------------------------------------------ acf_add_local_field_group( array( 'key' => 'group_dk_price', 'title' => 'Цена', 'location' => delkam_acf_location( 'dk_price' ), 'fields' => array( array( 'key' => 'field_dk_price_value', 'label' => 'Цена, ₽', 'name' => 'price_value', 'type' => 'number', 'required' => 1, 'instructions' => 'Только число, без пробелов и знака рубля. Разделители расставит шаблон.', 'min' => 0, 'step' => 10, 'wrapper' => array( 'width' => '25' ), ), array( 'key' => 'field_dk_price_is_from', 'label' => 'Показывать «от»', 'name' => 'price_is_from', 'type' => 'true_false', 'ui' => 1, 'wrapper' => array( 'width' => '25' ), ), array( 'key' => 'field_dk_price_unit', 'label' => 'За что', 'name' => 'price_unit', 'type' => 'select', 'choices' => array( 'usluga' => 'за услугу', 'zub' => 'за зуб', 'chelyust' => 'за челюсть', '2chelyusti' => 'за две челюсти', 'kurs' => 'за курс', 'mesyac' => 'в месяц', 'priem' => 'за приём', ), 'default_value' => 'usluga', 'instructions' => 'Критично для P21, Clear21 и All-on-4/6 — они считаются за одну челюсть.', 'wrapper' => array( 'width' => '25' ), ), array( 'key' => 'field_dk_price_old', 'label' => 'Старая цена, ₽', 'name' => 'price_old', 'type' => 'number', 'instructions' => 'Только если она действительно была. Зачёркнутая выдуманная цена — нарушение.', 'min' => 0, 'wrapper' => array( 'width' => '25' ), ), array( 'key' => 'field_dk_price_is_main', 'label' => 'Основная позиция', 'name' => 'price_is_main', 'type' => 'true_false', 'ui' => 1, 'instructions' => 'На страницы услуг выводятся только основные позиции — так решено владельцем. Полный прейскурант живёт отдельной страницей.', 'wrapper' => array( 'width' => '30' ), ), array( 'key' => 'field_dk_price_code', 'label' => 'Код по прейскуранту', 'name' => 'price_code', 'type' => 'text', 'instructions' => 'Из файла «Прейскурант (2026.07.08).xlsx». Нужен, чтобы сверять цены при обновлении.', 'wrapper' => array( 'width' => '30' ), ), array( 'key' => 'field_dk_price_service', 'label' => 'К какой услуге', 'name' => 'price_service', 'type' => 'post_object', 'post_type' => array( 'dk_service' ), 'return_format' => 'id', 'ui' => 1, 'allow_null' => 1, 'wrapper' => array( 'width' => '40' ), ), array( 'key' => 'field_dk_price_note', 'label' => 'Что входит / примечание', 'name' => 'price_note', 'type' => 'textarea', 'rows' => 2, 'instructions' => 'Например: работа врача, 4 импланта, анестезия.', ), ), ) ); // ------------------------------------------------------------------ // 5. Акция // ------------------------------------------------------------------ acf_add_local_field_group( array( 'key' => 'group_dk_promo', 'title' => 'Акция — условия и срок', 'location' => delkam_acf_location( 'dk_promo' ), 'fields' => array( array( 'key' => 'field_dk_promo_price_new', 'label' => 'Цена по акции, ₽', 'name' => 'promo_price_new', 'type' => 'number', 'min' => 0, 'wrapper' => array( 'width' => '25' ), ), array( 'key' => 'field_dk_promo_price_old', 'label' => 'Цена без акции, ₽', 'name' => 'promo_price_old', 'type' => 'number', 'instructions' => 'Должна совпадать с действующим прейскурантом.', 'min' => 0, 'wrapper' => array( 'width' => '25' ), ), array( 'key' => 'field_dk_promo_date_start', 'label' => 'Действует с', 'name' => 'promo_date_start', 'type' => 'date_picker', 'display_format' => 'd.m.Y', 'return_format' => 'Ymd', 'first_day' => 1, 'wrapper' => array( 'width' => '25' ), ), array( 'key' => 'field_dk_promo_date_end', 'label' => 'Действует по', 'name' => 'promo_date_end', 'type' => 'date_picker', 'display_format' => 'd.m.Y', 'return_format' => 'Ymd', 'first_day' => 1, 'instructions' => 'После этой даты акция перестаёт выводиться сама. Срок обязателен по закону о рекламе.', 'wrapper' => array( 'width' => '25' ), ), array( 'key' => 'field_dk_promo_service', 'label' => 'К какой услуге', 'name' => 'promo_service', 'type' => 'post_object', 'post_type' => array( 'dk_service' ), 'return_format' => 'id', 'ui' => 1, 'allow_null' => 1, 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_promo_cta_url', 'label' => 'Куда ведёт кнопка', 'name' => 'promo_cta_url', 'type' => 'url', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_promo_conditions', 'label' => 'Условия', 'name' => 'promo_conditions', 'type' => 'textarea', 'rows' => 3, 'instructions' => 'Что входит и чего не входит. Без условий акцию публиковать нельзя.', ), ), ) ); // ------------------------------------------------------------------ // 6. Кейс «до и после» // ------------------------------------------------------------------ acf_add_local_field_group( array( 'key' => 'group_dk_case', 'title' => 'Кейс — до и после', 'location' => delkam_acf_location( 'dk_case' ), 'fields' => array( array( 'key' => 'field_dk_case_before', 'label' => 'Фото «до»', 'name' => 'case_before', 'type' => 'image', 'return_format' => 'id', 'preview_size' => 'medium', 'instructions' => 'Без лица и без любых данных, по которым пациента можно узнать. 152-ФЗ.', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_case_after', 'label' => 'Фото «после»', 'name' => 'case_after', 'type' => 'image', 'return_format' => 'id', 'preview_size' => 'medium', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_case_doctor', 'label' => 'Врачи', 'name' => 'case_doctor', 'type' => 'post_object', 'post_type' => array( 'dk_doctor' ), 'return_format' => 'id', 'ui' => 1, 'allow_null' => 1, // Лечение ведут в четыре руки, и кейс бывает общим. // От числа врачей зависит и видимость кейса: он уходит // в черновик, только когда НИ ОДИН из его врачей не // работает — см. inc/case-visibility.php. 'multiple' => 1, 'instructions' => 'Можно указать нескольких. Если все указанные врачи переведены в черновик, кейс скроется с сайта автоматически и вернётся, когда кто-то из них снова выйдет на работу.', 'wrapper' => array( 'width' => '33' ), ), array( 'key' => 'field_dk_case_service', 'label' => 'Услуга', 'name' => 'case_service', 'type' => 'post_object', 'post_type' => array( 'dk_service' ), 'return_format' => 'id', 'ui' => 1, 'allow_null' => 1, 'wrapper' => array( 'width' => '33' ), ), array( 'key' => 'field_dk_case_duration', 'label' => 'Сколько заняло', 'name' => 'case_duration', 'type' => 'text', 'instructions' => 'Например: 8 месяцев.', 'wrapper' => array( 'width' => '34' ), ), array( 'key' => 'field_dk_case_task', 'label' => 'Задача', 'name' => 'case_task', 'type' => 'textarea', 'rows' => 3, 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_case_result', 'label' => 'Результат', 'name' => 'case_result', 'type' => 'textarea', 'rows' => 3, 'instructions' => 'Без обещаний такого же результата другим пациентам — результат индивидуален.', 'wrapper' => array( 'width' => '50' ), ), ), ) ); // ------------------------------------------------------------------ // 7. Главная — тексты блоков // ------------------------------------------------------------------ /* * Почему поля лежат на самой записи «Главная» (location: page_type == * front_page), а не на странице настроек, как остальные общие поля выше: * это редакторский текст конкретных секций одной конкретной страницы, * а не сквозная настройка сайта (адрес, телефон и т.п. остаются * в group_dk_settings). Три поля — `front_advantages`, `front_why_photo`, * `front_faq` — переехали сюда из «Настроек Дель Кам»: шаблоны * template-parts/front/advantages.php и front/faq.php ждали их там, * но поля никогда не были заведены, и оба блока молча работали на * зашитых в PHP текстах (единственный источник, который блок нашёл). * Дальше в шаблонах — та же смена адреса чтения, get_field(…, 'option') * на get_field(…, get_the_ID()). * * Поля-заголовки не заводятся там, где текст либо целиком считается * (числа `front/facts.php`, `front/directions.php`), либо юридически * значим (дисклеймеры ФЗ-38/152-ФЗ по всем блокам, точность цен) — * решение владельца 04.08.2026: такой текст правится только в коде. */ acf_add_local_field_group( array( 'key' => 'group_dk_front', 'title' => 'Главная — тексты блоков', 'location' => array( array( array( 'param' => 'page_type', 'operator' => '==', 'value' => 'front_page', ), ), ), 'fields' => array_merge( array( array( 'key' => 'field_dk_front_acc_hero', 'label' => 'Первый экран', 'name' => '', 'type' => 'accordion', 'open' => 1, 'multi_expand' => 1, ), array( 'key' => 'field_dk_front_hero_eyebrow', 'label' => 'Надпись над заголовком', 'name' => 'front_hero_eyebrow', 'type' => 'text', 'placeholder' => 'Стоматология в Казани', 'wrapper' => array( 'width' => '33' ), ), array( 'key' => 'field_dk_front_hero_title', 'label' => 'Заголовок H1', 'name' => 'front_hero_title', 'type' => 'text', 'placeholder' => 'Стоматологическая клиника «Дель Кам» в Казани', 'instructions' => 'Единственный H1 на странице.', 'wrapper' => array( 'width' => '34' ), ), array( 'key' => 'field_dk_front_hero_button', 'label' => 'Текст кнопки записи', 'name' => 'front_hero_button', 'type' => 'text', 'placeholder' => 'Записаться на приём', 'wrapper' => array( 'width' => '33' ), ), array( 'key' => 'field_dk_front_acc_hero_end', 'label' => '', 'name' => '', 'type' => 'accordion', 'endpoint' => 1, ), array( 'key' => 'field_dk_front_acc_facts', 'label' => 'Факты, а не лозунги', 'name' => '', 'type' => 'accordion', 'multi_expand' => 1, ), array( 'key' => 'field_dk_front_facts_eyebrow', 'label' => 'Надпись над заголовком', 'name' => 'front_facts_eyebrow', 'type' => 'text', 'placeholder' => 'Коротко о клинике', 'wrapper' => array( 'width' => '33' ), ), array( 'key' => 'field_dk_front_facts_h2', 'label' => 'Заголовок', 'name' => 'front_facts_h2', 'type' => 'text', 'placeholder' => 'Факты, а не лозунги', 'wrapper' => array( 'width' => '33' ), ), array( 'key' => 'field_dk_front_facts_lead', 'label' => 'Подзаголовок', 'name' => 'front_facts_lead', 'type' => 'textarea', 'rows' => 2, 'placeholder' => 'Каждое число — из базы сайта или по понятной формуле, а не для красоты. Нажмите, чтобы увидеть источник.', 'instructions' => 'Сами плашки с числами не редактируются здесь — каждая считается запросом к базе.', 'wrapper' => array( 'width' => '34' ), ), array( 'key' => 'field_dk_front_acc_facts_end', 'label' => '', 'name' => '', 'type' => 'accordion', 'endpoint' => 1, ), array( 'key' => 'field_dk_front_acc_directions', 'label' => 'С чем можно прийти', 'name' => '', 'type' => 'accordion', 'multi_expand' => 1, ), array( 'key' => 'field_dk_front_directions_eyebrow', 'label' => 'Надпись над заголовком', 'name' => 'front_directions_eyebrow', 'type' => 'text', 'placeholder' => 'Направления', 'wrapper' => array( 'width' => '20' ), ), array( 'key' => 'field_dk_front_directions_h2', 'label' => 'Заголовок', 'name' => 'front_directions_h2', 'type' => 'text', 'placeholder' => 'С чем можно прийти', 'wrapper' => array( 'width' => '20' ), ), array( 'key' => 'field_dk_front_directions_sub_tail', 'label' => 'Хвост подзаголовка', 'name' => 'front_directions_sub_tail', 'type' => 'text', 'placeholder' => '— приём по предварительной записи', 'instructions' => 'Числа врачей и направлений перед этим текстом считаются сами.', 'wrapper' => array( 'width' => '20' ), ), array( 'key' => 'field_dk_front_directions_button', 'label' => 'Кнопка записи', 'name' => 'front_directions_button', 'type' => 'text', 'placeholder' => 'Записаться на консультацию', 'wrapper' => array( 'width' => '20' ), ), array( 'key' => 'field_dk_front_directions_button_secondary', 'label' => 'Вторая кнопка', 'name' => 'front_directions_button_secondary', 'type' => 'text', 'placeholder' => 'Все услуги и направления', 'wrapper' => array( 'width' => '20' ), ), array( 'key' => 'field_dk_front_acc_directions_end', 'label' => '', 'name' => '', 'type' => 'accordion', 'endpoint' => 1, ), array( 'key' => 'field_dk_front_acc_prices', 'label' => 'Стоимость известна до начала лечения', 'name' => '', 'type' => 'accordion', 'multi_expand' => 1, ), array( 'key' => 'field_dk_front_prices_eyebrow', 'label' => 'Надпись над заголовком', 'name' => 'front_prices_eyebrow', 'type' => 'text', 'placeholder' => 'Цены', 'wrapper' => array( 'width' => '25' ), ), array( 'key' => 'field_dk_front_prices_h2', 'label' => 'Заголовок', 'name' => 'front_prices_h2', 'type' => 'text', 'placeholder' => 'Стоимость известна до начала лечения', 'wrapper' => array( 'width' => '25' ), ), array( 'key' => 'field_dk_front_prices_button_file', 'label' => 'Кнопка «файл прейскуранта»', 'name' => 'front_prices_button_file', 'type' => 'text', 'placeholder' => 'Скачать полный прейскурант', 'wrapper' => array( 'width' => '25' ), ), array( 'key' => 'field_dk_front_prices_button_all', 'label' => 'Кнопка «все цены»', 'name' => 'front_prices_button_all', 'type' => 'text', 'placeholder' => 'Все цены на сайте', 'wrapper' => array( 'width' => '25' ), ), array( 'key' => 'field_dk_front_acc_prices_end', 'label' => '', 'name' => '', 'type' => 'accordion', 'endpoint' => 1, ), array( 'key' => 'field_dk_front_acc_promos', 'label' => 'Акции', 'name' => '', 'type' => 'accordion', 'multi_expand' => 1, ), array( 'key' => 'field_dk_front_promos_eyebrow', 'label' => 'Надпись над заголовком', 'name' => 'front_promos_eyebrow', 'type' => 'text', 'placeholder' => 'Предложения', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_front_promos_h2', 'label' => 'Заголовок', 'name' => 'front_promos_h2', 'type' => 'text', 'placeholder' => 'Акции', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_front_acc_promos_end', 'label' => '', 'name' => '', 'type' => 'accordion', 'endpoint' => 1, ), array( 'key' => 'field_dk_front_acc_doctors', 'label' => 'Кто будет вас лечить', 'name' => '', 'type' => 'accordion', 'multi_expand' => 1, ), array( 'key' => 'field_dk_front_doctors_eyebrow', 'label' => 'Надпись над заголовком', 'name' => 'front_doctors_eyebrow', 'type' => 'text', 'placeholder' => 'Врачи клиники', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_front_doctors_h2', 'label' => 'Заголовок', 'name' => 'front_doctors_h2', 'type' => 'text', 'placeholder' => 'Кто будет вас лечить', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_front_acc_doctors_end', 'label' => '', 'name' => '', 'type' => 'accordion', 'endpoint' => 1, ), array( 'key' => 'field_dk_front_acc_cases', 'label' => 'До и после', 'name' => '', 'type' => 'accordion', 'multi_expand' => 1, ), array( 'key' => 'field_dk_front_cases_eyebrow', 'label' => 'Надпись над заголовком', 'name' => 'front_cases_eyebrow', 'type' => 'text', 'placeholder' => 'Работы клиники', 'wrapper' => array( 'width' => '34' ), ), array( 'key' => 'field_dk_front_cases_h2', 'label' => 'Заголовок', 'name' => 'front_cases_h2', 'type' => 'text', 'placeholder' => 'До и после', 'wrapper' => array( 'width' => '33' ), ), array( 'key' => 'field_dk_front_cases_button', 'label' => 'Кнопка «все работы»', 'name' => 'front_cases_button', 'type' => 'text', 'placeholder' => 'Все работы', 'wrapper' => array( 'width' => '33' ), ), array( 'key' => 'field_dk_front_acc_cases_end', 'label' => '', 'name' => '', 'type' => 'accordion', 'endpoint' => 1, ), array( 'key' => 'field_dk_front_acc_reviews', 'label' => 'Отзывы пациентов', 'name' => '', 'type' => 'accordion', 'multi_expand' => 1, ), array( 'key' => 'field_dk_front_reviews_eyebrow', 'label' => 'Надпись над заголовком', 'name' => 'front_reviews_eyebrow', 'type' => 'text', 'placeholder' => 'Отзывы', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_front_reviews_button', 'label' => 'Кнопка ленты отзывов', 'name' => 'front_reviews_button', 'type' => 'text', 'placeholder' => 'Читать отзывы', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_front_acc_reviews_end', 'label' => '', 'name' => '', 'type' => 'accordion', 'endpoint' => 1, ), array( 'key' => 'field_dk_front_acc_advantages', 'label' => 'Как устроена работа клиники', 'name' => '', 'type' => 'accordion', 'multi_expand' => 1, ), array( 'key' => 'field_dk_front_advantages_eyebrow', 'label' => 'Надпись над заголовком', 'name' => 'front_advantages_eyebrow', 'type' => 'text', 'placeholder' => 'О клинике', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_front_advantages_h2', 'label' => 'Заголовок', 'name' => 'front_advantages_h2', 'type' => 'text', 'placeholder' => 'Как устроена работа клиники', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_front_why_photo', 'label' => 'Живое фото клиники', 'name' => 'front_why_photo', 'type' => 'image', 'return_format' => 'id', 'preview_size' => 'medium', 'instructions' => 'Пусто — плашки с фото просто не будет, блок останется таким же, как сейчас.', ), ), ( $has_pro ? array( array( 'key' => 'field_dk_front_advantages', 'label' => 'Пункты преимуществ', 'name' => 'front_advantages', 'type' => 'repeater', 'layout' => 'block', 'button_label' => 'Добавить пункт', 'instructions' => 'Пусто — блок покажет четыре пункта по умолчанию, зашитых в шаблон (правки нужно утверждать отдельно — там формулировки, уже проверенные по стоп-листу рекламы медуслуг). Заполните хотя бы один пункт здесь, чтобы взять управление текстом на себя.', 'sub_fields' => array( array( 'key' => 'field_dk_front_advantages_icon', 'label' => 'Иконка', 'name' => 'adv_icon', 'type' => 'select', 'choices' => array( 'medal' => 'Медаль (экспертиза)', 'doctor' => 'Врач', 'tech' => 'Оборудование и диагностика', 'team' => 'Команда', 'globe' => 'Глобус (международное)', ), 'wrapper' => array( 'width' => '25' ), ), array( 'key' => 'field_dk_front_advantages_title', 'label' => 'Заголовок пункта', 'name' => 'adv_title', 'type' => 'text', 'wrapper' => array( 'width' => '35' ), ), array( 'key' => 'field_dk_front_advantages_text', 'label' => 'Текст пункта', 'name' => 'adv_text', 'type' => 'textarea', 'rows' => 3, 'wrapper' => array( 'width' => '40' ), ), ), ), ) : array() ), array( array( 'key' => 'field_dk_front_acc_advantages_end', 'label' => '', 'name' => '', 'type' => 'accordion', 'endpoint' => 1, ), array( 'key' => 'field_dk_front_acc_faq', 'label' => 'Что чаще всего спрашивают', 'name' => '', 'type' => 'accordion', 'multi_expand' => 1, ), array( 'key' => 'field_dk_front_faq_eyebrow', 'label' => 'Надпись над заголовком', 'name' => 'front_faq_eyebrow', 'type' => 'text', 'placeholder' => 'Вопросы', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_front_faq_h2', 'label' => 'Заголовок', 'name' => 'front_faq_h2', 'type' => 'text', 'placeholder' => 'Что чаще всего спрашивают', 'wrapper' => array( 'width' => '50' ), ), ), ( $has_pro ? array( array( 'key' => 'field_dk_front_faq', 'label' => 'Общеклинические вопросы', 'name' => 'front_faq', 'type' => 'repeater', 'layout' => 'block', 'button_label' => 'Добавить вопрос', 'instructions' => 'Показываются первыми, перед вопросами со страниц услуг. Пусто — блок берёт вопросы только со страниц услуг, как сейчас.', 'sub_fields' => array( array( 'key' => 'field_dk_front_faq_q', 'label' => 'Вопрос', 'name' => 'faq_q', 'type' => 'text', ), array( 'key' => 'field_dk_front_faq_a', 'label' => 'Ответ', 'name' => 'faq_a', 'type' => 'textarea', 'rows' => 3, ), ), ), ) : array() ), array( array( 'key' => 'field_dk_front_acc_faq_end', 'label' => '', 'name' => '', 'type' => 'accordion', 'endpoint' => 1, ), array( 'key' => 'field_dk_front_acc_contacts', 'label' => 'Дорога до клиники', 'name' => '', 'type' => 'accordion', 'multi_expand' => 1, ), array( 'key' => 'field_dk_front_contacts_eyebrow', 'label' => 'Надпись над заголовком', 'name' => 'front_contacts_eyebrow', 'type' => 'text', 'placeholder' => 'Как добраться', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_front_contacts_h2', 'label' => 'Заголовок', 'name' => 'front_contacts_h2', 'type' => 'text', 'placeholder' => 'Дорога до клиники', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_front_acc_contacts_end', 'label' => '', 'name' => '', 'type' => 'accordion', 'endpoint' => 1, ), array( 'key' => 'field_dk_front_acc_cta', 'label' => 'Финальный призыв', 'name' => '', 'type' => 'accordion', 'multi_expand' => 1, ), array( 'key' => 'field_dk_front_cta_eyebrow', 'label' => 'Надпись над заголовком', 'name' => 'front_cta_eyebrow', 'type' => 'text', 'placeholder' => 'Запись', 'wrapper' => array( 'width' => '25' ), ), array( 'key' => 'field_dk_front_cta_h2', 'label' => 'Заголовок', 'name' => 'front_cta_h2', 'type' => 'text', 'placeholder' => 'Остались вопросы?', 'wrapper' => array( 'width' => '25' ), ), array( 'key' => 'field_dk_front_cta_sub', 'label' => 'Подзаголовок', 'name' => 'front_cta_sub', 'type' => 'text', 'placeholder' => 'Администратор перезвонит, подберёт время и ответит до записи', 'wrapper' => array( 'width' => '25' ), ), array( 'key' => 'field_dk_front_cta_button', 'label' => 'Текст кнопки', 'name' => 'front_cta_button', 'type' => 'text', 'placeholder' => 'Записаться на консультацию', 'wrapper' => array( 'width' => '25' ), ), array( 'key' => 'field_dk_front_acc_cta_end', 'label' => '', 'name' => '', 'type' => 'accordion', 'endpoint' => 1, ), ) ), ) ); // ------------------------------------------------------------------ // О клинике (страница 310). Числа в шапке (год основания, число // врачей, число направлений) по-прежнему считаются в page-o-klinike.php // и полем не становятся — см. докблок страницы про «врут молча». // Редактируется вокруг них: эйбрау, необязательный хвост-предложение, // заголовок блока «Что мы о себе рассказываем» и подборка фото // с ручным порядком (раньше — только через WP-CLI). // ------------------------------------------------------------------ acf_add_local_field_group( array( 'key' => 'group_dk_about', 'title' => 'О клинике — тексты и фото', 'location' => array( array( array( 'param' => 'page', 'operator' => '==', 'value' => '310', ), ), ), 'fields' => array( array( 'key' => 'field_dk_about_acc_hero', 'label' => 'Шапка страницы', 'name' => '', 'type' => 'accordion', 'open' => 1, 'multi_expand' => 1, ), array( 'key' => 'field_dk_about_hero_eyebrow', 'label' => 'Надпись над заголовком', 'name' => 'about_hero_eyebrow', 'type' => 'text', 'placeholder' => 'О клинике', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_about_hero_tail', 'label' => 'Доп. предложение после подзаголовка', 'name' => 'about_hero_tail', 'type' => 'text', 'instructions' => 'Год основания, число врачей и направлений в строке выше считаются ' . 'из базы и здесь не редактируются — вписанные вручную числа разойдутся ' . 'с реальностью при первом же увольнении или найме. Это поле необязательное: ' . 'добавляет ещё одно предложение отдельной строкой после них.', 'wrapper' => array( 'width' => '50' ), ), array( 'key' => 'field_dk_about_acc_hero_end', 'label' => '', 'name' => '', 'type' => 'accordion', 'endpoint' => 1, ), array( 'key' => 'field_dk_about_acc_story', 'label' => 'Что мы о себе рассказываем', 'name' => '', 'type' => 'accordion', 'multi_expand' => 1, ), array( 'key' => 'field_dk_about_story_h2', 'label' => 'Заголовок блока', 'name' => 'about_story_h2', 'type' => 'text', 'placeholder' => 'Что мы о себе рассказываем', 'instructions' => 'Абзацы под заголовком редактируются не здесь, а обычным содержимым ' . 'страницы (кнопка «Редактировать» → тело записи).', ), array( 'key' => 'field_dk_about_acc_story_end', 'label' => '', 'name' => '', 'type' => 'accordion', 'endpoint' => 1, ), array( 'key' => 'field_dk_about_acc_photos', 'label' => 'Фотографии клиники', 'name' => '', 'type' => 'accordion', 'multi_expand' => 1, ), array( 'key' => 'field_dk_about_photos', 'label' => 'Подборка фото', 'name' => 'about_photos', 'type' => 'gallery', 'return_format' => 'id', 'preview_size' => 'medium', 'insert' => 'append', 'instructions' => 'Порядок на странице — тот же, что здесь: перетащите миниатюры, ' . 'чтобы поменять местами. Пусто — блок с фото на странице не показывается.', ), array( 'key' => 'field_dk_about_acc_photos_end', 'label' => '', 'name' => '', 'type' => 'accordion', 'endpoint' => 1, ), ), ) ); } ); /** * ACF хранит number в meta строкой и строкой же отдаёт. Из-за этого «19900» < «5000» * при любом сравнении в PHP, а вывод цены зависит от того, кто её достал. * Ровно эта беда и была в старой модели: цена текстом, сортировать нельзя. * Приводим к числу один раз здесь, чтобы в шаблонах не приводить нигде. */ add_filter( 'acf/format_value/type=number', function ( $value ) { if ( '' === $value || null === $value ) { return null; } $number = (float) $value; // Целое отдаём int, дробное — float. Рейтинг 4.8 обязан остаться 4.8. return floor( $number ) === $number ? (int) $number : $number; }, 20 ); /** * Если ACF пропал — сущности останутся, а поля исчезнут молча. * Пусть админ это видит. */ add_action( 'admin_notices', function () { if ( function_exists( 'acf_add_local_field_group' ) || ! current_user_can( 'manage_options' ) ) { return; } echo '
Дель Кам: плагин ACF не активен. ' . 'Типы записей работают, но все поля врачей, услуг, цен и отзывов не отображаются и не сохраняются.