143 lines
5.5 KiB
Go
143 lines
5.5 KiB
Go
package storage
|
||
|
||
import (
|
||
"context"
|
||
"time"
|
||
)
|
||
|
||
// Dashboard — сводка для главного экрана админки. Только счётчики и даты, без персональных данных:
|
||
// заявки с именами и телефонами главная страница берёт отдельным запросом (он пишется в журнал).
|
||
type Dashboard struct {
|
||
Doctors int `json:"doctors"`
|
||
Services int `json:"services"`
|
||
Branches int `json:"branches"`
|
||
NewLeads int `json:"new_leads"`
|
||
LeadsToday int `json:"leads_today"`
|
||
PendingReviews int `json:"pending_reviews"`
|
||
ActivePromos int `json:"active_promos"`
|
||
Days []DashboardDay `json:"days"`
|
||
Upcoming []DashboardDate `json:"upcoming"`
|
||
Todo []DashboardTodo `json:"todo"`
|
||
}
|
||
|
||
// DashboardDay — заявки за день: все и только записи на приём.
|
||
type DashboardDay struct {
|
||
Date string `json:"date"`
|
||
Leads int `json:"leads"`
|
||
Appointments int `json:"appointments"`
|
||
}
|
||
|
||
// DashboardDate — что скоро начнётся или закончится: акции и слайды на главной.
|
||
type DashboardDate struct {
|
||
Kind string `json:"kind"` // promo | banner
|
||
Title string `json:"title"`
|
||
Event string `json:"event"` // starts | ends
|
||
At time.Time `json:"at"`
|
||
}
|
||
|
||
// DashboardTodo — незакрытые дела для колокольчика в шапке.
|
||
type DashboardTodo struct {
|
||
Key string `json:"key"`
|
||
Count int `json:"count"`
|
||
}
|
||
|
||
func (s *Storage) Dashboard(ctx context.Context, days int) (*Dashboard, error) {
|
||
var d Dashboard
|
||
err := s.pool.QueryRow(ctx, `
|
||
SELECT
|
||
(SELECT count(*) FROM doctors WHERE published AND deleted_at IS NULL),
|
||
(SELECT count(*) FROM services WHERE published AND deleted_at IS NULL AND parent_id IS NOT NULL),
|
||
(SELECT count(*) FROM branches WHERE published),
|
||
(SELECT count(*) FROM leads WHERE status = 'new'),
|
||
(SELECT count(*) FROM leads WHERE created_at >= date_trunc('day', now() AT TIME ZONE 'Europe/Moscow') AT TIME ZONE 'Europe/Moscow'),
|
||
(SELECT count(*) FROM reviews WHERE status = 'pending'),
|
||
(SELECT count(*) FROM posts WHERE kind = 'promo' AND published AND deleted_at IS NULL
|
||
AND (starts_at IS NULL OR starts_at <= current_date) AND (ends_at IS NULL OR ends_at >= current_date))`).
|
||
Scan(&d.Doctors, &d.Services, &d.Branches, &d.NewLeads, &d.LeadsToday, &d.PendingReviews, &d.ActivePromos)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
rows, err := s.pool.Query(ctx, `
|
||
WITH days AS (
|
||
SELECT generate_series((now() AT TIME ZONE 'Europe/Moscow')::date - ($1::int - 1), (now() AT TIME ZONE 'Europe/Moscow')::date, '1 day')::date AS day
|
||
)
|
||
SELECT to_char(d.day, 'YYYY-MM-DD'),
|
||
count(l.id),
|
||
count(l.id) FILTER (WHERE f.code = 'appointment')
|
||
FROM days d
|
||
LEFT JOIN leads l ON (l.created_at AT TIME ZONE 'Europe/Moscow')::date = d.day
|
||
LEFT JOIN forms f ON f.id = l.form_id
|
||
GROUP BY d.day ORDER BY d.day`, days)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for rows.Next() {
|
||
var day DashboardDay
|
||
if err := rows.Scan(&day.Date, &day.Leads, &day.Appointments); err != nil {
|
||
rows.Close()
|
||
return nil, err
|
||
}
|
||
d.Days = append(d.Days, day)
|
||
}
|
||
rows.Close()
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
rows, err = s.pool.Query(ctx, `
|
||
SELECT kind, title, event, at FROM (
|
||
SELECT 'promo' AS kind, title, 'starts' AS event, starts_at::timestamptz AS at FROM posts
|
||
WHERE kind = 'promo' AND published AND deleted_at IS NULL AND starts_at > current_date
|
||
UNION ALL
|
||
SELECT 'promo', title, 'ends', ends_at::timestamptz FROM posts
|
||
WHERE kind = 'promo' AND published AND deleted_at IS NULL AND ends_at >= current_date
|
||
UNION ALL
|
||
SELECT 'banner', COALESCE(NULLIF(title, ''), 'Слайд без заголовка'), 'starts', starts_at FROM banners
|
||
WHERE published AND starts_at > now()
|
||
UNION ALL
|
||
SELECT 'banner', COALESCE(NULLIF(title, ''), 'Слайд без заголовка'), 'ends', ends_at FROM banners
|
||
WHERE published AND ends_at > now()
|
||
) x
|
||
WHERE at <= now() + interval '60 days'
|
||
ORDER BY at LIMIT 12`)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for rows.Next() {
|
||
var u DashboardDate
|
||
if err := rows.Scan(&u.Kind, &u.Title, &u.Event, &u.At); err != nil {
|
||
rows.Close()
|
||
return nil, err
|
||
}
|
||
d.Upcoming = append(d.Upcoming, u)
|
||
}
|
||
rows.Close()
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
var noServices, noBranches, noPhoto, hiddenPages int
|
||
if err := s.pool.QueryRow(ctx, `
|
||
SELECT
|
||
(SELECT count(*) FROM doctors d WHERE d.published AND d.deleted_at IS NULL
|
||
AND NOT EXISTS (SELECT 1 FROM doctor_services s WHERE s.doctor_id = d.id)),
|
||
(SELECT count(*) FROM doctors d WHERE d.published AND d.deleted_at IS NULL
|
||
AND NOT EXISTS (SELECT 1 FROM doctor_branches b WHERE b.doctor_id = d.id)),
|
||
(SELECT count(*) FROM doctors WHERE published AND deleted_at IS NULL AND photo_id IS NULL),
|
||
(SELECT count(*) FROM pages WHERE slug = 'patients' AND NOT published)`).
|
||
Scan(&noServices, &noBranches, &noPhoto, &hiddenPages); err != nil {
|
||
return nil, err
|
||
}
|
||
for _, t := range []DashboardTodo{
|
||
{"new_leads", d.NewLeads}, {"pending_reviews", d.PendingReviews},
|
||
{"doctors_without_services", noServices}, {"doctors_without_branches", noBranches},
|
||
{"doctors_without_photo", noPhoto}, {"patients_page_hidden", hiddenPages},
|
||
} {
|
||
if t.Count > 0 {
|
||
d.Todo = append(d.Todo, t)
|
||
}
|
||
}
|
||
return &d, nil
|
||
}
|