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 }