diff --git a/main.go b/main.go index e909dc6..f9bc773 100755 --- a/main.go +++ b/main.go @@ -483,6 +483,13 @@ func main() { app = pocketbase.New() + app.OnRecordAuthRequest("users").Add(func(e *core.RecordAuthEvent) error { + + e.Record.Set("lastLogin", time.Now().UTC()) + + return app.Dao().SaveRecord(e.Record) + }) + app.OnBeforeServe().Add(func(e *core.ServeEvent) error { // Serve privacy policy static page e.Router.GET("/privacy/*", apis.StaticDirectoryHandler(os.DirFS("./pb_public/privacy"), false)) @@ -1995,6 +2002,465 @@ func main() { }) + // ============================================ + // GET ALL COUNTRIES + // ============================================ + e.Router.GET("/api/getCountries", func(c echo.Context) error { + + language := c.QueryParam("language") + + countries := []struct { + ID string `db:"id" json:"id"` + CountryName string `db:"country_name" json:"country_name"` + CountryCode string `db:"country_code" json:"country_code"` + FlagUrl string `db:"flag_url" json:"flag_url"` + Logo string `db:"logo" json:"logo"` + }{} + + countryNameColumn := "country_name_" + language + query := ` SELECT id,country_code,flag_url,logo,` + countryNameColumn + ` AS country_name FROM countries` + err := app.DB(). + NewQuery(query). + All(&countries) + + if err != nil { + log.Printf("Failed to fetch countries: %v", err) + return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to fetch countries"}) + } + + if len(countries) == 0 { + return c.JSON(http.StatusNotFound, map[string]string{"error": "No countries found"}) + } + + // Build response with full logo URLs + result := []map[string]interface{}{} + for _, country := range countries { + var flagURL string + if country.Logo == "" { + flagURL = "" + } else { + flagURL = fmt.Sprintf("%s/api/files/countries/%s/%s", + baseUrl, + country.ID, + country.Logo, + ) + } + + result = append(result, map[string]interface{}{ + "id": country.ID, + "country_name": country.CountryName, + "country_code": country.CountryCode, + "flag_url": flagURL, + }) + } + + return c.JSON(http.StatusOK, result) + + }) + + // ============================================ + // GET COUNTRY DETAILS (Country + Leaders + All Statistics) + // ============================================ + e.Router.GET("/api/getCountryDetails", func(c echo.Context) error { + + countryCode := c.QueryParam("country_code") + language := c.QueryParam("language") + + if countryCode == "" { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "country_code is required"}) + } + + // Get country + country := struct { + ID string `db:"id" json:"id"` + CountryName string `db:"country_name" json:"country_name"` + CountryCode string `db:"country_code" json:"country_code"` + FlagUrl string `db:"flag_url" json:"flag_url"` + }{} + + countryNameColumn := "country_name_" + language + query := ` SELECT id,country_code,flag_url,logo,` + countryNameColumn + ` AS country_name FROM countries WHERE country_code = {:country_code}` + err := app.DB(). + NewQuery(query). + Bind(dbx.Params{ + "country_code": countryCode, + }). + One(&country) + + if err != nil { + log.Printf("Country not found: %v", err) + return c.JSON(http.StatusNotFound, map[string]string{"error": "Country not found"}) + } + + // Get leaders + leaders := []struct { + ID string `db:"id" json:"id"` + Name string `db:"name" json:"name"` + Position string `db:"position" json:"position"` + Photo string `db:"photo" json:"photo"` + IsCurrent bool `db:"is_current" json:"is_current"` + }{} + + nameColumn := "name_" + language + positionColumn := "position_" + language + + query2 := `SELECT id,` + nameColumn + ` AS name,` + positionColumn + ` AS position,photo,is_current FROM leaders WHERE country = {:country} AND is_current = true ORDER BY position ASC ` + + err = app.DB(). + NewQuery(query2). + Bind(dbx.Params{ + "country": country.ID, + }). + All(&leaders) + + if err != nil { + log.Printf("Failed to fetch leaders: %v", err) + } + + // Get all statistics + allStatistics := []struct { + ID string `db:"id" json:"id"` + Type string `db:"type" json:"type"` + Topic string `db:"topic" json:"topic"` + Value string `db:"value" json:"value"` + Rank float64 `db:"rank" json:"rank"` + IsRankPositive bool `db:"is_rank_positive" json:"is_rank_positive"` + RankHeading string `db:"rank_heading" json:"rank_heading"` + }{} + + topicColumn := "topic_" + language + valueColumn := "value_" + language + rankHeadingColumn := "rank_heading_" + language + + query3 := `SELECT id,type,` + topicColumn + ` AS topic,` + valueColumn + ` AS value,` + rankHeadingColumn + ` AS rank_heading,rank,is_rank_positive FROM country_statistics WHERE country = {:country} ORDER BY type ASC, rank ASC` + + err = app.DB(). + NewQuery(query3). + Bind(dbx.Params{ + "country": country.ID, + }). + All(&allStatistics) + + if err != nil { + log.Printf("Failed to fetch statistics: %v", err) + } + + // Group statistics by type + statisticsMap := map[string]interface{}{ + "demography": []interface{}{}, + "gdp": []interface{}{}, + "fdi": []interface{}{}, + "trade": []interface{}{}, + "bilateral_trade": []interface{}{}, + } + + for _, stat := range allStatistics { + // Remove quotes from type field if present + cleanType := stat.Type + if len(cleanType) > 2 && cleanType[0] == '"' && cleanType[len(cleanType)-1] == '"' { + cleanType = cleanType[1 : len(cleanType)-1] + } + + if _, exists := statisticsMap[cleanType]; exists { + statisticsMap[cleanType] = append(statisticsMap[cleanType].([]interface{}), stat) + } + } + + // Build final response + result := map[string]interface{}{ + "country": country, + "leaders": leaders, + "statistics": statisticsMap, + } + + return c.JSON(http.StatusOK, result) + }) + + // ============================================ + // GET BILATERAL TRADE DATA + // ============================================ + e.Router.GET("/api/getBilateralTradeData", func(c echo.Context) error { + + countryCode := c.QueryParam("country_code") + statisticsId := c.QueryParam("statistics_id") + language := c.QueryParam("language") + + if countryCode == "" { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "country_code is required"}) + } + + // Get country + country := struct { + ID string `db:"id" json:"id"` + CountryName string `db:"country_name" json:"country_name"` + CountryCode string `db:"country_code" json:"country_code"` + FlagUrl string `db:"flag_url" json:"flag_url"` + }{} + + countryNameColumn := "country_name_" + language + query := ` SELECT id,country_code,flag_url,logo,` + countryNameColumn + ` AS country_name FROM countries WHERE country_code = {:country_code}` + err := app.DB(). + NewQuery(query). + Bind(dbx.Params{ + "country_code": countryCode, + }). + One(&country) + + if err != nil { + return c.JSON(http.StatusNotFound, map[string]string{"error": "Country not found"}) + } + + // Get all bilateral trade statistics + allBilateralTradeData := []struct { + Topic string `db:"topic" json:"topic"` + Value string `db:"value" json:"value"` + Icon string `db:"icon" json:"icon"` + }{} + + topicColumn := "topic_" + language + + query2 := `SELECT ` + topicColumn + ` AS topic,value,icon FROM bilateral_trade_data WHERE country = {:country} AND statistics = {:statistics}` + + err = app.DB(). + NewQuery(query2). + Bind(dbx.Params{ + "country": country.ID, + "statistics": statisticsId, + }). + All(&allBilateralTradeData) + + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{ + "error": err.Error(), + }) + } + + if len(allBilateralTradeData) == 0 { + return c.JSON(http.StatusNotFound, map[string]string{ + "error": "Data not found", + }) + } + + return c.JSON(http.StatusOK, allBilateralTradeData) + }) + + // ============================================ + // GET ALL COMPETITIVENESS REPORTS + // ============================================ + e.Router.GET("/api/getCompetitivenessReports", func(c echo.Context) error { + + type CountryMini struct { + ID string `json:"id"` + CountryName string `json:"country_name"` + Flag string `json:"flag"` + } + + type CompetitivenessReport struct { + ID string `json:"id"` + Year int `json:"year"` + CurrentYearRank int `json:"current_year_rank"` + PreviousYearRank int `json:"previous_year_rank"` + IsRankPositive bool `json:"is_rank_positive"` + + Heading string `json:"heading"` + Content string `json:"content"` + Logo string `json:"logo"` + + FirstGlobally []CountryMini `json:"first_in_globally"` + FirstGCC []CountryMini `json:"first_in_gcc"` + FirstArab []CountryMini `json:"first_in_arab_countries"` + } + + language := c.QueryParam("language") + if language == "" { + language = "en" + } + + headingColumn := "heading_" + language + contentColumn := "content_" + language + + // Step 1: fetch competitiveness reports + rows := []struct { + ID string `db:"id"` + Year int `db:"year"` + CurrentYearRank int `db:"current_year_rank"` + PreviousYearRank int `db:"previous_year_rank"` + IsRankPositive bool `db:"is_rank_positive"` + Logo string `db:"logo"` + + Heading string `db:"heading"` + Content string `db:"content"` + + FirstGlobally string `db:"first_in_globally"` + FirstGCC string `db:"first_in_gcc"` + FirstArab string `db:"first_in_arab_countries"` + }{} + + query := ` + SELECT + id, + year, + current_year_rank, + previous_year_rank, + is_rank_positive, + logo, + ` + headingColumn + ` AS heading, + ` + contentColumn + ` AS content, + first_in_globally, + first_in_gcc, + first_in_arab_countries + FROM competitive_reports + ` + + err := app.DB().NewQuery(query).All(&rows) + if err != nil { + return c.JSON(http.StatusInternalServerError, echo.Map{ + "error": err.Error(), + }) + } + + if len(rows) == 0 { + return c.JSON(http.StatusNotFound, echo.Map{ + "error": "No competitiveness reports found", + }) + } + + // return c.JSON(http.StatusOK, rows) + + // Step 2: loop & attach relation data + result := []CompetitivenessReport{} + + for _, r := range rows { + + report := CompetitivenessReport{ + ID: r.ID, + Year: r.Year, + CurrentYearRank: r.CurrentYearRank, + PreviousYearRank: r.PreviousYearRank, + IsRankPositive: r.IsRankPositive, + Heading: r.Heading, + Content: r.Content, + Logo: baseUrl + "/api/files/competitive_reports/" + r.ID + "/" + r.Logo, + } + + loadCountries := func(ids []string) []CountryMini { + if len(ids) == 0 { + return []CountryMini{} + } + + inClause := buildSafeInClause(ids) + + query := ` + SELECT + id, + country_name_` + language + ` AS country_name, + flag_url AS flag + FROM countries + WHERE id IN ` + inClause + + countries := []CountryMini{} + + err := app.DB(). + NewQuery(query). + All(&countries) + + if err != nil { + log.Println("Country fetch error:", err) + } + + return countries + } + + report.FirstGlobally = loadCountries(parseRelationIDs(r.FirstGlobally)) + report.FirstGCC = loadCountries(parseRelationIDs(r.FirstGCC)) + report.FirstArab = loadCountries(parseRelationIDs(r.FirstArab)) + + result = append(result, report) + } + + return c.JSON(http.StatusOK, result) + }) + + // ============================================ + // SEED SAMPLE DATA (Run once to populate database) + // ============================================ + e.Router.POST("/api/seedSampleData", func(c echo.Context) error { + + // Create Argentina + countryID := "" + err := app.Dao().DB(). + Select("id"). + From("countries"). + Where(dbx.HashExp{"country_code": "AR"}). + One(&struct{ ID string }{ID: countryID}) + + // If Argentina doesn't exist, create it + if err != nil { + _, err = app.Dao().DB(). + Insert("countries", dbx.Params{ + "country_name": "Argentina", + "country_code": "AR", + "flag_url": "https://flagcdn.com/ar.svg", + }).Execute() + + if err != nil { + log.Printf("Failed to create Argentina: %v", err) + return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to create country"}) + } + + // Get the newly created country ID + err = app.Dao().DB(). + Select("id"). + From("countries"). + Where(dbx.HashExp{"country_code": "AR"}). + One(&struct{ ID string }{ID: countryID}) + } + + // Insert Leaders (simplified - you can add more) + leaders := []map[string]interface{}{ + {"country": countryID, "name": "Javier Milei", "position": "President", "is_current": true}, + {"country": countryID, "name": "Victoria Villarruel", "position": "Vice President", "is_current": true}, + } + + for _, leader := range leaders { + _, err = app.Dao().DB().Insert("leaders", leader).Execute() + if err != nil { + log.Printf("Failed to insert leader: %v", err) + } + } + + // Insert Statistics + statistics := []map[string]interface{}{ + // Demography + {"country": countryID, "type": "\"demography\"", "topic": "Capital City", "value": "Buenos Aires"}, + {"country": countryID, "type": "\"demography\"", "topic": "Area", "value": "2,780,400 sq. km"}, + {"country": countryID, "type": "\"demography\"", "topic": "2021 Population", "value": "45.81 M", "rank": 0.9, "is_rank_positive": true}, + + // GDP + {"country": countryID, "type": "\"gdp\"", "topic": "2021 Nominal (AED)", "value": "1.8 T", "rank": 2.5, "is_rank_positive": true}, + {"country": countryID, "type": "\"gdp\"", "topic": "2021 Real (AED)", "value": "2.08 T", "rank": 3.2, "is_rank_positive": true}, + + // FDI + {"country": countryID, "type": "\"fdi\"", "topic": "2020 FDI Inflow (AED)", "value": "4.75 B", "rank": -10.41, "is_rank_positive": false}, + + // Trade + {"country": countryID, "type": "\"trade\"", "topic": "2021 Total Trade (AED)", "value": "603 B", "rank": 35.36, "is_rank_positive": true}, + + // Bilateral Trade + {"country": countryID, "type": "\"bilateral_trade\"", "topic": "Total (2022)", "value": "$4.7M"}, + {"country": countryID, "type": "\"bilateral_trade\"", "topic": "Top 5 Import Commodities", "value": "Jewellery, Diamonds, Gold", "rank": 1}, + } + + for _, stat := range statistics { + _, err = app.Dao().DB().Insert("country_statistics", stat).Execute() + if err != nil { + log.Printf("Failed to insert statistic: %v", err) + } + } + + return c.JSON(http.StatusOK, map[string]string{"message": "Sample data seeded successfully"}) + }) + type DownloadRequest struct { Username string `json:"username"` Password string `json:"password"` @@ -3428,3 +3894,31 @@ func sendStatusChangeEmail(app *pocketbase.PocketBase, record *models.Record, ne log.Printf("Status change email sent successfully to: %s", userEmail) return nil } + +func parseRelationIDs(value string) []string { + if value == "" { + return []string{} + } + + var ids []string + _ = json.Unmarshal([]byte(value), &ids) + return ids +} + +func buildSafeInClause(ids []string) string { + quoted := make([]string, 0, len(ids)) + + for _, id := range ids { + if id == "" { + continue + } + // PocketBase IDs are safe: lowercase letters + numbers + quoted = append(quoted, "'"+id+"'") + } + + if len(quoted) == 0 { + return "('')" // never matches + } + + return "(" + strings.Join(quoted, ",") + ")" +} diff --git a/pb_data/data.db b/pb_data/data.db index d69fca6..e4f29dc 100644 Binary files a/pb_data/data.db and b/pb_data/data.db differ diff --git a/pb_data/data.db-shm b/pb_data/data.db-shm index 13c0381..fe9ac28 100644 Binary files a/pb_data/data.db-shm and b/pb_data/data.db-shm differ diff --git a/pb_data/data.db-wal b/pb_data/data.db-wal index dd152de..e69de29 100644 Binary files a/pb_data/data.db-wal and b/pb_data/data.db-wal differ diff --git a/pb_data/logs.db b/pb_data/logs.db index dcf464e..239cb77 100644 Binary files a/pb_data/logs.db and b/pb_data/logs.db differ diff --git a/pb_data/logs.db-shm b/pb_data/logs.db-shm index d141e3b..fe9ac28 100644 Binary files a/pb_data/logs.db-shm and b/pb_data/logs.db-shm differ diff --git a/pb_data/logs.db-wal b/pb_data/logs.db-wal index 61353fd..e69de29 100644 Binary files a/pb_data/logs.db-wal and b/pb_data/logs.db-wal differ