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 }