Files
aura-crm/production/backend/internal/analytics/operations.go
T

147 lines
4.3 KiB
Go

package analytics
import (
"context"
"github.com/gofiber/fiber/v2"
)
type statusCount struct {
Status string `json:"status"`
Count int `json:"count"`
}
type deviceTypeCount struct {
DeviceType string `json:"device_type"`
Count int `json:"count"`
}
type masterWorkload struct {
Master string `json:"master"`
Count int `json:"count"`
}
// Operations reports order counts by status/device type/assigned master
// (all bucketed by order creation date within [from, to)) plus average
// turnaround for orders completed in that same window.
func (h *Handler) Operations(c *fiber.Ctx) error {
from, to := c.Query("from"), c.Query("to")
ctx := context.Background()
byStatus, err := h.fetchOrdersByStatus(ctx, from, to)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": "internal error"})
}
byDevice, err := h.fetchOrdersByDeviceType(ctx, from, to)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": "internal error"})
}
byMaster, err := h.fetchOrdersByMaster(ctx, from, to)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": "internal error"})
}
avgHours, err := h.fetchAvgTurnaroundHours(ctx, from, to)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": "internal error"})
}
return c.JSON(fiber.Map{
"by_status": byStatus,
"by_device_type": byDevice,
"by_master": byMaster,
"avg_turnaround_hours": avgHours,
})
}
func (h *Handler) fetchOrdersByStatus(ctx context.Context, from, to string) ([]statusCount, error) {
rows, err := h.db.Query(ctx,
`SELECT status, COUNT(*) FROM orders
WHERE ($1 = '' OR created_at >= $1::timestamptz)
AND ($2 = '' OR created_at < $2::timestamptz)
GROUP BY status ORDER BY COUNT(*) DESC`, from, to)
if err != nil {
return nil, err
}
defer rows.Close()
out := []statusCount{}
for rows.Next() {
var s statusCount
if err := rows.Scan(&s.Status, &s.Count); err != nil {
return nil, err
}
out = append(out, s)
}
return out, rows.Err()
}
func (h *Handler) fetchOrdersByDeviceType(ctx context.Context, from, to string) ([]deviceTypeCount, error) {
rows, err := h.db.Query(ctx,
`SELECT device_type, COUNT(*) FROM orders
WHERE ($1 = '' OR created_at >= $1::timestamptz)
AND ($2 = '' OR created_at < $2::timestamptz)
GROUP BY device_type ORDER BY COUNT(*) DESC LIMIT 10`, from, to)
if err != nil {
return nil, err
}
defer rows.Close()
out := []deviceTypeCount{}
for rows.Next() {
var d deviceTypeCount
if err := rows.Scan(&d.DeviceType, &d.Count); err != nil {
return nil, err
}
out = append(out, d)
}
return out, rows.Err()
}
func (h *Handler) fetchOrdersByMaster(ctx context.Context, from, to string) ([]masterWorkload, error) {
rows, err := h.db.Query(ctx,
`SELECT COALESCE(assigned_master_name, 'Не назначен') AS master, COUNT(*)
FROM orders
WHERE ($1 = '' OR created_at >= $1::timestamptz)
AND ($2 = '' OR created_at < $2::timestamptz)
GROUP BY master ORDER BY COUNT(*) DESC`, from, to)
if err != nil {
return nil, err
}
defer rows.Close()
out := []masterWorkload{}
for rows.Next() {
var m masterWorkload
if err := rows.Scan(&m.Master, &m.Count); err != nil {
return nil, err
}
out = append(out, m)
}
return out, rows.Err()
}
// fetchAvgTurnaroundHours averages, per completed order, the time between
// creation and the LAST time its status was set to 'completed' (the
// LATERAL subquery's ORDER BY ... DESC LIMIT 1 picks that last event, not
// the first — an order can be marked completed, reopened, and completed
// again, however rare). Returns nil (not zero) when nothing completed in
// range, so JSON consumers can tell "no data" from "an average of exactly
// zero hours".
func (h *Handler) fetchAvgTurnaroundHours(ctx context.Context, from, to string) (*float64, error) {
var avg *float64
err := h.db.QueryRow(ctx,
`SELECT AVG(EXTRACT(EPOCH FROM (e.completed_at - o.created_at)) / 3600.0)
FROM orders o
JOIN LATERAL (
SELECT created_at AS completed_at FROM order_events
WHERE order_id = o.id AND type = 'status_change' AND body = 'completed'
ORDER BY created_at DESC LIMIT 1
) e ON true
WHERE o.status = 'completed'
AND ($1 = '' OR e.completed_at >= $1::timestamptz)
AND ($2 = '' OR e.completed_at < $2::timestamptz)`,
from, to,
).Scan(&avg)
if err != nil {
return nil, err
}
return avg, nil
}