147 lines
4.3 KiB
Go
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
|
|
}
|