fix(booking): remove Medflex and keep appointments on site
This commit is contained in:
parent
30cdeb8dbc
commit
6e30d4cb5f
@ -9,7 +9,7 @@ import (
|
|||||||
"dana/internal/storage"
|
"dana/internal/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
var bookingProviders = map[string]bool{"none": true, "native": true, "medflex": true}
|
var bookingProviders = map[string]bool{"none": true, "native": true}
|
||||||
var bioKinds = map[string]bool{
|
var bioKinds = map[string]bool{
|
||||||
"education": true, "experience": true, "training": true,
|
"education": true, "experience": true, "training": true,
|
||||||
"award": true, "conference": true,
|
"award": true, "conference": true,
|
||||||
@ -54,12 +54,6 @@ func (a *API) adminUpdateDoctor(w http.ResponseWriter, r *http.Request, u *stora
|
|||||||
fail(w, http.StatusUnprocessableEntity, "validation", "Рейтинг должен быть от 1,0 до 5,0")
|
fail(w, http.StatusUnprocessableEntity, "validation", "Рейтинг должен быть от 1,0 до 5,0")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Medflex без ссылки работать не будет — не даём сохранить заведомо мёртвую кнопку.
|
|
||||||
if in.Booking == "medflex" && strings.TrimSpace(in.MedflexUser) == "" {
|
|
||||||
fail(w, http.StatusUnprocessableEntity, "validation",
|
|
||||||
"Для записи через Medflex нужна ссылка")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
oldSlug, err := a.store.PublishedDoctorSlug(r.Context(), id)
|
oldSlug, err := a.store.PublishedDoctorSlug(r.Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleErr(w, err)
|
handleErr(w, err)
|
||||||
|
|||||||
@ -93,7 +93,7 @@ const DoctorDash = {
|
|||||||
['Основные сведения', ({ d }) => !!(d.last_name && d.first_name && d.position && d.specialties)],
|
['Основные сведения', ({ d }) => !!(d.last_name && d.first_name && d.position && d.specialties)],
|
||||||
['Фото', ({ d }) => !!d.photo],
|
['Фото', ({ d }) => !!d.photo],
|
||||||
['Стаж', ({ d }) => d.practice_start_year != null],
|
['Стаж', ({ d }) => d.practice_start_year != null],
|
||||||
['Онлайн-запись', ({ d }) => d.booking_provider === 'medflex'],
|
['Онлайн-запись', ({ d }) => d.booking_provider === 'native'],
|
||||||
['Категория', ({ d }) => !!d.category],
|
['Категория', ({ d }) => !!d.category],
|
||||||
['«О враче»', ({ d }) => !!d.bio],
|
['«О враче»', ({ d }) => !!d.bio],
|
||||||
],
|
],
|
||||||
@ -172,7 +172,7 @@ const DoctorDash = {
|
|||||||
['Образование', cnt(({ q }) => q.education > 0), (v) => `заполнено у ${v}`],
|
['Образование', cnt(({ q }) => q.education > 0), (v) => `заполнено у ${v}`],
|
||||||
['Награды', cnt(({ q }) => q.awards > 0), (v) => `заполнено у ${v}`],
|
['Награды', cnt(({ q }) => q.awards > 0), (v) => `заполнено у ${v}`],
|
||||||
['Конференции', cnt(({ q }) => q.conferences > 0), (v) => `заполнено у ${v}`],
|
['Конференции', cnt(({ q }) => q.conferences > 0), (v) => `заполнено у ${v}`],
|
||||||
['Онлайн-запись Medflex', cnt(({ d }) => d.booking_provider === 'medflex'), (v) => `подключена у ${v}`, 'no-medflex'],
|
['Онлайн-запись 1С', cnt(({ d }) => d.booking_provider === 'native'), (v) => `подключена у ${v}`, 'no-booking'],
|
||||||
['Услуги врача', cnt(({ d }) => d.service_ids.length > 0), (v) => v ? `указаны у ${v}` : 'не указаны', 'no-services'],
|
['Услуги врача', cnt(({ d }) => d.service_ids.length > 0), (v) => v ? `указаны у ${v}` : 'не указаны', 'no-services'],
|
||||||
['Филиалы приёма', cnt(({ d }) => d.branch_ids.length > 0), (v) => v ? `указаны у ${v}` : 'не заполнены', 'no-branches'],
|
['Филиалы приёма', cnt(({ d }) => d.branch_ids.length > 0), (v) => v ? `указаны у ${v}` : 'не заполнены', 'no-branches'],
|
||||||
['Отзывы', withReviews, () => (pending + live
|
['Отзывы', withReviews, () => (pending + live
|
||||||
@ -208,7 +208,7 @@ const DoctorDash = {
|
|||||||
'Онлайн-запись', 'Услуг', 'Филиалов', 'Отзывов на проверке', 'Отзывов опубликовано', 'Заявок']];
|
'Онлайн-запись', 'Услуг', 'Филиалов', 'Отзывов на проверке', 'Отзывов опубликовано', 'Заявок']];
|
||||||
for (const { d, q } of DoctorDash.rows()) {
|
for (const { d, q } of DoctorDash.rows()) {
|
||||||
rows.push([fullName(d), d.specialties, yes(d.photo), d.practice_start_year ? new Date().getFullYear() - d.practice_start_year + ' лет' : 'нет',
|
rows.push([fullName(d), d.specialties, yes(d.photo), d.practice_start_year ? new Date().getFullYear() - d.practice_start_year + ' лет' : 'нет',
|
||||||
d.category || 'нет', yes(d.bio), q.education || 0, q.awards || 0, q.conferences || 0, d.booking_provider === 'medflex' ? 'Medflex' : 'заявка',
|
d.category || 'нет', yes(d.bio), q.education || 0, q.awards || 0, q.conferences || 0, d.booking_provider === 'native' ? '1С' : 'заявка',
|
||||||
d.service_ids.length, d.branch_ids.length, q.reviews_pending || 0, q.reviews_published || 0, q.leads || 0]);
|
d.service_ids.length, d.branch_ids.length, q.reviews_pending || 0, q.reviews_published || 0, q.leads || 0]);
|
||||||
}
|
}
|
||||||
const esc = (v) => { const x = String(v ?? ''); return /[";\n]/.test(x) ? '"' + x.replace(/"/g, '""') + '"' : x; };
|
const esc = (v) => { const x = String(v ?? ''); return /[";\n]/.test(x) ? '"' + x.replace(/"/g, '""') + '"' : x; };
|
||||||
@ -224,7 +224,7 @@ $('#dd-spec').onchange = () => DoctorDash.render();
|
|||||||
window.addEventListener('resize', debounce(() => { if (DoctorDash.q && !$('#tab-doctors-dash').hidden) DoctorDash.chart(DoctorDash.rows()); }, 150));
|
window.addEventListener('resize', debounce(() => { if (DoctorDash.q && !$('#tab-doctors-dash').hidden) DoctorDash.chart(DoctorDash.rows()); }, 150));
|
||||||
{
|
{
|
||||||
const acts = [['users', 'Все врачи', () => DoctorDash.toList('')], ['check', 'Проверить карточки', () => DoctorDash.toList('incomplete')],
|
const acts = [['users', 'Все врачи', () => DoctorDash.toList('')], ['check', 'Проверить карточки', () => DoctorDash.toList('incomplete')],
|
||||||
['chat', 'Отзывы', () => go('reviews')], ['calendar', 'Онлайн-запись', () => DoctorDash.toList('no-medflex')],
|
['chat', 'Отзывы', () => go('reviews')], ['calendar', 'Онлайн-запись', () => DoctorDash.toList('no-booking')],
|
||||||
['download', 'Экспорт отчёта', () => DoctorDash.exportCSV()]];
|
['download', 'Экспорт отчёта', () => DoctorDash.exportCSV()]];
|
||||||
const bar = $('#dd-actions');
|
const bar = $('#dd-actions');
|
||||||
for (const [icon, label, fn] of acts) {
|
for (const [icon, label, fn] of acts) {
|
||||||
|
|||||||
@ -44,7 +44,7 @@ const Doctors = {
|
|||||||
(f !== 'no-services' || !d.service_ids.length) && (f !== 'no-branches' || !d.branch_ids.length) &&
|
(f !== 'no-services' || !d.service_ids.length) && (f !== 'no-branches' || !d.branch_ids.length) &&
|
||||||
(f !== 'no-photo' || !d.photo) &&
|
(f !== 'no-photo' || !d.photo) &&
|
||||||
(f !== 'no-experience' || d.practice_start_year == null) && (f !== 'no-category' || !d.category) &&
|
(f !== 'no-experience' || d.practice_start_year == null) && (f !== 'no-category' || !d.category) &&
|
||||||
(f !== 'no-bio' || !d.bio) && (f !== 'no-medflex' || d.booking_provider !== 'medflex') &&
|
(f !== 'no-bio' || !d.bio) && (f !== 'no-booking' || d.booking_provider !== 'native') &&
|
||||||
(f !== 'incomplete' || !d.photo || d.practice_start_year == null || (!d.bio && !d.bio_count)) &&
|
(f !== 'incomplete' || !d.photo || d.practice_start_year == null || (!d.bio && !d.bio_count)) &&
|
||||||
(!f.startsWith('sp:') || String(d.specialty_id) === f.slice(3)));
|
(!f.startsWith('sp:') || String(d.specialty_id) === f.slice(3)));
|
||||||
const { key, dir } = Doctors.sort;
|
const { key, dir } = Doctors.sort;
|
||||||
@ -150,7 +150,7 @@ const Doctors = {
|
|||||||
const body = {
|
const body = {
|
||||||
slug: d.slug, last_name: d.last_name, first_name: d.first_name, middle_name: d.middle_name,
|
slug: d.slug, last_name: d.last_name, first_name: d.first_name, middle_name: d.middle_name,
|
||||||
position: d.position, category: d.category, degree: d.degree, practice_start_year: d.practice_start_year, rating: d.rating,
|
position: d.position, category: d.category, degree: d.degree, practice_start_year: d.practice_start_year, rating: d.rating,
|
||||||
photo_id: d.photo_id, quote: d.quote, bio: d.bio, booking_provider: d.booking_provider, medflex_user: d.medflex_user,
|
photo_id: d.photo_id, quote: d.quote, bio: d.bio, booking_provider: d.booking_provider,
|
||||||
sort: d.sort, published: d.published, absent: d.absent, show_on_home: d.show_on_home,
|
sort: d.sort, published: d.published, absent: d.absent, show_on_home: d.show_on_home,
|
||||||
seo_title: d.seo_title, seo_description: d.seo_description, seo_h1: d.seo_h1, ...change,
|
seo_title: d.seo_title, seo_description: d.seo_description, seo_h1: d.seo_h1, ...change,
|
||||||
};
|
};
|
||||||
@ -192,7 +192,7 @@ const Doctors = {
|
|||||||
const base = [['', 'Все врачи'], ['published', 'На сайте'], ['hidden', 'Скрытые'], ['absent', 'Не принимают'],
|
const base = [['', 'Все врачи'], ['published', 'На сайте'], ['hidden', 'Скрытые'], ['absent', 'Не принимают'],
|
||||||
['no-services', 'Без услуг'], ['no-branches', 'Без филиала'], ['no-photo', 'Без фото'],
|
['no-services', 'Без услуг'], ['no-branches', 'Без филиала'], ['no-photo', 'Без фото'],
|
||||||
['incomplete', 'Неполные карточки'], ['no-experience', 'Без стажа'], ['no-category', 'Без категории'],
|
['incomplete', 'Неполные карточки'], ['no-experience', 'Без стажа'], ['no-category', 'Без категории'],
|
||||||
['no-bio', 'Без «О враче»'], ['no-medflex', 'Без онлайн-записи']];
|
['no-bio', 'Без «О враче»'], ['no-booking', 'Без онлайн-записи 1С']];
|
||||||
for (const [v, t] of base) sel.append(el('option', { value: v, textContent: t }));
|
for (const [v, t] of base) sel.append(el('option', { value: v, textContent: t }));
|
||||||
const grp = el('optgroup', { label: 'Специальность' });
|
const grp = el('optgroup', { label: 'Специальность' });
|
||||||
for (const s of specialtiesCache.filter((s) => s.doctors)) grp.append(el('option', { value: 'sp:' + s.id, textContent: `${s.name_plural} (${s.doctors})` }));
|
for (const s of specialtiesCache.filter((s) => s.doctors)) grp.append(el('option', { value: 'sp:' + s.id, textContent: `${s.name_plural} (${s.doctors})` }));
|
||||||
|
|||||||
@ -1717,8 +1717,7 @@ function editDoctor(d) {
|
|||||||
{ key: 'bio', label: 'О враче — абзацы через пустую строку, пункты списка начинайте с «•»', value: d.bio, type: 'textarea', rows: 8 },
|
{ key: 'bio', label: 'О враче — абзацы через пустую строку, пункты списка начинайте с «•»', value: d.bio, type: 'textarea', rows: 8 },
|
||||||
{ key: 'quote', label: 'Цитата врача (необязательно)', value: d.quote, type: 'textarea', rows: 2 },
|
{ key: 'quote', label: 'Цитата врача (необязательно)', value: d.quote, type: 'textarea', rows: 2 },
|
||||||
{ key: 'booking_provider', label: 'Запись на приём', value: d.booking_provider || 'none', type: 'select',
|
{ key: 'booking_provider', label: 'Запись на приём', value: d.booking_provider || 'none', type: 'select',
|
||||||
options: [{ value: 'none', label: 'Заявка на сайте (перезвонит администратор)' }, { value: 'medflex', label: 'Онлайн-запись Medflex' }, { value: 'native', label: 'Своя запись' }] },
|
options: [{ value: 'none', label: 'Заявка на сайте (перезвонит администратор)' }, { value: 'native', label: 'Онлайн-запись через 1С' }] },
|
||||||
{ key: 'medflex_user', label: 'Ссылка Medflex (для онлайн-записи)', value: d.medflex_user },
|
|
||||||
{ key: 'absent', label: 'Временно не принимает (кнопка записи скрывается)', value: d.absent, type: 'checkbox' },
|
{ key: 'absent', label: 'Временно не принимает (кнопка записи скрывается)', value: d.absent, type: 'checkbox' },
|
||||||
{ key: 'show_on_home', label: 'Показывать на главной', value: d.show_on_home, type: 'checkbox' },
|
{ key: 'show_on_home', label: 'Показывать на главной', value: d.show_on_home, type: 'checkbox' },
|
||||||
{ key: 'sort', label: 'Порядок (меньше — выше в списке)', value: d.sort ?? 100, type: 'number' },
|
{ key: 'sort', label: 'Порядок (меньше — выше в списке)', value: d.sort ?? 100, type: 'number' },
|
||||||
|
|||||||
@ -165,7 +165,7 @@ const Onec = {
|
|||||||
if (!r.doctor_id) {
|
if (!r.doctor_id) {
|
||||||
way.append(ocEl('span', { className: 'muted', textContent: 'сначала привяжите карточку' }));
|
way.append(ocEl('span', { className: 'muted', textContent: 'сначала привяжите карточку' }));
|
||||||
} else {
|
} else {
|
||||||
const label = { native: '1С', medflex: 'Medflex', none: 'заявка', '': 'заявка' }[r.booking] || r.booking;
|
const label = { native: '1С', none: 'заявка', '': 'заявка' }[r.booking] || 'заявка';
|
||||||
way.append(ocEl('span', { className: 'pill ' + (bookingNative ? '' : 'grey'), textContent: label }));
|
way.append(ocEl('span', { className: 'pill ' + (bookingNative ? '' : 'grey'), textContent: label }));
|
||||||
const b = ocEl('button', { type: 'button', className: 'btn ghost oc-small', textContent: bookingNative ? 'Отключить' : 'Включить запись через 1С' });
|
const b = ocEl('button', { type: 'button', className: 'btn ghost oc-small', textContent: bookingNative ? 'Отключить' : 'Включить запись через 1С' });
|
||||||
b.onclick = () => Onec.setBooking(r, !bookingNative);
|
b.onclick = () => Onec.setBooking(r, !bookingNative);
|
||||||
|
|||||||
24
internal/api/booking_providers_test.go
Normal file
24
internal/api/booking_providers_test.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"dana/internal/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDoctorRejectsRemovedExternalBookingProvider(t *testing.T) {
|
||||||
|
app := &API{}
|
||||||
|
r := httptest.NewRequest(http.MethodPut, "/api/v1/admin/doctors/1", strings.NewReader(`{"last_name":"Тест","slug":"test","booking_provider":"medflex","medflex_user":"https://booking.medflex.ru/example"}`))
|
||||||
|
r.SetPathValue("id", "1")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
app.adminUpdateDoctor(w, r, &storage.AdminUser{})
|
||||||
|
if w.Code != http.StatusUnprocessableEntity {
|
||||||
|
t.Fatalf("removed provider accepted: %d %s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
if len(bookingProviders) != 2 || !bookingProviders["native"] || !bookingProviders["none"] {
|
||||||
|
t.Fatal("only on-site appointment methods should remain")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -80,8 +80,7 @@ type DoctorService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Booking struct {
|
type Booking struct {
|
||||||
Provider string `json:"provider"` // none | native | medflex
|
Provider string `json:"provider"` // none | native
|
||||||
URL string `json:"url,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type BioEntry struct {
|
type BioEntry struct {
|
||||||
|
|||||||
@ -22,7 +22,6 @@ type AdminDoctor struct {
|
|||||||
SpecialtyID *int64 `json:"specialty_id"`
|
SpecialtyID *int64 `json:"specialty_id"`
|
||||||
SpecialtySlug string `json:"specialty_slug"`
|
SpecialtySlug string `json:"specialty_slug"`
|
||||||
Booking string `json:"booking_provider"`
|
Booking string `json:"booking_provider"`
|
||||||
MedflexUser string `json:"medflex_user"`
|
|
||||||
Sort int `json:"sort"`
|
Sort int `json:"sort"`
|
||||||
Published bool `json:"published"`
|
Published bool `json:"published"`
|
||||||
Absent bool `json:"absent"`
|
Absent bool `json:"absent"`
|
||||||
@ -48,7 +47,7 @@ func (s *Storage) AdminDoctors(ctx context.Context, q string) ([]AdminDoctor, er
|
|||||||
WHERE ds.doctor_id = d.id
|
WHERE ds.doctor_id = d.id
|
||||||
), ''),
|
), ''),
|
||||||
ps.id, COALESCE(ps.slug, ''),
|
ps.id, COALESCE(ps.slug, ''),
|
||||||
d.booking_provider, d.medflex_user, d.sort, d.published,
|
d.booking_provider, d.sort, d.published,
|
||||||
d.absent, d.show_on_home,
|
d.absent, d.show_on_home,
|
||||||
(SELECT count(*) FROM doctor_bio_entries e WHERE e.doctor_id = d.id),
|
(SELECT count(*) FROM doctor_bio_entries e WHERE e.doctor_id = d.id),
|
||||||
d.seo_title, d.seo_description, d.seo_h1,
|
d.seo_title, d.seo_description, d.seo_h1,
|
||||||
@ -79,11 +78,14 @@ func (s *Storage) AdminDoctors(ctx context.Context, q string) ([]AdminDoctor, er
|
|||||||
&a.Position, &a.Category, &a.Degree, &a.PracticeYear, &a.Rating,
|
&a.Position, &a.Category, &a.Degree, &a.PracticeYear, &a.Rating,
|
||||||
&a.PhotoID, &a.Photo, &a.Quote, &a.Bio, &a.Specialties,
|
&a.PhotoID, &a.Photo, &a.Quote, &a.Bio, &a.Specialties,
|
||||||
&a.SpecialtyID, &a.SpecialtySlug,
|
&a.SpecialtyID, &a.SpecialtySlug,
|
||||||
&a.Booking, &a.MedflexUser, &a.Sort, &a.Published,
|
&a.Booking, &a.Sort, &a.Published,
|
||||||
&a.Absent, &a.ShowOnHome, &a.BioCount,
|
&a.Absent, &a.ShowOnHome, &a.BioCount,
|
||||||
&a.SEOTitle, &a.SEODesc, &a.SEOH1, &a.ServiceIDs, &a.BranchIDs); err != nil {
|
&a.SEOTitle, &a.SEODesc, &a.SEOH1, &a.ServiceIDs, &a.BranchIDs); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if a.Booking != "native" {
|
||||||
|
a.Booking = "none"
|
||||||
|
}
|
||||||
out = append(out, a)
|
out = append(out, a)
|
||||||
}
|
}
|
||||||
return out, rows.Err()
|
return out, rows.Err()
|
||||||
@ -103,7 +105,6 @@ type DoctorUpdate struct {
|
|||||||
Quote string `json:"quote"`
|
Quote string `json:"quote"`
|
||||||
Bio string `json:"bio"`
|
Bio string `json:"bio"`
|
||||||
Booking string `json:"booking_provider"`
|
Booking string `json:"booking_provider"`
|
||||||
MedflexUser string `json:"medflex_user"`
|
|
||||||
Sort int `json:"sort"`
|
Sort int `json:"sort"`
|
||||||
Published bool `json:"published"`
|
Published bool `json:"published"`
|
||||||
Absent bool `json:"absent"`
|
Absent bool `json:"absent"`
|
||||||
@ -123,7 +124,7 @@ func (s *Storage) AdminUpdateDoctor(ctx context.Context, id int64, in DoctorUpda
|
|||||||
WHERE id=$1 AND deleted_at IS NULL`,
|
WHERE id=$1 AND deleted_at IS NULL`,
|
||||||
id, in.Slug, in.LastName, in.FirstName, in.MiddleName,
|
id, in.Slug, in.LastName, in.FirstName, in.MiddleName,
|
||||||
in.Position, in.Category, in.Degree, in.PracticeYear, in.Rating,
|
in.Position, in.Category, in.Degree, in.PracticeYear, in.Rating,
|
||||||
in.Quote, in.Bio, in.Booking, in.MedflexUser,
|
in.Quote, in.Bio, in.Booking, "",
|
||||||
in.Sort, in.Published, in.Absent, in.ShowOnHome,
|
in.Sort, in.Published, in.Absent, in.ShowOnHome,
|
||||||
in.SEOTitle, in.SEODesc, in.SEOH1, in.PhotoID)
|
in.SEOTitle, in.SEODesc, in.SEOH1, in.PhotoID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
74
internal/storage/doctor_booking_cleanup_test.go
Normal file
74
internal/storage/doctor_booking_cleanup_test.go
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDoctorExternalBookingCannotLeakOrReturn(t *testing.T) {
|
||||||
|
dsn := os.Getenv("DANA_AUTONOMY_TEST_DSN")
|
||||||
|
if dsn == "" {
|
||||||
|
t.Skip("requires disposable autonomy database")
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
s, err := New(ctx, dsn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
if err = s.Migrate(ctx); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
slug := fmt.Sprintf("legacy-external-%d", time.Now().UnixNano())
|
||||||
|
var id int64
|
||||||
|
if err = s.Pool().QueryRow(ctx, `INSERT INTO doctors(slug,last_name,first_name,booking_provider,medflex_user) VALUES($1,'Тест','Врач','medflex','https://booking.medflex.ru/legacy') RETURNING id`, slug).Scan(&id); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
d, err := s.DoctorBySlug(ctx, slug)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
public, _ := json.Marshal(d)
|
||||||
|
if d.Booking.Provider != "none" || strings.Contains(string(public), "medflex") {
|
||||||
|
t.Fatalf("legacy link leaked: %s", public)
|
||||||
|
}
|
||||||
|
rows, err := s.AdminDoctors(ctx, slug)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(rows) != 0 {
|
||||||
|
t.Fatal("slug must not match the name filter")
|
||||||
|
}
|
||||||
|
rows, err = s.AdminDoctors(ctx, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
found := false
|
||||||
|
for _, row := range rows {
|
||||||
|
if row.ID == id {
|
||||||
|
found = true
|
||||||
|
admin, _ := json.Marshal(row)
|
||||||
|
if row.Booking != "none" || strings.Contains(string(admin), "medflex") {
|
||||||
|
t.Fatalf("legacy option leaked in admin: %s", admin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatal("doctor missing from admin")
|
||||||
|
}
|
||||||
|
if err = s.AdminUpdateDoctor(ctx, id, DoctorUpdate{Slug: slug, LastName: "Тест", FirstName: "Врач", Booking: "none", Published: true}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var link string
|
||||||
|
if err = s.Pool().QueryRow(ctx, `SELECT medflex_user FROM doctors WHERE id=$1`, id).Scan(&link); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if link != "" {
|
||||||
|
t.Fatal("editing doctor restored an external link")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -16,7 +16,7 @@ import (
|
|||||||
// приоритет, а при пустом значении используется средняя оценка отзывов.
|
// приоритет, а при пустом значении используется средняя оценка отзывов.
|
||||||
const doctorColumns = `d.id, d.slug, d.last_name, d.first_name, d.middle_name, d.position,
|
const doctorColumns = `d.id, d.slug, d.last_name, d.first_name, d.middle_name, d.position,
|
||||||
d.category, d.degree, d.practice_start_year, COALESCE(m.path, ''), d.quote,
|
d.category, d.degree, d.practice_start_year, COALESCE(m.path, ''), d.quote,
|
||||||
d.languages, d.accepts_kids_from, d.booking_provider, d.medflex_user, d.absent,
|
d.languages, d.accepts_kids_from, d.booking_provider, d.absent,
|
||||||
d.seo_title, d.seo_description, d.seo_h1,
|
d.seo_title, d.seo_description, d.seo_h1,
|
||||||
COALESCE(d.rating_override::float8,
|
COALESCE(d.rating_override::float8,
|
||||||
(SELECT round(avg(r.rating)::numeric, 1)::float8 FROM reviews r
|
(SELECT round(avg(r.rating)::numeric, 1)::float8 FROM reviews r
|
||||||
@ -24,15 +24,12 @@ const doctorColumns = `d.id, d.slug, d.last_name, d.first_name, d.middle_name, d
|
|||||||
(SELECT count(*) FROM reviews r
|
(SELECT count(*) FROM reviews r
|
||||||
WHERE r.doctor_id = d.id AND r.status = 'published')`
|
WHERE r.doctor_id = d.id AND r.status = 'published')`
|
||||||
|
|
||||||
const medflexBase = "https://booking.medflex.ru/?user="
|
|
||||||
|
|
||||||
func scanDoctor(row pgx.Row) (domain.Doctor, error) {
|
func scanDoctor(row pgx.Row) (domain.Doctor, error) {
|
||||||
var d domain.Doctor
|
var d domain.Doctor
|
||||||
var startYear *int
|
var startYear *int
|
||||||
var medflexUser string
|
|
||||||
err := row.Scan(&d.ID, &d.Slug, &d.LastName, &d.FirstName, &d.MiddleName, &d.Position,
|
err := row.Scan(&d.ID, &d.Slug, &d.LastName, &d.FirstName, &d.MiddleName, &d.Position,
|
||||||
&d.Category, &d.Degree, &startYear, &d.Photo, &d.Quote,
|
&d.Category, &d.Degree, &startYear, &d.Photo, &d.Quote,
|
||||||
&d.Languages, &d.KidsFrom, &d.Booking.Provider, &medflexUser, &d.Absent,
|
&d.Languages, &d.KidsFrom, &d.Booking.Provider, &d.Absent,
|
||||||
&d.SEO.Title, &d.SEO.Description, &d.SEO.H1,
|
&d.SEO.Title, &d.SEO.Description, &d.SEO.H1,
|
||||||
&d.Rating, &d.ReviewCount)
|
&d.Rating, &d.ReviewCount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -46,14 +43,8 @@ func scanDoctor(row pgx.Row) (domain.Doctor, error) {
|
|||||||
d.Experience = &years
|
d.Experience = &years
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if d.Booking.Provider == "medflex" && medflexUser != "" {
|
if d.Booking.Provider != "native" {
|
||||||
// При переносе со старого сайта в поле попала ссылка целиком;
|
d.Booking.Provider = "none"
|
||||||
// через админку можно ввести и только идентификатор.
|
|
||||||
if strings.HasPrefix(medflexUser, "http://") || strings.HasPrefix(medflexUser, "https://") {
|
|
||||||
d.Booking.URL = medflexUser
|
|
||||||
} else {
|
|
||||||
d.Booking.URL = medflexBase + medflexUser
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,16 +9,9 @@ export function roleLabel(doctor) {
|
|||||||
return doctor.position || doctor.specialties?.[0]?.name || '';
|
return doctor.position || doctor.specialties?.[0]?.name || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Запись к врачу: у кого настроен Medflex — ведём туда, у остальных открываем форму заявки.
|
// Запись всегда остаётся на сайте: окна 1С либо обычная форма заявки.
|
||||||
export function BookButton({ doctor, className = '', children = 'Записаться' }) {
|
export function BookButton({ doctor, className = '', children = 'Записаться' }) {
|
||||||
const { open } = useLeadModal();
|
const { open } = useLeadModal();
|
||||||
if (doctor.booking?.provider === 'medflex' && doctor.booking.url) {
|
|
||||||
return (
|
|
||||||
<a className={className} href={doctor.booking.url} target="_blank" rel="noreferrer">
|
|
||||||
{children}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<button type="button" className={className} onClick={() => open({ form: 'appointment', doctor })}>
|
<button type="button" className={className} onClick={() => open({ form: 'appointment', doctor })}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user