heating-monitor/internal/web/utils.go

281 lines
8.2 KiB
Go

package web
import (
"encoding/json"
"heating-monitor/internal/bot"
"html/template"
"log"
"os"
"path/filepath"
"sort"
"strconv"
"time"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
)
// Función para formatear la fecha en el formato necesario para el campo datetime-local
func FormatDate(t time.Time) string {
if t.IsZero() {
return ""
}
return t.Format("2006-01-02T15:04")
}
// Función para convertir un valor a JSON
func ToJson(v interface{}) template.JS {
b, err := json.Marshal(v)
if err != nil {
log.Printf("Error al convertir a JSON: %v", err)
return template.JS("[]")
}
return template.JS(b)
}
// Función para cargar todas las plantillas desde un directorio
func loadTemplatesFromDir(dir string) ([]string, error) {
var templates []string
// Buscar todos los archivos con extensión .html en el directorio y subdirectorios
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && filepath.Ext(path) == ".html" {
templates = append(templates, path)
}
return nil
})
return templates, err
}
// Cargar las plantillas y los partials de los directorios "templates" y "partials"
func LoadTemplate(c echo.Context, templateName string, data interface{}) error {
// Creamos un mapa con las funciones que vamos a pasar al template
funcs := template.FuncMap{
"date": FormatDate, // Asignamos la función FormatDate con el nombre "date"
"json": ToJson, // Agregamos la función ToJson con el nombre "json"
"formatTimestamp": func(t time.Time) string {
location, _ := time.LoadLocation("Europe/Madrid")
localTime := t.In(location)
return localTime.Format("2006-01-02 15:04 (MST)")
},
}
// Cargar las plantillas desde los directorios
templates, err := loadTemplatesFromDir("internal/web/templates")
if err != nil {
log.Printf("Error al cargar las plantillas: %v", err)
return err
}
// Parsear las plantillas con las funciones
tmpl, err := template.New(templateName).Funcs(funcs).ParseFiles(templates...)
if err != nil {
log.Printf("Error al parsear las plantillas: %v", err)
return err
}
// Ejecutamos el template y lo renderizamos
return tmpl.Execute(c.Response(), data)
}
func getPaginationParams(c echo.Context, defaultPageSize int) (int, int) {
page := c.QueryParam("page")
size := c.QueryParam("size")
pageNum, err := strconv.Atoi(page)
if err != nil || pageNum < 1 {
pageNum = 1
}
sizeNum, err := strconv.Atoi(size)
if err != nil || sizeNum < 1 {
sizeNum = defaultPageSize
}
return pageNum, sizeNum
}
func getDateFilter(filter string) (time.Time, time.Time) {
now := time.Now()
var startDate, endDate time.Time
switch filter {
case "today":
startDate = now.Truncate(24 * time.Hour)
endDate = startDate.Add(24 * time.Hour)
case "yesterday":
endDate = now.Truncate(24 * time.Hour)
startDate = endDate.Add(-24 * time.Hour)
case "this-week":
weekday := int(now.Weekday())
startDate = now.AddDate(0, 0, -weekday).Truncate(24 * time.Hour)
endDate = startDate.AddDate(0, 0, 7)
case "this-month":
startDate = time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
endDate = startDate.AddDate(0, 1, 0)
case "this-year":
startDate = time.Date(now.Year(), 1, 1, 0, 0, 0, 0, now.Location())
endDate = startDate.AddDate(1, 0, 0)
}
return startDate, endDate
}
func applyFilters(query *gorm.DB, filter string, search string, startDateStr string, endDateStr string) *gorm.DB {
// Filtrar por fechas si se proporcionaron
if startDateStr != "" && endDateStr != "" {
// Convertir las fechas de string a time.Time
startDate, err := time.Parse("2006-01-02", startDateStr) // Formato esperado: "YYYY-MM-DD"
if err != nil {
log.Printf("Error al parsear startDate: %v", err)
}
endDate, err := time.Parse("2006-01-02", endDateStr)
if err != nil {
log.Printf("Error al parsear endDate: %v", err)
}
// Establecemos la hora a las 23:59:59
endDate = endDate.Add(23 * time.Hour).Add(59 * time.Minute).Add(59 * time.Second)
// Filtrar eventos por el rango de fechas
query = query.Where("timestamp >= ? AND timestamp <= ?", startDate, endDate)
}
// Filtrar por búsqueda (si se proporcionó)
if search != "" {
query = query.Where("event_type LIKE ?", "%"+search+"%")
}
// Filtrar por otros filtros (como "Hoy", "Este mes", etc.), si se aplica
if filter != "" {
startDate, endDate := getDateFilter(filter)
if !startDate.IsZero() && !endDate.IsZero() {
query = query.Where("timestamp >= ? AND timestamp < ?", startDate, endDate)
}
}
return query
}
func calculateTotalOnHours(events []bot.HeatingEvent) (int, int) {
if len(events) == 0 {
return 0, 0
}
// Ordenar los eventos por Timestamp
sort.Slice(events, func(i, j int) bool {
return events[i].Timestamp.Before(events[j].Timestamp)
})
var totalDuration time.Duration
var lastOnTime time.Time // Usamos una variable no-puntero
for _, event := range events {
if event.EventType == "ON" {
// Guardamos la fecha del evento ON en lastOnTime como una copia (no puntero)
lastOnTime = event.Timestamp
} else if event.EventType == "OFF" && !lastOnTime.IsZero() {
// Verificar si los tiempos son realmente distintos
utcLastOnTime := lastOnTime.UTC()
utcCurrent := event.Timestamp.UTC()
// Asegúrate de que no estemos comparando el mismo segundo
if utcCurrent.After(utcLastOnTime) {
duration := utcCurrent.Sub(utcLastOnTime)
if duration > 0 {
totalDuration += duration
}
}
// Resetear lastOnTime después de usarlo
lastOnTime = time.Time{}
}
}
// Calcular horas y minutos
totalHours := totalDuration.Hours() // Total en horas con decimales
hours := int(totalHours) // Parte entera: horas completas
minutes := int((totalHours - float64(hours)) * 60) // Parte decimal convertida a minutos
// Devolver resultado
return hours, minutes
}
func calculateConsumptionData(events []bot.HeatingEvent) (map[string]float64, bool) {
if len(events) == 0 {
return map[string]float64{}, false
}
// Ordenar los eventos por Timestamp (aseguramos el orden cronológico)
sort.Slice(events, func(i, j int) bool {
return events[i].Timestamp.Before(events[j].Timestamp)
})
// Variable para determinar si agrupamos por mes
isGroupedByMonth := determineGrouping(events)
// Mapas para acumular consumos
consumptionByKey := make(map[string]float64) // Mapa general para día o mes
monthAggregation := make(map[string]float64) // Solo para el caso de agrupación por mes
var lastOnTime time.Time
for _, event := range events {
if event.EventType == "ON" {
// Guardamos el evento ON en la variable temporal
lastOnTime = event.Timestamp
} else if event.EventType == "OFF" && !lastOnTime.IsZero() {
// Calcular el consumo para este intervalo ON -> OFF
duration := event.Timestamp.Sub(lastOnTime).Hours()
// Determinar la clave para agrupar (día o mes)
var key string
if isGroupedByMonth {
key = lastOnTime.Format("January 2006") // Agrupado por mes
monthAggregation[key] += duration // Sumar al total del mes
} else {
key = lastOnTime.Format("2006-01-02") // Agrupado por día
consumptionByKey[key] += duration // Sumar al total del día
}
// Reiniciar lastOnTime
lastOnTime = time.Time{}
}
}
// Si estamos agrupando por mes, transferimos los valores de monthAggregation a consumptionByKey
if isGroupedByMonth {
for month, total := range monthAggregation {
consumptionByKey[month] = total
}
}
return consumptionByKey, isGroupedByMonth
}
// Función para determinar si los eventos deben agruparse por mes o por día
func determineGrouping(events []bot.HeatingEvent) bool {
// Usamos un mapa para almacenar pares de año y mes únicos
uniqueMonths := make(map[string]struct{})
// Recorremos todos los eventos y extraemos el año y mes
for _, event := range events {
// Extraemos el mes y el año del timestamp
yearMonth := event.Timestamp.Format("2006-01") // Formato "Año-Mes"
uniqueMonths[yearMonth] = struct{}{} // Usamos un map para asegurarnos de que no se repita
// Si ya tenemos más de un mes, terminamos
if len(uniqueMonths) > 1 {
return true // Agrupar por mes
}
}
// Si solo hay un mes único, agrupamos por día
return false
}