package handlers import ( "fmt" "net/http" "strconv" "time" "github.com/gin-gonic/gin" "github.com/rss2/backend/internal/db" "github.com/rss2/backend/internal/models" ) type NewsResponse struct { ID string `json:"id"` Titulo string `json:"titulo"` Resumen string `json:"resumen"` URL string `json:"url"` Fecha *time.Time `json:"fecha"` ImagenURL *string `json:"imagen_url"` CategoriaID *int64 `json:"categoria_id"` PaisID *int64 `json:"pais_id"` FuenteNombre string `json:"fuente_nombre"` TitleTranslated *string `json:"title_translated"` SummaryTranslated *string `json:"summary_translated"` LangTranslated *string `json:"lang_translated"` Entities []Entity `json:"entities,omitempty"` } func GetNews(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) perPage, _ := strconv.Atoi(c.DefaultQuery("per_page", "30")) query := c.Query("q") categoryID := c.Query("category_id") countryID := c.Query("country_id") translatedOnly := c.Query("translated_only") == "true" if page < 1 { page = 1 } if perPage < 1 || perPage > 100 { perPage = 30 } offset := (page - 1) * perPage where := "1=1" args := []interface{}{} argNum := 1 if query != "" { where += fmt.Sprintf(" AND (n.titulo ILIKE $%d OR n.resumen ILIKE $%d)", argNum, argNum) args = append(args, "%"+query+"%") argNum++ } if categoryID != "" { where += fmt.Sprintf(" AND n.categoria_id = $%d", argNum) args = append(args, categoryID) argNum++ } if countryID != "" { where += fmt.Sprintf(" AND n.pais_id = $%d", argNum) args = append(args, countryID) argNum++ } if translatedOnly { where += " AND t.status = 'done' AND t.titulo_trad IS NOT NULL AND t.titulo_trad != n.titulo" } var total int countQuery := fmt.Sprintf("SELECT COUNT(*) FROM noticias n LEFT JOIN traducciones t ON t.noticia_id = n.id AND t.lang_to = 'es' WHERE %s", where) err := db.GetPool().QueryRow(c.Request.Context(), countQuery, args...).Scan(&total) if err != nil { c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: "Failed to count news", Message: err.Error()}) return } if total == 0 { c.JSON(http.StatusOK, models.NewsListResponse{ News: []models.NewsWithTranslations{}, Total: 0, Page: page, PerPage: perPage, TotalPages: 0, }) return } sqlQuery := fmt.Sprintf(` SELECT n.id, n.titulo, COALESCE(n.resumen, ''), n.url, n.fecha, n.imagen_url, n.categoria_id, n.pais_id, n.fuente_nombre, t.titulo_trad, t.resumen_trad, t.lang_to as lang_trad FROM noticias n LEFT JOIN traducciones t ON t.noticia_id = n.id AND t.lang_to = 'es' WHERE %s ORDER BY n.fecha DESC LIMIT $%d OFFSET $%d `, where, argNum, argNum+1) args = append(args, perPage, offset) rows, err := db.GetPool().Query(c.Request.Context(), sqlQuery, args...) if err != nil { c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: "Failed to fetch news", Message: err.Error()}) return } defer rows.Close() var newsList []NewsResponse for rows.Next() { var n NewsResponse var imagenURL, fuenteNombre *string var categoriaID, paisID *int32 err := rows.Scan( &n.ID, &n.Titulo, &n.Resumen, &n.URL, &n.Fecha, &imagenURL, &categoriaID, &paisID, &fuenteNombre, &n.TitleTranslated, &n.SummaryTranslated, &n.LangTranslated, ) if err != nil { continue } if imagenURL != nil { n.ImagenURL = imagenURL } if fuenteNombre != nil { n.FuenteNombre = *fuenteNombre } if categoriaID != nil { catID := int64(*categoriaID) n.CategoriaID = &catID } if paisID != nil { pID := int64(*paisID) n.PaisID = &pID } newsList = append(newsList, n) } totalPages := (total + perPage - 1) / perPage c.JSON(http.StatusOK, gin.H{ "news": newsList, "total": total, "page": page, "per_page": perPage, "total_pages": totalPages, }) } func GetNewsByID(c *gin.Context) { id := c.Param("id") sqlQuery := ` SELECT n.id, n.titulo, COALESCE(n.resumen, ''), n.url, n.fecha, n.imagen_url, n.categoria_id, n.pais_id, n.fuente_nombre, t.titulo_trad, t.resumen_trad, t.lang_to as lang_trad FROM noticias n LEFT JOIN traducciones t ON t.noticia_id = n.id AND t.lang_to = 'es' WHERE n.id = $1` var n NewsResponse var imagenURL, fuenteNombre *string var categoriaID, paisID *int32 err := db.GetPool().QueryRow(c.Request.Context(), sqlQuery, id).Scan( &n.ID, &n.Titulo, &n.Resumen, &n.URL, &n.Fecha, &imagenURL, &categoriaID, &paisID, &fuenteNombre, &n.TitleTranslated, &n.SummaryTranslated, &n.LangTranslated, ) if err != nil { c.JSON(http.StatusNotFound, models.ErrorResponse{Error: "News not found"}) return } if imagenURL != nil { n.ImagenURL = imagenURL } if fuenteNombre != nil { n.FuenteNombre = *fuenteNombre } if categoriaID != nil { catID := int64(*categoriaID) n.CategoriaID = &catID } if paisID != nil { pID := int64(*paisID) n.PaisID = &pID } // Fetch entities for this news entitiesQuery := ` SELECT t.valor, t.tipo, 1 as cnt, t.wiki_summary, t.wiki_url, t.image_path FROM tags_noticia tn JOIN tags t ON tn.tag_id = t.id JOIN traducciones tr ON tn.traduccion_id = tr.id WHERE tr.noticia_id = $1 AND t.tipo IN ('persona', 'organizacion') ` rows, err := db.GetPool().Query(c.Request.Context(), entitiesQuery, id) var entities []Entity if err == nil { defer rows.Close() for rows.Next() { var e Entity if err := rows.Scan(&e.Valor, &e.Tipo, &e.Count, &e.WikiSummary, &e.WikiURL, &e.ImagePath); err == nil { entities = append(entities, e) } } } if entities == nil { entities = []Entity{} } n.Entities = entities c.JSON(http.StatusOK, n) } func DeleteNews(c *gin.Context) { id := c.Param("id") result, err := db.GetPool().Exec(c.Request.Context(), "DELETE FROM noticias WHERE id = $1", id) if err != nil { c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: "Failed to delete news", Message: err.Error()}) return } if result.RowsAffected() == 0 { c.JSON(http.StatusNotFound, models.ErrorResponse{Error: "News not found"}) return } c.JSON(http.StatusOK, models.SuccessResponse{Message: "News deleted successfully"}) } type Entity struct { Valor string `json:"valor"` Tipo string `json:"tipo"` Count int `json:"count"` WikiSummary *string `json:"wiki_summary"` WikiURL *string `json:"wiki_url"` ImagePath *string `json:"image_path"` } type EntityListResponse struct { Entities []Entity `json:"entities"` Total int `json:"total"` Page int `json:"page"` PerPage int `json:"per_page"` TotalPages int `json:"total_pages"` } func GetEntities(c *gin.Context) { countryID := c.Query("country_id") categoryID := c.Query("category_id") entityType := c.DefaultQuery("tipo", "persona") q := c.Query("q") pageStr := c.DefaultQuery("page", "1") perPageStr := c.DefaultQuery("per_page", "50") page, _ := strconv.Atoi(pageStr) perPage, _ := strconv.Atoi(perPageStr) if page < 1 { page = 1 } if perPage < 1 || perPage > 100 { perPage = 50 } offset := (page - 1) * perPage where := "t.tipo = $1" args := []interface{}{entityType} if countryID != "" { where += fmt.Sprintf(" AND n.pais_id = $%d", len(args)+1) args = append(args, countryID) } if categoryID != "" { where += fmt.Sprintf(" AND n.categoria_id = $%d", len(args)+1) args = append(args, categoryID) } if q != "" { where += fmt.Sprintf(" AND COALESCE(ea.canonical_name, t.valor) ILIKE $%d", len(args)+1) args = append(args, "%"+q+"%") } // 1. Get the total count of distinct canonical entities matching the filter countQuery := fmt.Sprintf(` SELECT COUNT(DISTINCT COALESCE(ea.canonical_name, t.valor)) FROM tags_noticia tn JOIN tags t ON tn.tag_id = t.id JOIN traducciones tr ON tn.traduccion_id = tr.id JOIN noticias n ON tr.noticia_id = n.id LEFT JOIN entity_aliases ea ON LOWER(ea.alias) = LOWER(t.valor) AND ea.tipo = t.tipo WHERE %s `, where) var total int err := db.GetPool().QueryRow(c.Request.Context(), countQuery, args...).Scan(&total) if err != nil { c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: "Failed to get entities count", Message: err.Error()}) return } if total == 0 { c.JSON(http.StatusOK, EntityListResponse{ Entities: []Entity{}, Total: 0, Page: page, PerPage: perPage, TotalPages: 0, }) return } // 2. Fetch the paginated entities args = append(args, perPage, offset) query := fmt.Sprintf(` SELECT COALESCE(ea.canonical_name, t.valor) as valor, t.tipo, COUNT(*)::int as cnt, MAX(t.wiki_summary), MAX(t.wiki_url), MAX(t.image_path) FROM tags_noticia tn JOIN tags t ON tn.tag_id = t.id JOIN traducciones tr ON tn.traduccion_id = tr.id JOIN noticias n ON tr.noticia_id = n.id LEFT JOIN entity_aliases ea ON LOWER(ea.alias) = LOWER(t.valor) AND ea.tipo = t.tipo WHERE %s GROUP BY COALESCE(ea.canonical_name, t.valor), t.tipo ORDER BY cnt DESC LIMIT $%d OFFSET $%d `, where, len(args)-1, len(args)) rows, err := db.GetPool().Query(c.Request.Context(), query, args...) if err != nil { c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: "Failed to get entities", Message: err.Error()}) return } defer rows.Close() var entities []Entity for rows.Next() { var e Entity if err := rows.Scan(&e.Valor, &e.Tipo, &e.Count, &e.WikiSummary, &e.WikiURL, &e.ImagePath); err != nil { continue } entities = append(entities, e) } if entities == nil { entities = []Entity{} } totalPages := (total + perPage - 1) / perPage c.JSON(http.StatusOK, EntityListResponse{ Entities: entities, Total: total, Page: page, PerPage: perPage, TotalPages: totalPages, }) }