167 lines
6.1 KiB
Go
167 lines
6.1 KiB
Go
package storage
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"time"
|
||
|
||
"github.com/jackc/pgx/v5"
|
||
)
|
||
|
||
// WeeklyChannel — одна строка канала в недельном/месячном дашборде.
|
||
// Budget вводится вручную в админке и сохраняется между перезагрузками того же периода.
|
||
type WeeklyChannel struct {
|
||
Key string `json:"key"`
|
||
Title string `json:"title"`
|
||
Leads int `json:"leads"`
|
||
Patients int `json:"patients"`
|
||
Services float64 `json:"services"`
|
||
PerPat float64 `json:"per_patient"`
|
||
Revenue float64 `json:"revenue"`
|
||
AvgCheck float64 `json:"avg_check"`
|
||
Budget float64 `json:"budget"`
|
||
}
|
||
|
||
// WeeklyCallCenter — блок «Колл-ц» дашборда: весь колл-центр за период, без привязки к каналу.
|
||
type WeeklyCallCenter struct {
|
||
Total int `json:"total"`
|
||
Incoming int `json:"incoming"`
|
||
Outgoing int `json:"outgoing"`
|
||
Missed int `json:"missed"`
|
||
MissedPct float64 `json:"missed_pct"`
|
||
Unique int `json:"unique"` // уникальных позвонивших номеров
|
||
Patients int `json:"patients"`
|
||
Services float64 `json:"services"`
|
||
Revenue float64 `json:"revenue"`
|
||
AvgCheck float64 `json:"avg_check"`
|
||
TalkSec int `json:"talk_seconds"` // средняя длительность разговора по отвеченным, сек
|
||
WaitSec int `json:"wait_seconds"` // среднее ожидание ответа по отвеченным, сек
|
||
}
|
||
|
||
type WeeklyReport struct {
|
||
ID int64 `json:"id"`
|
||
PeriodFrom time.Time `json:"period_from"`
|
||
PeriodTo time.Time `json:"period_to"`
|
||
Label string `json:"label"`
|
||
SourceName string `json:"source_name"`
|
||
CallCenter WeeklyCallCenter `json:"callcenter"`
|
||
Channels []WeeklyChannel `json:"channels"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
}
|
||
|
||
// UpsertWeeklyReport сохраняет расчёт за период. Если период уже загружался раньше,
|
||
// значения бюджета по каналам (вводятся вручную) переносятся из прежней записи —
|
||
// повторная загрузка файла 1С их не стирает.
|
||
func (s *Storage) UpsertWeeklyReport(ctx context.Context, clinic string, from, to time.Time, label, sourceName string,
|
||
cc WeeklyCallCenter, channels []WeeklyChannel, adminID int64) (int64, error) {
|
||
var existing []byte
|
||
err := s.pool.QueryRow(ctx,
|
||
`SELECT channels FROM weekly_reports WHERE clinic=$1 AND period_from=$2 AND period_to=$3`, clinic, from, to).Scan(&existing)
|
||
switch {
|
||
case err == nil:
|
||
var old []WeeklyChannel
|
||
if json.Unmarshal(existing, &old) == nil {
|
||
budgets := make(map[string]float64, len(old))
|
||
for _, c := range old {
|
||
budgets[c.Key] = c.Budget
|
||
}
|
||
for i := range channels {
|
||
channels[i].Budget = budgets[channels[i].Key]
|
||
}
|
||
}
|
||
case errors.Is(err, pgx.ErrNoRows):
|
||
// период новый — бюджета ещё нет, переносить нечего
|
||
default:
|
||
return 0, err
|
||
}
|
||
ccJSON, err := json.Marshal(cc)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
chJSON, err := json.Marshal(channels)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
var id int64
|
||
err = s.pool.QueryRow(ctx, `
|
||
INSERT INTO weekly_reports (clinic, period_from, period_to, label, source_name, callcenter, channels, admin_id, updated_at)
|
||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,now())
|
||
ON CONFLICT (clinic, period_from, period_to) DO UPDATE SET
|
||
label=EXCLUDED.label, source_name=EXCLUDED.source_name, callcenter=EXCLUDED.callcenter,
|
||
channels=EXCLUDED.channels, admin_id=EXCLUDED.admin_id, updated_at=now()
|
||
RETURNING id`,
|
||
clinic, from, to, label, sourceName, ccJSON, chJSON, adminID).Scan(&id)
|
||
return id, err
|
||
}
|
||
|
||
func scanWeeklyReport(row pgx.Row) (*WeeklyReport, error) {
|
||
var r WeeklyReport
|
||
var cc, ch []byte
|
||
if err := row.Scan(&r.ID, &r.PeriodFrom, &r.PeriodTo, &r.Label, &r.SourceName, &cc, &ch, &r.CreatedAt, &r.UpdatedAt); err != nil {
|
||
return nil, err
|
||
}
|
||
_ = json.Unmarshal(cc, &r.CallCenter)
|
||
_ = json.Unmarshal(ch, &r.Channels)
|
||
return &r, nil
|
||
}
|
||
|
||
const weeklyCols = `id, period_from, period_to, label, source_name, callcenter, channels, created_at, updated_at`
|
||
|
||
// WeeklyReports — вся история клиники, от старых периодов к новым (для дашборда со столбцами-неделями).
|
||
func (s *Storage) WeeklyReports(ctx context.Context, clinic string) ([]WeeklyReport, error) {
|
||
rows, err := s.pool.Query(ctx, `SELECT `+weeklyCols+` FROM weekly_reports WHERE clinic=$1 ORDER BY period_from ASC, id ASC`, clinic)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
out := []WeeklyReport{}
|
||
for rows.Next() {
|
||
r, err := scanWeeklyReport(rows)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
out = append(out, *r)
|
||
}
|
||
return out, rows.Err()
|
||
}
|
||
|
||
func (s *Storage) WeeklyReport(ctx context.Context, id int64) (*WeeklyReport, error) {
|
||
return scanWeeklyReport(s.pool.QueryRow(ctx, `SELECT `+weeklyCols+` FROM weekly_reports WHERE id=$1`, id))
|
||
}
|
||
|
||
// SetWeeklyBudget — правка бюджета одного канала одного периода руками в админке.
|
||
func (s *Storage) SetWeeklyBudget(ctx context.Context, id int64, channelKey string, amount float64) error {
|
||
var raw []byte
|
||
if err := s.pool.QueryRow(ctx, `SELECT channels FROM weekly_reports WHERE id=$1`, id).Scan(&raw); err != nil {
|
||
return err
|
||
}
|
||
var channels []WeeklyChannel
|
||
if err := json.Unmarshal(raw, &channels); err != nil {
|
||
return err
|
||
}
|
||
found := false
|
||
for i := range channels {
|
||
if channels[i].Key == channelKey {
|
||
channels[i].Budget = amount
|
||
found = true
|
||
break
|
||
}
|
||
}
|
||
if !found {
|
||
return pgx.ErrNoRows
|
||
}
|
||
chJSON, err := json.Marshal(channels)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
_, err = s.pool.Exec(ctx, `UPDATE weekly_reports SET channels=$2, updated_at=now() WHERE id=$1`, id, chJSON)
|
||
return err
|
||
}
|
||
|
||
func (s *Storage) DeleteWeeklyReport(ctx context.Context, id int64) error {
|
||
_, err := s.pool.Exec(ctx, `DELETE FROM weekly_reports WHERE id=$1`, id)
|
||
return err
|
||
}
|