2024-11-28 13:52:58 +01:00
|
|
|
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"
|
2024-11-30 13:14:29 +01:00
|
|
|
"formatTimestamp": func(t time.Time) string {
|
|
|
|
location, _ := time.LoadLocation("Europe/Madrid")
|
|
|
|
localTime := t.In(location)
|
|
|
|
return localTime.Format("2006-01-02 15:04 (MST)")
|
|
|
|
},
|
2024-11-28 13:52:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|