dana-medical-site/internal/storage/dashboard.go
2026-10-14 13:33:13 +00:00

143 lines
5.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}