205 lines
7.3 KiB
Go
205 lines
7.3 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"math"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const dataFile = "/data/chu_stats.json"
|
|
|
|
type CHUStats struct {
|
|
GeneratedAt time.Time `json:"generated_at"`
|
|
HospitalName string `json:"hospital_name"`
|
|
Date string `json:"date"`
|
|
BedsTotal int `json:"beds_total"`
|
|
BedsOccupied int `json:"beds_occupied"`
|
|
OccupancyRate float64 `json:"occupancy_rate"`
|
|
PatientsAdmitted int `json:"patients_admitted"`
|
|
PatientsDischarge int `json:"patients_discharged"`
|
|
EmergencyVisits int `json:"emergency_visits"`
|
|
AvgERWaitMinutes int `json:"avg_er_wait_minutes"`
|
|
SurgeriesPerformed int `json:"surgeries_performed"`
|
|
ICUBeds int `json:"icu_beds"`
|
|
ICUOccupied int `json:"icu_occupied"`
|
|
MortalityRate float64 `json:"mortality_rate"`
|
|
InfectionRate float64 `json:"infection_rate"`
|
|
StaffPresent int `json:"staff_present"`
|
|
StaffRequired int `json:"staff_required"`
|
|
PatientSatisfaction float64 `json:"patient_satisfaction"`
|
|
}
|
|
|
|
type Check struct {
|
|
Indicator string `json:"indicator"`
|
|
Value float64 `json:"value"`
|
|
Unit string `json:"unit"`
|
|
Threshold string `json:"threshold"`
|
|
Conformant bool `json:"conformant"`
|
|
Severity string `json:"severity"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
type Analysis struct {
|
|
AnalyzedAt time.Time `json:"analyzed_at"`
|
|
HospitalName string `json:"hospital_name"`
|
|
StatsDate string `json:"stats_date"`
|
|
Conformant bool `json:"conformant"`
|
|
Score int `json:"score"`
|
|
PassingChecks int `json:"passing_checks"`
|
|
TotalChecks int `json:"total_checks"`
|
|
CriticalAlerts int `json:"critical_alerts"`
|
|
WarningAlerts int `json:"warning_alerts"`
|
|
Checks []Check `json:"checks"`
|
|
Summary string `json:"summary"`
|
|
}
|
|
|
|
func round2(v float64) float64 {
|
|
return math.Round(v*100) / 100
|
|
}
|
|
|
|
func analyze(s CHUStats) Analysis {
|
|
var checks []Check
|
|
|
|
occ := Check{Indicator: "Taux d'occupation des lits", Value: s.OccupancyRate, Unit: "%", Threshold: "< 85%"}
|
|
switch {
|
|
case s.OccupancyRate < 85:
|
|
occ.Conformant, occ.Severity, occ.Message = true, "ok", "Taux d'occupation normal"
|
|
case s.OccupancyRate < 95:
|
|
occ.Conformant, occ.Severity, occ.Message = false, "warning", "Taux d'occupation élevé, risque de saturation"
|
|
default:
|
|
occ.Conformant, occ.Severity, occ.Message = false, "critical", "Saturation critique des lits hospitaliers"
|
|
}
|
|
checks = append(checks, occ)
|
|
|
|
er := Check{Indicator: "Temps d'attente aux urgences", Value: float64(s.AvgERWaitMinutes), Unit: "min", Threshold: "< 60 min"}
|
|
switch {
|
|
case s.AvgERWaitMinutes < 60:
|
|
er.Conformant, er.Severity, er.Message = true, "ok", "Temps d'attente aux urgences acceptable"
|
|
case s.AvgERWaitMinutes < 120:
|
|
er.Conformant, er.Severity, er.Message = false, "warning", "Temps d'attente aux urgences trop long"
|
|
default:
|
|
er.Conformant, er.Severity, er.Message = false, "critical", "Saturation critique des urgences"
|
|
}
|
|
checks = append(checks, er)
|
|
|
|
icuRate := round2(float64(s.ICUOccupied) / float64(s.ICUBeds) * 100)
|
|
icu := Check{Indicator: "Taux d'occupation ICU", Value: icuRate, Unit: "%", Threshold: "< 85%"}
|
|
switch {
|
|
case icuRate < 85:
|
|
icu.Conformant, icu.Severity, icu.Message = true, "ok", "Capacité de réanimation suffisante"
|
|
case icuRate < 95:
|
|
icu.Conformant, icu.Severity, icu.Message = false, "warning", "Taux d'occupation ICU élevé"
|
|
default:
|
|
icu.Conformant, icu.Severity, icu.Message = false, "critical", "Réanimation en situation critique"
|
|
}
|
|
checks = append(checks, icu)
|
|
|
|
mort := Check{Indicator: "Taux de mortalité hospitalière", Value: s.MortalityRate, Unit: "%", Threshold: "< 2%"}
|
|
switch {
|
|
case s.MortalityRate < 2.0:
|
|
mort.Conformant, mort.Severity, mort.Message = true, "ok", "Taux de mortalité dans les normes"
|
|
case s.MortalityRate < 4.0:
|
|
mort.Conformant, mort.Severity, mort.Message = false, "warning", "Taux de mortalité supérieur au seuil recommandé"
|
|
default:
|
|
mort.Conformant, mort.Severity, mort.Message = false, "critical", "Taux de mortalité critique — investigation requise"
|
|
}
|
|
checks = append(checks, mort)
|
|
|
|
infect := Check{Indicator: "Taux d'infection nosocomiale", Value: s.InfectionRate, Unit: "%", Threshold: "< 5%"}
|
|
switch {
|
|
case s.InfectionRate < 5.0:
|
|
infect.Conformant, infect.Severity, infect.Message = true, "ok", "Taux d'infection nosocomiale acceptable"
|
|
case s.InfectionRate < 8.0:
|
|
infect.Conformant, infect.Severity, infect.Message = false, "warning", "Taux d'infection nosocomiale préoccupant"
|
|
default:
|
|
infect.Conformant, infect.Severity, infect.Message = false, "critical", "Risque infectieux critique — mesures d'urgence requises"
|
|
}
|
|
checks = append(checks, infect)
|
|
|
|
staffRate := round2(float64(s.StaffPresent) / float64(s.StaffRequired) * 100)
|
|
staff := Check{Indicator: "Taux de couverture du personnel", Value: staffRate, Unit: "%", Threshold: ">= 80%"}
|
|
switch {
|
|
case staffRate >= 80:
|
|
staff.Conformant, staff.Severity, staff.Message = true, "ok", "Personnel suffisant"
|
|
case staffRate >= 70:
|
|
staff.Conformant, staff.Severity, staff.Message = false, "warning", "Manque de personnel, vigilance requise"
|
|
default:
|
|
staff.Conformant, staff.Severity, staff.Message = false, "critical", "Sous-effectif critique"
|
|
}
|
|
checks = append(checks, staff)
|
|
|
|
sat := Check{Indicator: "Satisfaction des patients", Value: s.PatientSatisfaction, Unit: "/ 5", Threshold: ">= 3.5 / 5"}
|
|
switch {
|
|
case s.PatientSatisfaction >= 3.5:
|
|
sat.Conformant, sat.Severity, sat.Message = true, "ok", "Satisfaction des patients satisfaisante"
|
|
case s.PatientSatisfaction >= 3.0:
|
|
sat.Conformant, sat.Severity, sat.Message = false, "warning", "Satisfaction des patients insuffisante"
|
|
default:
|
|
sat.Conformant, sat.Severity, sat.Message = false, "critical", "Satisfaction des patients très mauvaise"
|
|
}
|
|
checks = append(checks, sat)
|
|
|
|
passing, critical, warning := 0, 0, 0
|
|
for _, c := range checks {
|
|
if c.Conformant {
|
|
passing++
|
|
}
|
|
switch c.Severity {
|
|
case "critical":
|
|
critical++
|
|
case "warning":
|
|
warning++
|
|
}
|
|
}
|
|
total := len(checks)
|
|
score := passing * 100 / total
|
|
|
|
return Analysis{
|
|
AnalyzedAt: time.Now().UTC(),
|
|
HospitalName: s.HospitalName,
|
|
StatsDate: s.Date,
|
|
Conformant: passing == total,
|
|
Score: score,
|
|
PassingChecks: passing,
|
|
TotalChecks: total,
|
|
CriticalAlerts: critical,
|
|
WarningAlerts: warning,
|
|
Checks: checks,
|
|
Summary: fmt.Sprintf(
|
|
"%d/%d indicateurs conformes (score: %d%%) — %d alerte(s) critique(s), %d avertissement(s)",
|
|
passing, total, score, critical, warning,
|
|
),
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
for i := range []int{0, 1} {
|
|
data, err := os.ReadFile(strings.ReplaceAll(dataFile, "stats", "stats"+fmt.Sprintf("%v", i)))
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
log.Fatalf("stats file not found at %s — run chu-stats-generator first", dataFile)
|
|
}
|
|
log.Fatalf("failed to read stats file: %v", err)
|
|
}
|
|
|
|
var stats CHUStats
|
|
if err := json.Unmarshal(data, &stats); err != nil {
|
|
log.Fatalf("failed to parse stats file: %v", err)
|
|
}
|
|
|
|
analysis := analyze(stats)
|
|
|
|
result, err := json.MarshalIndent(analysis, "", " ")
|
|
if err != nil {
|
|
log.Fatalf("failed to marshal analysis: %v", err)
|
|
}
|
|
|
|
log.Printf("Analysis complete: %s", analysis.Summary)
|
|
fmt.Println(string(result))
|
|
}
|
|
}
|