chore: build
This commit is contained in:
@@ -0,0 +1,449 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"gestion/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ── Reset des sections de stats ─────────────────────────────────────────────
|
||||
|
||||
// ResetAdminStat enregistre (ou met à jour) la date de reset pour une section.
|
||||
func (d *Database) ResetAdminStat(section string) error {
|
||||
now := time.Now().UTC().Format(time.RFC3339)
|
||||
upsert := `INSERT INTO app_settings (key, value) VALUES (?, ?)
|
||||
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`
|
||||
return d.GDB.Exec(upsert, section, now).Error
|
||||
}
|
||||
|
||||
// ReadResetAt lit la date de reset stockée pour une clé donnée (zero value si absente).
|
||||
func (d *Database) ReadResetAt(key string) time.Time {
|
||||
var row struct {
|
||||
Value string
|
||||
}
|
||||
err := d.GDB.Table("app_settings").
|
||||
Select("value").
|
||||
Where("key = ?", key).
|
||||
Scan(&row).Error
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
if row.Value != "" {
|
||||
if t, err := time.Parse(time.RFC3339, row.Value); err == nil {
|
||||
return t
|
||||
}
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// ── Construction des clauses WHERE (filtrage par reset) ────────────────────
|
||||
|
||||
// statusFilterClause construit "<baseStatus> [AND created_at >= ?]" et renvoie
|
||||
// la clause ainsi que les arguments à binder, dans l'ordre.
|
||||
func statusFilterClause(baseStatus string, resetAt time.Time) (string, []interface{}) {
|
||||
if !resetAt.IsZero() {
|
||||
return baseStatus + " AND created_at >= ?", []interface{}{resetAt.Format(time.RFC3339)}
|
||||
}
|
||||
return baseStatus, nil
|
||||
}
|
||||
|
||||
// AdminStatsFilters regroupe les dates de reset pour chaque section, lues une
|
||||
// seule fois puis transmises aux différentes requêtes.
|
||||
type AdminStatsFilters struct {
|
||||
ResetCommandes time.Time
|
||||
ResetRevenus time.Time
|
||||
ResetProduits time.Time
|
||||
ResetHeures time.Time
|
||||
ResetJours time.Time
|
||||
ResetDoses time.Time
|
||||
}
|
||||
|
||||
// LoadAdminStatsFilters lit toutes les dates de reset en une seule requête.
|
||||
func (d *Database) LoadAdminStatsFilters() AdminStatsFilters {
|
||||
keys := []string{
|
||||
"stats_reset_commandes_at",
|
||||
"stats_reset_revenus_at",
|
||||
"stats_reset_produits_at",
|
||||
"stats_reset_heures_at",
|
||||
"stats_reset_jours_at",
|
||||
"stats_reset_doses_at",
|
||||
}
|
||||
var rows []struct {
|
||||
Key string `gorm:"column:key"`
|
||||
Value string `gorm:"column:value"`
|
||||
}
|
||||
d.GDB.Table("app_settings").Select("key, value").Where("key IN ?", keys).Scan(&rows)
|
||||
|
||||
m := make(map[string]time.Time, len(keys))
|
||||
for _, r := range rows {
|
||||
if t, err := time.Parse(time.RFC3339, r.Value); err == nil {
|
||||
m[r.Key] = t
|
||||
}
|
||||
}
|
||||
return AdminStatsFilters{
|
||||
ResetCommandes: m["stats_reset_commandes_at"],
|
||||
ResetRevenus: m["stats_reset_revenus_at"],
|
||||
ResetProduits: m["stats_reset_produits_at"],
|
||||
ResetHeures: m["stats_reset_heures_at"],
|
||||
ResetJours: m["stats_reset_jours_at"],
|
||||
ResetDoses: m["stats_reset_doses_at"],
|
||||
}
|
||||
}
|
||||
|
||||
// ── Commandes par jour de la semaine (non annulées) ─────────────────────────
|
||||
|
||||
func (d *Database) OrderPerDaysPerWeeks(wdRows *[]models.WeekdayRow, resetAt time.Time) error {
|
||||
where, args := statusFilterClause("status != 'cancelled'", resetAt)
|
||||
query := `
|
||||
SELECT EXTRACT(DOW FROM created_at)::int AS dow, COUNT(*) AS count
|
||||
FROM commandes
|
||||
WHERE ` + where + `
|
||||
GROUP BY dow
|
||||
ORDER BY dow
|
||||
`
|
||||
return d.GDB.Raw(query, args...).Scan(wdRows).Error
|
||||
}
|
||||
|
||||
// ── Commandes par jour sur 30 jours ──────────────────────────────────────────
|
||||
|
||||
func (d *Database) OrdersByDayLast30(dayRows *[]models.DayRow, resetAt time.Time) error {
|
||||
where, args := statusFilterClause("status != 'cancelled'", resetAt)
|
||||
query := `
|
||||
SELECT DATE(created_at) AS day, COUNT(*) AS count
|
||||
FROM commandes
|
||||
WHERE created_at >= NOW() - INTERVAL '30 days'
|
||||
AND ` + where + `
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY day
|
||||
`
|
||||
return d.GDB.Raw(query, args...).Scan(dayRows).Error
|
||||
}
|
||||
|
||||
// ── Revenus par jour sur 30 jours (commandes approuvées) ─────────────────────
|
||||
|
||||
func (d *Database) RevenueByDayLast30(dayRevRows *[]models.DayRevenueRow, resetAt time.Time) error {
|
||||
where, args := statusFilterClause("status = 'approved'", resetAt)
|
||||
query := `
|
||||
SELECT DATE(created_at) AS day, COALESCE(SUM(total_prix - COALESCE(referral_used, 0)), 0) AS revenue
|
||||
FROM commandes
|
||||
WHERE created_at >= NOW() - INTERVAL '30 days'
|
||||
AND ` + where + `
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY day
|
||||
`
|
||||
return d.GDB.Raw(query, args...).Scan(dayRevRows).Error
|
||||
}
|
||||
|
||||
// ── Commandes par jour sur un mois calendaire complet ────────────────────────
|
||||
|
||||
type DailyMonthStatRow struct {
|
||||
Day time.Time
|
||||
Count int
|
||||
Revenue float64
|
||||
Quantity float64
|
||||
}
|
||||
|
||||
func (d *Database) StatsByDayForMonth(rows *[]DailyMonthStatRow, monthStart time.Time, resetAt time.Time) error {
|
||||
start := time.Date(monthStart.Year(), monthStart.Month(), 1, 0, 0, 0, 0, monthStart.Location())
|
||||
end := start.AddDate(0, 1, 0)
|
||||
|
||||
where, whereArgs := statusFilterClause("status != 'cancelled'", resetAt)
|
||||
|
||||
query := `
|
||||
SELECT
|
||||
d.day,
|
||||
COALESCE(d.count, 0) AS count,
|
||||
COALESCE(rv.revenue, 0) AS revenue,
|
||||
COALESCE(qt.quantity, 0) AS quantity
|
||||
FROM (
|
||||
SELECT DATE(created_at) AS day, COUNT(*) AS count
|
||||
FROM commandes
|
||||
WHERE created_at >= ? AND created_at < ?
|
||||
AND ` + where + `
|
||||
GROUP BY DATE(created_at)
|
||||
) d
|
||||
LEFT JOIN (
|
||||
SELECT DATE(created_at) AS day,
|
||||
COALESCE(SUM(total_prix - COALESCE(referral_used, 0)), 0) AS revenue
|
||||
FROM commandes
|
||||
WHERE created_at >= ? AND created_at < ?
|
||||
AND status = 'approved'
|
||||
GROUP BY DATE(created_at)
|
||||
) rv ON rv.day = d.day
|
||||
LEFT JOIN (
|
||||
SELECT DATE(c.created_at) AS day, SUM(ci.quantite) AS quantity
|
||||
FROM commandes c
|
||||
JOIN command_items ci ON ci.command_id = c.id
|
||||
WHERE c.created_at >= ? AND c.created_at < ?
|
||||
AND c.status != 'cancelled'
|
||||
GROUP BY DATE(c.created_at)
|
||||
) qt ON qt.day = d.day
|
||||
ORDER BY d.day
|
||||
`
|
||||
|
||||
// Ordre des "?" dans la requête : (start, end, [reset]) pour le bloc "d",
|
||||
// puis (start, end) pour "rv", puis (start, end) pour "qt".
|
||||
args := []interface{}{start, end}
|
||||
args = append(args, whereArgs...)
|
||||
args = append(args, start, end)
|
||||
args = append(args, start, end)
|
||||
|
||||
return d.GDB.Raw(query, args...).Scan(rows).Error
|
||||
}
|
||||
|
||||
// OrdersAndRevenueByHour renvoie, par heure, le nombre de commandes non annulées
|
||||
// (volume d'activité) et le revenu confirmé (commandes approuvées uniquement —
|
||||
// cohérent avec TotalRevenue/RevenueByDayLast30, pour ne pas compter comme
|
||||
// "revenu" une commande encore en cours qui pourrait être annulée).
|
||||
func (d *Database) OrdersAndRevenueByHour(hourRows *[]models.HourRow, resetAt time.Time) error {
|
||||
where, args := statusFilterClause("status != 'cancelled'", resetAt)
|
||||
query := `
|
||||
SELECT
|
||||
EXTRACT(HOUR FROM created_at)::int AS hour,
|
||||
COUNT(*) AS count,
|
||||
COALESCE(SUM(CASE WHEN status = 'approved' THEN total_prix - COALESCE(referral_used, 0) ELSE 0 END), 0) AS revenue
|
||||
FROM commandes
|
||||
WHERE ` + where + `
|
||||
GROUP BY hour
|
||||
ORDER BY hour
|
||||
`
|
||||
return d.GDB.Raw(query, args...).Scan(hourRows).Error
|
||||
}
|
||||
|
||||
// ── Top produits (quantité vendue) ───────────────────────────────────────────
|
||||
|
||||
// TopProducts renvoie les produits les plus commandés. La quantité/le nombre de
|
||||
// commandes reflètent l'activité (non annulées), le revenu ne compte que les
|
||||
// commandes approuvées (revenu confirmé, cohérent avec le résumé global).
|
||||
func (d *Database) TopProducts(prodRows *[]models.ProductRow, resetAt time.Time, limit int) error {
|
||||
where, args := statusFilterClause("c.status != 'cancelled'", resetAt)
|
||||
args = append(args, limit)
|
||||
query := `
|
||||
SELECT
|
||||
ci.product_id,
|
||||
ci.produit AS name,
|
||||
SUM(ci.quantite) AS total_quantity,
|
||||
COUNT(DISTINCT ci.command_id) AS order_count,
|
||||
SUM(CASE WHEN c.status = 'approved'
|
||||
THEN ci.prix * (c.total_prix - COALESCE(c.referral_used, 0)) / NULLIF(c.total_prix, 0)
|
||||
ELSE 0 END) AS revenue,
|
||||
COALESCE(p.category, '') AS category,
|
||||
COALESCE(cat.color, '#7c3aed') AS category_color
|
||||
FROM command_items ci
|
||||
JOIN commandes c ON c.id = ci.command_id
|
||||
LEFT JOIN products p ON p.id = ci.product_id
|
||||
LEFT JOIN categories cat ON cat.name = p.category
|
||||
WHERE ` + where + `
|
||||
GROUP BY ci.product_id, ci.produit, p.category, cat.color
|
||||
ORDER BY total_quantity DESC
|
||||
LIMIT ?
|
||||
`
|
||||
return d.GDB.Raw(query, args...).Scan(prodRows).Error
|
||||
}
|
||||
|
||||
// ── Répartition des doses/quantités par produit ──────────────────────────────
|
||||
|
||||
// QuantityBreakdown : quantité/nombre de commandes reflètent l'activité (non
|
||||
// annulées), le revenu ne compte que les commandes approuvées (revenu confirmé).
|
||||
func (d *Database) QuantityBreakdown(qtyRows *[]models.QuantityBreakdownRow, resetAt time.Time) error {
|
||||
where, args := statusFilterClause("c.status != 'cancelled'", resetAt)
|
||||
query := `
|
||||
SELECT
|
||||
ci.product_id,
|
||||
ci.produit AS product_name,
|
||||
ci.quantite AS quantity,
|
||||
COUNT(DISTINCT ci.command_id) AS order_count,
|
||||
SUM(ci.quantite) AS total_sold,
|
||||
SUM(CASE WHEN c.status = 'approved'
|
||||
THEN ci.prix * (c.total_prix - COALESCE(c.referral_used, 0)) / NULLIF(c.total_prix, 0)
|
||||
ELSE 0 END) AS revenue,
|
||||
COALESCE(cat.color, '#7c3aed') AS category_color
|
||||
FROM command_items ci
|
||||
JOIN commandes c ON c.id = ci.command_id
|
||||
LEFT JOIN products p ON p.id = ci.product_id
|
||||
LEFT JOIN categories cat ON cat.name = p.category
|
||||
WHERE ` + where + `
|
||||
GROUP BY ci.product_id, ci.produit, ci.quantite, cat.color
|
||||
ORDER BY ci.product_id, COUNT(DISTINCT ci.command_id) DESC
|
||||
`
|
||||
return d.GDB.Raw(query, args...).Scan(qtyRows).Error
|
||||
}
|
||||
|
||||
// ── Détail du jour (catégorie → produits) ────────────────────────────────────
|
||||
|
||||
// DailyProductDetail : quantité/nombre de commandes reflètent l'activité (non
|
||||
// annulées), le revenu ne compte que les commandes approuvées (revenu confirmé).
|
||||
func (d *Database) DailyProductDetail(dailyRows *[]models.DailyProductRow) error {
|
||||
query := `
|
||||
SELECT
|
||||
ci.product_id,
|
||||
ci.produit AS product_name,
|
||||
COALESCE(p.category, 'Sans catégorie') AS category,
|
||||
COALESCE(cat.color, '#7c3aed') AS category_color,
|
||||
SUM(ci.quantite) AS total_quantity,
|
||||
COUNT(DISTINCT ci.command_id) AS order_count,
|
||||
SUM(CASE WHEN c.status = 'approved'
|
||||
THEN ci.prix * (c.total_prix - COALESCE(c.referral_used, 0)) / NULLIF(c.total_prix, 0)
|
||||
ELSE 0 END) AS revenue
|
||||
FROM command_items ci
|
||||
JOIN commandes c ON c.id = ci.command_id
|
||||
LEFT JOIN products p ON p.id = ci.product_id
|
||||
LEFT JOIN categories cat ON cat.name = p.category
|
||||
WHERE DATE(c.created_at) = CURRENT_DATE
|
||||
AND c.status != 'cancelled'
|
||||
GROUP BY ci.product_id, ci.produit, p.category, cat.color
|
||||
ORDER BY p.category, SUM(ci.quantite) DESC
|
||||
`
|
||||
return d.GDB.Raw(query).Scan(dailyRows).Error
|
||||
}
|
||||
|
||||
func (d *Database) DailyProductDetailForDate(dailyRows *[]models.DailyProductRow, date time.Time) error {
|
||||
start := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
|
||||
end := start.AddDate(0, 0, 1)
|
||||
|
||||
query := `
|
||||
SELECT
|
||||
ci.product_id,
|
||||
ci.produit AS product_name,
|
||||
COALESCE(p.category, 'Sans catégorie') AS category,
|
||||
COALESCE(cat.color, '#7c3aed') AS category_color,
|
||||
SUM(ci.quantite) AS total_quantity,
|
||||
COUNT(DISTINCT ci.command_id) AS order_count,
|
||||
SUM(CASE WHEN c.status = 'approved'
|
||||
THEN ci.prix * (c.total_prix - COALESCE(c.referral_used, 0)) / NULLIF(c.total_prix, 0)
|
||||
ELSE 0 END) AS revenue
|
||||
FROM command_items ci
|
||||
JOIN commandes c ON c.id = ci.command_id
|
||||
LEFT JOIN products p ON p.id = ci.product_id
|
||||
LEFT JOIN categories cat ON cat.name = p.category
|
||||
WHERE c.created_at >= ? AND c.created_at < ?
|
||||
AND c.status != 'cancelled'
|
||||
GROUP BY ci.product_id, ci.produit, p.category, cat.color
|
||||
ORDER BY p.category, SUM(ci.quantite) DESC
|
||||
`
|
||||
return d.GDB.Raw(query, start, end).Scan(dailyRows).Error
|
||||
}
|
||||
|
||||
// DailyOrdersCountForDate renvoie le nombre de commandes (non annulées) pour
|
||||
// une date précise.
|
||||
func (d *Database) DailyOrdersCountForDate(date time.Time) (int64, error) {
|
||||
start := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
|
||||
end := start.AddDate(0, 0, 1)
|
||||
|
||||
var count int64
|
||||
err := d.GDB.Raw(`
|
||||
SELECT COUNT(DISTINCT id) FROM commandes
|
||||
WHERE created_at >= ? AND created_at < ? AND status != 'cancelled'
|
||||
`, start, end).Scan(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// DailyOrdersCount renvoie le nombre de commandes (non annulées) du jour.
|
||||
func (d *Database) DailyOrdersCount() (int64, error) {
|
||||
var count int64
|
||||
err := d.GDB.Raw(`
|
||||
SELECT COUNT(DISTINCT id) FROM commandes
|
||||
WHERE DATE(created_at) = CURRENT_DATE AND status != 'cancelled'
|
||||
`).Scan(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// ── Résumé global ────────────────────────────────────────────────────────────
|
||||
|
||||
// TotalOrders renvoie le nombre total de commandes filtré par le reset "commandes".
|
||||
func (d *Database) TotalOrders(resetAt time.Time) (int64, error) {
|
||||
where, args := statusFilterClause("status != 'cancelled'", resetAt)
|
||||
var total int64
|
||||
err := d.GDB.Raw(`SELECT COUNT(*) FROM commandes WHERE `+where, args...).Scan(&total).Error
|
||||
return total, err
|
||||
}
|
||||
|
||||
// TotalRevenue renvoie le revenu total (commandes approuvées) filtré par le reset "revenus".
|
||||
func (d *Database) TotalRevenue(resetAt time.Time) (float64, error) {
|
||||
where, args := statusFilterClause("status = 'approved'", resetAt)
|
||||
var total float64
|
||||
err := d.GDB.Raw(`SELECT COALESCE(SUM(total_prix - COALESCE(referral_used, 0)), 0) FROM commandes WHERE `+where, args...).
|
||||
Scan(&total).Error
|
||||
return total, err
|
||||
}
|
||||
|
||||
// ActiveDaysLast30 renvoie le nombre de jours distincts ayant eu au moins une commande sur 30 jours.
|
||||
func (d *Database) ActiveDaysLast30(resetAt time.Time) (int64, error) {
|
||||
where, args := statusFilterClause("status != 'cancelled'", resetAt)
|
||||
var activeDays int64
|
||||
query := `
|
||||
SELECT COUNT(DISTINCT DATE(created_at))
|
||||
FROM commandes
|
||||
WHERE created_at >= NOW() - INTERVAL '30 days' AND ` + where
|
||||
err := d.GDB.Raw(query, args...).Scan(&activeDays).Error
|
||||
return activeDays, err
|
||||
}
|
||||
|
||||
// OrdersCountLast30 renvoie le nombre de commandes sur les 30 derniers jours.
|
||||
func (d *Database) OrdersCountLast30(resetAt time.Time) (int64, error) {
|
||||
where, args := statusFilterClause("status != 'cancelled'", resetAt)
|
||||
var count int64
|
||||
query := `
|
||||
SELECT COUNT(*) FROM commandes
|
||||
WHERE created_at >= NOW() - INTERVAL '30 days' AND ` + where
|
||||
err := d.GDB.Raw(query, args...).Scan(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (d *Database) GetMyDeliveryStatsPerDay(statsRows *[]models.DayRowWithResult, username string) error {
|
||||
query := `
|
||||
SELECT DATE(updated_at) AS day,
|
||||
COUNT(*) AS count,
|
||||
COALESCE(SUM(total_prix - COALESCE(referral_used, 0)), 0) AS revenue
|
||||
FROM commandes
|
||||
WHERE livreur_assign = ?
|
||||
AND status IN ('livre', 'approved')
|
||||
AND updated_at >= NOW() - INTERVAL '30 days'
|
||||
GROUP BY DATE(updated_at)
|
||||
ORDER BY day
|
||||
`
|
||||
return d.GDB.Raw(query, username).Scan(statsRows).Error
|
||||
}
|
||||
|
||||
func (d *Database) GetMyDeliveryStatsPerWeek(statsRow *[]models.WeekRow, username string) error {
|
||||
query := `
|
||||
SELECT EXTRACT(WEEK FROM updated_at)::int AS week_num,
|
||||
EXTRACT(YEAR FROM updated_at)::int AS year,
|
||||
COUNT(*) AS count,
|
||||
COALESCE(SUM(total_prix - COALESCE(referral_used, 0)), 0) AS revenue
|
||||
FROM commandes
|
||||
WHERE livreur_assign = ?
|
||||
AND status IN ('livre', 'approved')
|
||||
AND updated_at >= NOW() - INTERVAL '12 weeks'
|
||||
GROUP BY week_num, year
|
||||
ORDER BY year, week_num
|
||||
`
|
||||
return d.GDB.Raw(query, username).Scan(statsRow).Error
|
||||
}
|
||||
|
||||
func (d *Database) GetMyDeliveryStatsPerMonth(statsRow *[]models.MonthRow, username string) error {
|
||||
query := `
|
||||
SELECT EXTRACT(MONTH FROM updated_at)::int AS month_num,
|
||||
EXTRACT(YEAR FROM updated_at)::int AS year,
|
||||
COUNT(*) AS count,
|
||||
COALESCE(SUM(total_prix - COALESCE(referral_used, 0)), 0) AS revenue
|
||||
FROM commandes
|
||||
WHERE livreur_assign = ?
|
||||
AND status IN ('livre', 'approved')
|
||||
AND updated_at >= NOW() - INTERVAL '12 months'
|
||||
GROUP BY month_num, year
|
||||
ORDER BY year, month_num
|
||||
`
|
||||
return d.GDB.Raw(query, username).Scan(statsRow).Error
|
||||
}
|
||||
|
||||
func (d *Database) GetMyDeliveryStatsToday(statsRow *models.TodayRow, username string) error {
|
||||
query := `
|
||||
SELECT COUNT(*) AS count,
|
||||
COALESCE(SUM(total_prix - COALESCE(referral_used, 0)), 0) AS revenue
|
||||
FROM commandes
|
||||
WHERE livreur_assign = ?
|
||||
AND status IN ('livre', 'approved')
|
||||
AND DATE(updated_at) = CURRENT_DATE
|
||||
`
|
||||
return d.GDB.Raw(query, username).Scan(statsRow).Error
|
||||
}
|
||||
Reference in New Issue
Block a user