From c1f769e1cf5a040bc79d8f319d96489f90f3db00 Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Wed, 1 Oct 2025 15:53:08 +0530 Subject: [PATCH] GWM --- pb.zip | Bin 30896 -> 0 bytes pb/.env | 4 - pb/main.go | 3069 ------------------------------ pb/utils/calculation.go | 825 -------- pb/utils/exclude_filter_data.go | 65 - pb/utils/translate_chart_data.go | 48 - 6 files changed, 4011 deletions(-) delete mode 100644 pb.zip delete mode 100644 pb/.env delete mode 100755 pb/main.go delete mode 100755 pb/utils/calculation.go delete mode 100755 pb/utils/exclude_filter_data.go delete mode 100755 pb/utils/translate_chart_data.go diff --git a/pb.zip b/pb.zip deleted file mode 100644 index 6611fdba1e2fd42a76450d4bedfea70f655d4df1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30896 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 Zf|Hj5|LU55u`VP~(pN_XclmquzW`LTTipNv diff --git a/pb/.env b/pb/.env deleted file mode 100644 index 5f2b1eb..0000000 --- a/pb/.env +++ /dev/null @@ -1,4 +0,0 @@ -APP_SIGNATURE = fcsc.gov.ae.X7pL9qZm2A - -LIVE_BASE_URL = https://pocket.fcsc.gov.ae -BASE_URL = https://pocket.fcsc.gov.ae diff --git a/pb/main.go b/pb/main.go deleted file mode 100755 index d148581..0000000 --- a/pb/main.go +++ /dev/null @@ -1,3069 +0,0 @@ -package main - -import ( - "bytes" - "context" - "crypto/rand" - "encoding/hex" - "encoding/json" - "encoding/xml" - "fmt" - "io/ioutil" - "log" - "math/big" - "net/http" - "net/mail" - "os" - "path/filepath" - "pocketbase/utils" - "strings" - "time" - - firebase "firebase.google.com/go" - "firebase.google.com/go/messaging" - "github.com/joho/godotenv" - "github.com/labstack/echo/v5" - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase" - "github.com/pocketbase/pocketbase/apis" - "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/models" - - // "github.com/pocketbase/pocketbase/tools/hook" - - "github.com/pocketbase/pocketbase/tools/mailer" - "github.com/pocketbase/pocketbase/tools/types" - "google.golang.org/api/option" -) - -var baseUrl = os.Getenv("BASE_URL") -var liveBaseUrl = os.Getenv("LIVE_BASE_URL") - -type EmailConfigurationResponse struct { - Page int `json:"page"` - PerPage int `json:"perPage"` - TotalItems int `json:"totalItems"` - TotalPages int `json:"totalPages"` - Items []struct { - Type string `json:"type"` - Email string `json:"email"` - } `json:"items"` -} - -type GenericData struct { - XMLName xml.Name `xml:"GenericData"` - DataSet []Obs `xml:"DataSet>Obs"` -} - -type Obs struct { - ObsKey []ObsKey `xml:"ObsKey>Value"` // Dynamic key-value pairs - ObsValue ObsValue `xml:"ObsValue"` -} - -type ObsKey struct { - ID string `xml:"id,attr"` // Attribute for 'id' - Value string `xml:"value,attr"` // Attribute for 'value' -} - -type ObsValue struct { - Value string `xml:"value,attr"` // Attribute for 'value' -} - -// Helper function to map ObsKey into a dictionary -func mapObsKey(keys []ObsKey) map[string]string { - result := map[string]string{} - for _, key := range keys { - result[key.ID] = key.Value - } - return result -} - -// Helper function to clean XML namespace prefixes -func removeNamespace(xmlBytes []byte) string { - return strings.ReplaceAll(string(xmlBytes), "generic:", "") -} - -// Function to fetch XML from an API and convert it to JSON -func fetchAndConvertXMLToJSON(apiURL string, outputDir string, fileName string, tableName string, id string) ([]map[string]interface{}, error) { - // Call the API and get the XML response - resp, err := http.Get(apiURL) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - // Read the response body - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - // fmt.Println("Response url:", apiURL) - // fmt.Println("Response status:", resp.Status) - // fmt.Println("Response body:", string(body)) - - // Clean the XML by removing namespace prefixes - cleanXML := []byte(removeNamespace(body)) - - // Parse the cleaned XML data - var data GenericData - err = xml.Unmarshal(cleanXML, &data) - if err != nil { - return nil, err - } - - // Transform the data for JSON output - response := []map[string]interface{}{} - for _, obs := range data.DataSet { - obsMap := map[string]interface{}{ - "ObsKey": mapObsKey(obs.ObsKey), - "ObsValue": obs.ObsValue, - } - response = append(response, obsMap) - } - - // Convert the response to JSON - jsonData, err := json.MarshalIndent(response, "", " ") - if err != nil { - return nil, err - } - - // Save the JSON data to a file if required - // Ensure the folder exists - if err := os.MkdirAll(outputDir, os.ModePerm); err != nil { - return nil, err - } - - // Save the file - file := fmt.Sprintf("%s/%s.json", outputDir, fileName) - if err := ioutil.WriteFile(file, jsonData, 0644); err != nil { - return nil, err - } - - // Find the charts record - record, err := app.Dao().FindRecordById(tableName, id) - if err != nil { - // return c.JSON(500, map[string]interface{}{"code": 500,"message": "Error finding user.",}) - } - record.Set("file_status", true) - if err := app.Dao().SaveRecord(record); err != nil { - // return c.JSON(500, map[string]interface{}{"code": 500,"message": "Failed to verify user.",}) - } - - return response, nil -} - -// Function to read JSON data from a file -func readJSONFromFile(folderName, fileName string) ([]map[string]interface{}, error) { - // Construct the file path dynamically using the folder name - filePath := fmt.Sprintf("%s/%s.json", folderName, fileName) - - // Read and return the JSON content from the file - fileContent, err := ioutil.ReadFile(filePath) - if err != nil { - return nil, fmt.Errorf("failed to read file '%s': %w", filePath, err) - } - - // Parse the JSON content from the file - var jsonResponse []map[string]interface{} - if err := json.Unmarshal(fileContent, &jsonResponse); err != nil { - return nil, fmt.Errorf("failed to parse JSON from file '%s': %w", filePath, err) - } - - return jsonResponse, nil -} - -type ResponseItem struct { - ObsKey map[string]string `json:"ObsKey"` - ObsValue map[string]string `json:"ObsValue"` -} - -type FilterJsonData struct { - Order int `json:"order"` - En string `json:"en"` - Ar string `json:"ar"` -} - -type FilterGroup struct { - FilterKey string `json:"filter_key"` - FilterData []interface{} `json:"filter_data"` - FilterTextAndOrder FilterJsonData `json:"filter_text_and_order"` -} - -func generateFilters(responseRaw []map[string]interface{}, filterKeys []string, filterJsonData map[string]FilterJsonData) ([]FilterGroup, error) { - // Transform responseRaw to []ResponseItem - var response []ResponseItem - responseBytes, err := json.Marshal(responseRaw) // Marshal raw data to JSON - if err != nil { - log.Printf("Failed to marshal raw response: %v", err) - return nil, err - } - - if err := json.Unmarshal(responseBytes, &response); err != nil { // Unmarshal to []ResponseItem - log.Printf("Failed to unmarshal response: %v", err) - return nil, err - } - - // Create a map to hold unique filter data - filterMap := make(map[string]map[string]struct{}) - for _, key := range filterKeys { - filterMap[key] = make(map[string]struct{}) - } - - // Populate filter data dynamically - for _, item := range response { - for _, key := range filterKeys { - if value, exists := item.ObsKey[key]; exists { - filterMap[key][value] = struct{}{} - } - } - } - - // Convert filter map to the desired structure - var filters []FilterGroup - for key, values := range filterMap { - filterData := make([]interface{}, 0, len(values)) - for value := range values { - filterData = append(filterData, value) - } - - if key == "REF_AREA" { - filterData = []interface{}{"AE-AZ", "AE-DU", "AE-SH", "AE-AJ", "AE-UQ", "AE-RK", "AE-FJ"} - } - filters = append(filters, FilterGroup{ - FilterKey: key, - FilterData: filterData, - FilterTextAndOrder: filterJsonData[key], - }) - } - - return filters, nil -} - -// Function to fetch filter data of dataset -func fetchfilterJSON(kpi string) ([]FilterGroup, error) { - - // Define struct for the result - var dataSetFilter struct { - Id string `db:"id" json:"id"` - FullDataUrl string `db:"full_data_url" json:"full_data_url"` - FilterKeys string `db:"filter_keys" json:"filter_keys"` - FileStatus bool `db:"file_status" json:"file_status"` - DataSet string `db:"dataset" json:"dataset"` - Kpi string `db:"kpi" json:"kpi"` - ExclusionFiltersJSON string `db:"exclusion_filters_array" json:"exclusion_filters_array"` - FilterJson string `db:"filter_json" json:"filter_json"` - } - - // Query to get one row from the data_set_filter table - err := app.DB(). - Select("id", "full_data_url", "filter_keys", "file_status", "dataset", "kpi", "filter_json", "exclusion_filters_array"). - From("data_set_filter"). - Where(dbx.HashExp{"kpi": kpi}). - One(&dataSetFilter) - - if err != nil { - log.Printf("Failed to fetch data_set_filter data: %v", err) - return nil, err - } - - // Use a map to store the dynamic JSON keys - var filterJson map[string]FilterJsonData - - // Parse the JSON - err = json.Unmarshal([]byte(dataSetFilter.FilterJson), &filterJson) - if err != nil { - log.Fatalf("Error parsing JSON: %v", err) - } - - // Fetch and convert XML to JSON if file is not saved - if !dataSetFilter.FileStatus { // Check if FileStatus is false - _, err := fetchAndConvertXMLToJSON(dataSetFilter.FullDataUrl, "filter_files", dataSetFilter.Kpi, "data_set_filter", dataSetFilter.Id) - if err != nil { - log.Printf("Failed to process URL '%s': %v", dataSetFilter.FullDataUrl, err) - return nil, err - } - } - - // Read the JSON data from the file - response, err := readJSONFromFile("filter_files", dataSetFilter.Kpi) - if err != nil { - log.Printf("Failed to read JSON for Kpi filter '%s': %v", dataSetFilter.Kpi, err) - return nil, err - } - - // Parse the filter keys - filterKeys := strings.Split(dataSetFilter.FilterKeys, ",") - - // Generate filters - filters, err := generateFilters(response, filterKeys, filterJson) - if err != nil { - log.Printf("Failed to generate filters: %v", err) - return nil, err - } - - // Parse exclusion filters JSON if it's not empty - var exclusionFilters []utils.FilterGroup - if dataSetFilter.ExclusionFiltersJSON != "" { - err = json.Unmarshal([]byte(dataSetFilter.ExclusionFiltersJSON), &exclusionFilters) - if err != nil { - log.Printf("Failed to parse exclusion filters JSON: %v", err) - return nil, err - } - // log.Printf("Parsed Exclusion Filters: %+v", exclusionFilters) - } - - // Convert filters from []FilterGroup to []utils.FilterGroup manually - var utilsFilters []utils.FilterGroup - for _, f := range filters { - utilsFilters = append(utilsFilters, convertToUtilsFilterGroup(f)) - } - - // Apply the filter removal logic only if exclusionFilters has values - if len(exclusionFilters) > 0 { - filteredResult := utils.RemoveExcludedFilters(utilsFilters, exclusionFilters) - - // Convert back to []FilterGroup before returning - var finalFilters []FilterGroup - for _, f := range filteredResult { - finalFilters = append(finalFilters, convertToFilterGroup(f)) - } - - return finalFilters, nil - } - - // If no exclusion filters, return the original filters - return filters, nil - -} - -// Convert FilterGroup to utils.FilterGroup -func convertToUtilsFilterGroup(f FilterGroup) utils.FilterGroup { - return utils.FilterGroup{ - FilterKey: f.FilterKey, - FilterData: f.FilterData, - FilterTextAndOrder: utils.FilterJsonData{ - Order: f.FilterTextAndOrder.Order, - En: f.FilterTextAndOrder.En, - Ar: f.FilterTextAndOrder.Ar, - }, - } -} - -// Convert utils.FilterGroup to FilterGroup -func convertToFilterGroup(f utils.FilterGroup) FilterGroup { - return FilterGroup{ - FilterKey: f.FilterKey, - FilterData: f.FilterData, - FilterTextAndOrder: FilterJsonData{ - Order: f.FilterTextAndOrder.Order, - En: f.FilterTextAndOrder.En, - Ar: f.FilterTextAndOrder.Ar, - }, - } -} - -// LanguageSource represents the source language data structure -type LanguageSource struct { - Dataset string `json:"dataset"` - Key string `json:"key"` - Value string `json:"value"` - ValueEn string `json:"value_en"` - ValueAr string `json:"value_ar"` -} - -func TranslateFilters(app *pocketbase.PocketBase, dataset string, filterGroups []FilterGroup, langKey string) ([]FilterGroup, error) { - // Fetch source data from the PocketBase collection - param := dbx.NewExp("dataset = {:dataset}", dbx.Params{"dataset": dataset}) - records, err := app.Dao().FindRecordsByExpr("charts_variables", param) - if err != nil { - return nil, fmt.Errorf("failed to fetch source data: %w", err) - } - - // Parse the source data into a slice of LanguageSource - var sourceData []LanguageSource - for _, record := range records { - source := LanguageSource{ - Dataset: record.GetString("dataset"), - Key: record.GetString("key"), - Value: record.GetString("value"), - ValueEn: record.GetString("value_en"), - ValueAr: record.GetString("value_ar"), - } - sourceData = append(sourceData, source) - } - - // Translate the filter data - for i, filter := range filterGroups { - - // Create a map for quick lookup of translations - translationMap := make(map[string]string) - for _, source := range sourceData { - var value string - switch langKey { - case "value_en": - value = source.ValueEn - case "value_ar": - value = source.ValueAr - default: - continue - } - if source.Key == filter.FilterKey { - translationMap[source.Value] = value - } - - } - - if filter.FilterKey == "TIME_PERIOD" { - // Skip translation for TIME_PERIOD - continue - } - for j, v := range filter.FilterData { - // Type assertion for interface{} to string - value, ok := v.(string) - if !ok { - return nil, fmt.Errorf("unexpected type for filter data: %v", v) - } - - // Translate the value if it exists in the map - if translatedValue, exists := translationMap[value]; exists { - filterGroups[i].FilterData[j] = translatedValue - } - } - } - - return filterGroups, nil -} - -// Generic function to filter JSON data dynamically based on key-value pairs -func filterDynamicChartData(chartData []map[string]interface{}, keyToFilter, valueToExclude string) []map[string]interface{} { - var filteredData []map[string]interface{} - - for _, item := range chartData { - obsKey, ok := item["ObsKey"].(map[string]interface{}) - if !ok { - // Skip items without "ObsKey" or malformed data - continue - } - - // Check if the key exists and matches the value to exclude - if obsKey[keyToFilter] == valueToExclude { - continue - } - - // Add item to filtered data - filteredData = append(filteredData, item) - } - - return filteredData -} - -func generateToken() string { - bytes := make([]byte, 32) - _, err := rand.Read(bytes) - if err != nil { - log.Fatal(err) - } - return hex.EncodeToString(bytes) -} - -var app *pocketbase.PocketBase - -func main() { - - // load .env file - err := godotenv.Load() - if err != nil { - log.Println("No .env file found") - } - - var baseUrl = os.Getenv("BASE_URL") - var liveBaseUrl = os.Getenv("LIVE_BASE_URL") - - app = pocketbase.New() - - 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)) - - // API endpoint: returns JSON policy - e.Router.GET("/api/privacy", func(c echo.Context) error { - policy := map[string]string{ - "title": "Privacy Policy", - "content": "We respect your privacy. Data is collected only for authentication and secure service delivery.", - } - return c.JSON(http.StatusOK, policy) - }) - - return nil - }) - - // Hook to send notifications when a new record is created - app.OnRecordAfterCreateRequest("notification").Add(func(e *core.RecordCreateEvent) error { - title_en := e.Record.GetString("title_en") - message_en := e.Record.GetString("message_en") - title_ar := e.Record.GetString("title_ar") - message_ar := e.Record.GetString("message_ar") - notificationID := e.Record.GetString("id") // Get the new notification ID - - // Initialize Firebase - opt := option.WithCredentialsFile("firebase-adminsdk.json") - firebaseApp, err := firebase.NewApp(context.Background(), nil, opt) - if err != nil { - log.Fatalf("Error initializing Firebase: %v", err) - } - - fcmClient, err := firebaseApp.Messaging(context.Background()) - if err != nil { - log.Fatalf("Error getting FCM client: %v", err) - } - - // Fetch users with valid device tokens - users, err := app.Dao().FindRecordsByExpr("users", nil) // Adjust collection name if different - if err != nil { - return err - } - - for _, user := range users { - token := user.GetString("device_token") - language := user.GetString("language") - if token != "" { - // tokens = append(tokens, token) - var message *messaging.Message - if language == "en" { - message = &messaging.Message{ - Token: token, - Notification: &messaging.Notification{ - Title: title_en, - Body: message_en, - }, - } - } else { - message = &messaging.Message{ - Token: token, - Notification: &messaging.Notification{ - Title: title_ar, - Body: message_ar, - }, - } - - } - - response, err := fcmClient.Send(context.Background(), message) - if err != nil { - - if err.Error() == "messaging/registration-token-not-registered" { - log.Println("⚠️ Device token is invalid. Remove it from the database.") - // Here you should remove the token from your database - } else { - log.Printf("Error sending message: %v", err) - } - } else { - fmt.Printf("✅ Successfully sent message: %s\n", response) - - // Update the pushed_notification field - var pushedNotifications []string - - // Get existing pushed_notification JSON field - pushedNotificationsJSON := user.GetString("pushed_notification") - if pushedNotificationsJSON != "" { - err := json.Unmarshal([]byte(pushedNotificationsJSON), &pushedNotifications) - if err != nil { - log.Printf("⚠️ Error parsing pushed_notification for user %s: %v", user.GetString("id"), err) - continue - } - } - - // Append the new notification ID if not already present - alreadyExists := false - for _, existingID := range pushedNotifications { - if existingID == notificationID { - alreadyExists = true - break - } - } - if !alreadyExists { - pushedNotifications = append(pushedNotifications, notificationID) - } - - // Convert back to JSON - updatedPushedNotifications, err := json.Marshal(pushedNotifications) - if err != nil { - log.Printf("⚠️ Error marshalling pushed_notification JSON: %v", err) - continue - } - - // Update the user record in the database - user.Set("pushed_notification", string(updatedPushedNotifications)) - if err := app.Dao().SaveRecord(user); err != nil { - log.Printf("❌ Error updating pushed_notification for user %s: %v", user.GetString("id"), err) - } else { - fmt.Printf("✅ Updated pushed_notification for user %s\n", user.GetString("id")) - } - } - - } - } - - return nil - }) - - app.OnBeforeServe().Add(func(e *core.ServeEvent) error { - - e.Router.GET("/api/getImagePath", func(c echo.Context) error { - - fileName := c.QueryParam("file") - if fileName == "" { - return c.JSON(http.StatusBadRequest, map[string]string{ - "error": "missing file parameter", - }) - } - - // Static files folder - staticPath := "./pb_public" - fullPath := filepath.Join(staticPath, fileName) - - // Check if file exists - if _, err := os.Stat(fullPath); os.IsNotExist(err) { - return c.JSON(http.StatusNotFound, map[string]string{ - "error": "file not found", - }) - } - - // Serve the file directly (browser will show the image) - return c.File(fullPath) - }) - - // e.Router.Static("/static", "./pb_public") - - e.Router.GET("/pw/auth/confirm-password-reset/:token", func(c echo.Context) error { - return c.File("pb_public/confirm_password_reset.html") - }) - - e.Router.GET("/api/auth/request-password-reset", func(c echo.Context) error { - - apiKey := c.Request().Header.Get("APP_SIGNATURE") - if apiKey != os.Getenv("APP_SIGNATURE") { - return c.JSON(http.StatusForbidden, map[string]string{"error": "Invalid API Key"}) - } - - email := c.QueryParam("email") - if email == "" { - return c.JSON(http.StatusBadRequest, map[string]string{"error": "Email is required"}) - } - - // Find user by email - user, err := app.Dao().FindFirstRecordByData("users", "email", email) - if err != nil { - return c.JSON(http.StatusNotFound, map[string]string{"error": "User not found"}) - } - - //check the user type - isOAuthLogin := user.GetString("is_oauth_login") - if isOAuthLogin != "0" { - return c.JSON(http.StatusOK, map[string]string{"status": "failed", "message": "Password reset is not available for accounts signed in with Apple or Google."}) - } - - name := user.GetString("name") - // Generate token - token := generateToken() - expiration := time.Now().Add(30 * time.Minute).UTC() - - // Store token in the database - user.Set("pw_reset_token", token) - user.Set("pw_reset_token_expiry", expiration) - - if err := app.Dao().SaveRecord(user); err != nil { - return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to update user record"}) - } - - // Send email - subject := "Reset your FCSC password" - // resetURL := fmt.Sprintf("%s/api/auth/reset-password?token=%s", baseUrl, token) - - resetURL := fmt.Sprintf("https://fcscapp.onelink.me/forgot-password/%s", email) - - body := fmt.Sprintf(` - - - - - - Registration Update – UAE Stats App - - - - - - -
- - - - - - - - - - - - - - - - -
- - إعادة تعيين كلمة المرور - تطبيق إحصاءات الإمارات العربية المتحدة
-

Reset your FCSC password -


-
- - - - - -
- Hello,

- Click on the link below to reset your password. -

- Reset Password -

- If you didn't ask to reset your password, you can ignore this email. -
- مرحبًا،

- يُرجى الضغط على الرابط أدناه لإعادة تعيين كلمة المرور الخاصة بك:

- إعادة تعيين كلمة المرور -

- إذا لم تطلب إعادة تعيين كلمة المرور، يمكنك تجاهل هذا البريد الإلكتروني. -
-
- We The UAE 2031 -
- تنبيه:
هذه الرسالة الإلكترونية و أيٌ من مرفقاتها قد تحتوي على معلومات سرية وهامة موجهه للشخص/الأشخاص المعنيين وعليه يرجى من المتلقي في حال تلقي الرسالة الإلكترونية عن طريق
الخطأ ولم يكن المعني بها، إخطار المرسل وحذفها من بريده الإلكتروني و كذلك إتلاف أي نسخ مطبوعة عنها حيث أنه يحظر عليه قراءة ونسخ ونشر أو توزيع أو استخدام هذه الرسالة
إلكترونية و أيٌ من مرفقاتها بأي شكل من الأشكال -
- Disclaimer:
The information contained in and transmitted with this e-mail message is PRIVILEGED AND/OR CONFIDENTIAL.If you are not the intended recipient, or have received the message by error, please notify the sender via E-Mail or over the telephone and delete this e-mail. You are not authorized to read, copy, disseminate, distribute or use this E-Mail or any of its attachments in any way. -
-
- - `, resetURL, resetURL) - - // Create the email message - message := &mailer.Message{ - From: mail.Address{ - Name: "FCSC", - Address: app.Settings().Meta.SenderAddress, - }, - To: []mail.Address{ - { - Name: name, - Address: email, - }, - }, - Subject: subject, - HTML: body, - } - - // Send the email - err = app.NewMailClient().Send(message) - if err != nil { - log.Printf("Failed to send verification email to user: %v", err) - return err - } - - return c.JSON(http.StatusOK, map[string]string{"status": "success", "message": "Reset link sent to your email"}) - }) - - e.Router.GET("/api/auth/reset-password", func(c echo.Context) error { - - apiKey := c.Request().Header.Get("APP_SIGNATURE") - if apiKey != os.Getenv("APP_SIGNATURE") { - return c.JSON(http.StatusForbidden, map[string]string{"error": "Invalid API Key"}) - } - - token := c.QueryParam("token") - if token == "" { - return c.JSON(http.StatusBadRequest, map[string]string{"error": "Token is required"}) - } - - // Find user with the token - user, err := app.Dao().FindFirstRecordByData("users", "pw_reset_token", token) - if err != nil { - return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Invalid token"}) - } - - // Convert PocketBase `types.DateTime` to `time.Time` - expiryTime := user.GetDateTime("pw_reset_token_expiry").Time() - log.Printf("expiryTime : '%v'", expiryTime) - log.Printf("time.Now : '%v'", time.Now().UTC()) - if expiryTime.Before(time.Now().UTC()) { - return c.File("pb_public/token_expired.html") - // return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Token expired"}) - } - - // return c.JSON(http.StatusOK, map[string]string{"message": "Token is valid"}) - - // Serve the reset page - return c.File("pb_public/confirm_password_reset.html") - }) - - e.Router.GET("/api/login_success", func(c echo.Context) error { - - apiKey := c.Request().Header.Get("APP_SIGNATURE") - if apiKey != os.Getenv("APP_SIGNATURE") { - return c.JSON(http.StatusForbidden, map[string]string{"error": "Invalid API Key"}) - } - - userID := c.QueryParam("id") - if userID == "" { - return c.JSON(http.StatusBadRequest, map[string]string{"error": "User ID is required"}) - } - - // Find user by ID - record, err := app.Dao().FindRecordById("users", userID) - if err != nil { - log.Printf("User not found: %v", err) - return c.JSON(http.StatusNotFound, map[string]string{"error": "User not found"}) - } - - // Get current login count and convert it properly - var loginCount int - if count, ok := record.Get("login_count").(float64); ok { - loginCount = int(count) // Convert float64 to int - } else { - loginCount = 0 // Default if not set - } - - // Increment login count - loginCount++ - record.Set("login_count", loginCount) - - // Save the updated record - if err := app.Dao().SaveRecord(record); err != nil { - log.Printf("Failed to update login count: %v", err) - return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to update login count"}) - } - - log.Printf("User %s login count updated to %d", userID, loginCount) - return c.JSON(http.StatusOK, map[string]interface{}{ - "message": "Success", - "login_count": loginCount, - }) - - }) - - e.Router.GET("/api/getNotification", func(c echo.Context) error { - - apiKey := c.Request().Header.Get("APP_SIGNATURE") - if apiKey != os.Getenv("APP_SIGNATURE") { - return c.JSON(http.StatusForbidden, map[string]string{"error": "Invalid API Key"}) - } - - language := c.QueryParam("language") - if language == "" { - return c.JSON(http.StatusBadRequest, map[string]string{"error": "language is required"}) - } - - categoryColumn := "category_" + language - titleColumn := "title_" + language - messageColumn := "message_" + language - - id := c.QueryParam("id") - - if id != "" { - // Define a single notification struct, not a slice - var notification struct { - Id string `db:"id" json:"id"` - Category string `db:"category" json:"category"` - Title string `db:"title" json:"title"` - Message string `db:"message" json:"message"` - Created string `db:"created" json:"created"` - } - - query := `SELECT id, created, ` + categoryColumn + ` AS category, ` + titleColumn + ` AS title, ` + messageColumn + ` AS message FROM notification WHERE id = {:id}` - err := app.DB().NewQuery(query).Bind(dbx.Params{"id": id}).One(¬ification) - - if err != nil { - log.Printf("notification not found: %v", err) - return c.JSON(http.StatusNotFound, map[string]string{"error": "notification not found"}) - } - - return c.JSON(http.StatusOK, map[string]interface{}{ - "message": "Success", - "data": notification, - }) - } - - // Fetch all notifications if no ID is provided - var notifications []struct { - Id string `db:"id" json:"id"` - Category string `db:"category" json:"category"` - Title string `db:"title" json:"title"` - Message string `db:"message" json:"message"` - Created string `db:"created" json:"created"` - } - - query := `SELECT id, created, ` + categoryColumn + ` AS category, ` + titleColumn + ` AS title, ` + messageColumn + ` AS message FROM notification` - err := app.DB().NewQuery(query).All(¬ifications) - - if err != nil { - log.Printf("notifications not found: %v", err) - return c.JSON(http.StatusNotFound, map[string]string{"error": "notifications not found"}) - } - - return c.JSON(http.StatusOK, map[string]interface{}{ - "message": "Success", - "data": notifications, - }) - }) - - e.Router.GET("/api/removeNotification", func(c echo.Context) error { - - apiKey := c.Request().Header.Get("APP_SIGNATURE") - if apiKey != os.Getenv("APP_SIGNATURE") { - return c.JSON(http.StatusForbidden, map[string]string{"error": "Invalid API Key"}) - } - - userID := c.QueryParam("user_id") - notificationID := c.QueryParam("notification_id") - - if userID == "" || notificationID == "" { - return c.JSON(http.StatusBadRequest, map[string]string{"error": "user_id and notification_id are required"}) - } - - // Fetch the user by ID - user, err := app.Dao().FindRecordById("users", userID) - if err != nil { - return c.JSON(http.StatusNotFound, map[string]string{"error": "user not found"}) - } - - var pushedNotifications []string - - // Get existing pushed_notification JSON field - pushedNotificationsJSON := user.GetString("pushed_notification") - if pushedNotificationsJSON != "" { - err := json.Unmarshal([]byte(pushedNotificationsJSON), &pushedNotifications) - if err != nil { - log.Printf("⚠️ Error parsing pushed_notification for user %s: %v", userID, err) - return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to parse pushed_notification"}) - } - } - - // Remove the notification ID if it exists - updatedNotifications := []string{} - for _, existingID := range pushedNotifications { - if existingID != notificationID { - updatedNotifications = append(updatedNotifications, existingID) - } - } - - // Convert updated array back to JSON - updatedJSON, err := json.Marshal(updatedNotifications) - if err != nil { - return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to update pushed_notification"}) - } - - // Update the user record with the new pushed_notification field - user.Set("pushed_notification", string(updatedJSON)) - if err := app.Dao().SaveRecord(user); err != nil { - return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to save updated notifications"}) - } - - return c.JSON(http.StatusOK, map[string]interface{}{ - "message": "success", - "pushed_notification": updatedNotifications, - }) - }) - - e.Router.GET("/api/pushNotification", func(c echo.Context) error { - - apiKey := c.Request().Header.Get("APP_SIGNATURE") - if apiKey != os.Getenv("APP_SIGNATURE") { - return c.JSON(http.StatusForbidden, map[string]string{"error": "Invalid API Key"}) - } - - // type Request struct { - // DeviceToken string `json:"device_token"` - // } - - // var requestData Request - // log.Printf("🔥 requestData: '%v'", requestData) - // // Validate required fields - // if requestData.DeviceToken == "" { - // return c.JSON(http.StatusBadRequest, map[string]string{"error": "DeviceToken is required"}) - // } - - // DeviceToken := requestData.DeviceToken - - DeviceToken := c.QueryParam("device_token") - - // Load Firebase credentials JSON file - opt := option.WithCredentialsFile("firebase-adminsdk.json") - app, err := firebase.NewApp(context.Background(), nil, opt) - if err != nil { - log.Printf("🔥 Error initializing Firebase App: '%v'", err) - } - - client, err := app.Messaging(context.Background()) - if err != nil { - log.Printf("Error getting Messaging client: %v", err) - } - - message := &messaging.Message{ - Token: DeviceToken, - Notification: &messaging.Notification{ - Title: "Hello!", - Body: "This is a test push notification By Gowtham.", - }, - } - - response, err := client.Send(context.Background(), message) - if err != nil { - - if err.Error() == "messaging/registration-token-not-registered" { - log.Println("⚠️ Device token is invalid. Remove it from the database.") - // Here you should remove the token from your database - } else { - log.Printf("Error sending message: %v", err) - } - } else { - fmt.Printf("✅ Successfully sent message: %s\n", response) - } - - return c.JSON(http.StatusOK, map[string]string{"status": "success"}) - }) - - e.Router.POST("/api/getDataSet", func(c echo.Context) error { - - apiKey := c.Request().Header.Get("APP_SIGNATURE") - if apiKey != os.Getenv("APP_SIGNATURE") { - return c.JSON(http.StatusForbidden, map[string]string{"error": "Invalid API Key"}) - } - - type RequestData struct { - Dataset string `json:"dataset"` - KPI string `json:"kpi"` - Language string `json:"language"` - ColorMode string `json:"color_mode"` - FilterData []utils.Filter `json:"filter_data"` - } - - var requestData RequestData - - // Bind JSON request to struct - if err := c.Bind(&requestData); err != nil { - return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request format"}) - } - - // Validate required fields - if requestData.Dataset == "" { - return c.JSON(http.StatusBadRequest, map[string]string{"error": "dataset is required"}) - } - - dataset := requestData.Dataset - language := requestData.Language - langKey := "value_" + language - colorMode := requestData.ColorMode - - // Fetch all matching records from charts table - param := dbx.NewExp("dataset = {:dataset}", dbx.Params{"dataset": dataset}) - records, err := app.Dao().FindRecordsByExpr("charts", param) - if err != nil { - log.Printf("Failed to fetch records for dataset '%s': %v", dataset, err) - return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to fetch records"}) - } - if len(records) == 0 { - return c.JSON(http.StatusNotFound, map[string]string{"error": "No records found for the given dataset"}) - } - - // Fetch all matching records from charts variables table - languageSource, err := app.Dao().FindRecordsByExpr("charts_variables", param) - if err != nil { - log.Printf("Failed to fetch language source: %v", err) - } - // Convert languageSource to []map[string]interface{} - languageSourceConverted := []map[string]interface{}{} - for _, record := range languageSource { - languageSourceConverted = append(languageSourceConverted, record.SchemaData()) - } - - //construct chart and card data based of configuration/calculation/default - var aggregatedResponse []map[string]interface{} - for _, record := range records { - - // Extract the URL for each record - apiURL := record.GetString("url") - file_name := record.GetString("kpi_file_name") - if apiURL == "" { - log.Printf("No URL for record '%v'", record) - continue // Skip records with no URL - } - - // Fetch and convert XML to JSON - saveToFile := record.GetBool("file_status") - chartId := record.GetString("id") - if !saveToFile { // Check if saveToFile is false - _, err := fetchAndConvertXMLToJSON(apiURL, "kpi_files", file_name, "charts", chartId) - if err != nil { - log.Printf("Failed to process URL '%s': %v", apiURL, err) - continue // Skip this record and proceed with others - } - } - - // Read the JSON data from the file - chartData, err := readJSONFromFile("kpi_files", file_name) - if err != nil { - log.Printf("Failed to read JSON for KPI '%s': %v", file_name, err) - } - - // unset key and value if value to be excluded - keyToFilter := record.GetString("unset_key") - valueToExclude := record.GetString("unset_value") - if keyToFilter == "" || valueToExclude == "" { - // fmt.Println("unset_key or unset_value is empty. Skipping processing.") - } else { - // Filter the data dynamically and update chartData - chartData = filterDynamicChartData(chartData, keyToFilter, valueToExclude) - } - - //logo url generating - var logoURL string - cardLogo := record.GetString("card_logo") - if cardLogo == "" { - logoURL = "" - } else { - logoURL = fmt.Sprintf("%s/api/files/charts/%s/%s", - baseUrl, - record.GetString("id"), - cardLogo, - ) - } - - cResRaw := utils.TranslateChartData(chartData, languageSourceConverted, langKey) - - //responce data structure - chartHeadingKey := "chart_heading_" + language - chartSubHeadingKey := "chart_sub_heading_" + language - tabHeadingKey := "tab_heading_" + language - // Correct conditional assignment for "response" - recordResult := map[string]interface{}{ - "url": apiURL, - "dataset": record.GetString("dataset"), - "main_id": record.GetString("main_id"), - "sub_id": record.GetString("sub_id"), - "kpi": record.GetString("kpi"), - "is_chart": record.GetString("is_chart"), - "chart_type": record.GetString("chart_type"), - "group_by": record.GetString("group_by"), - "chart_heading": record.GetString(chartHeadingKey), - "chart_sub_heading": record.GetString(chartSubHeadingKey), - "tab_heading": record.GetString(tabHeadingKey), - "unset_value": record.Get("unset_value"), - "card_logo": logoURL, - "order_id": record.Get("order_id"), - "card_type_json": record.Get("card_type_json"), - "tab_order": record.GetString("tab_order"), - "chart_type_json": record.Get("chart_type_json"), - "card_full_length": record.Get("card_full_length"), - "chart_bar_color": record.Get("chart_bar_color"), - "chart_height": record.Get("chart_height"), - "chart_label_width": record.Get("chart_label_width"), - } - - var cRes []utils.Observation - cResJSON, err := json.Marshal(cResRaw) - if err != nil { - fmt.Println("Error encoding chart data:", err) - } - err = json.Unmarshal(cResJSON, &cRes) - if err != nil { - fmt.Println("Error decoding chart data:", err) - } - - if requestData.KPI == record.GetString("kpi") && len(requestData.FilterData) > 0 { - - filteredData, err := utils.FilterByMultipleKeys(cRes, requestData.FilterData) - if err != nil { - log.Fatal(err) - } - if 0 < len(filteredData) { - cRes = filteredData - } else { - defaultYearsFilteredData, err := utils.DefaultYearFilter(cRes, record.GetString("default_latest_year_count")) - if err != nil { - fmt.Println("Error processing defaultYearsFilteredData:", err) - } - cRes = defaultYearsFilteredData - - } - - } else { - defaultYearsFilteredData, err := utils.DefaultYearFilter(cRes, record.GetString("default_latest_year_count")) - if err != nil { - fmt.Println("Error processing defaultYearsFilteredData:", err) - } - // fmt.Println("defaultYearsFilteredData:", defaultYearsFilteredData) - cRes = defaultYearsFilteredData - } - - var cardResults []map[string]string - if record.GetString("is_chart") == "false" { // card data calculations - - // Extract `card_type` JSON from `record.Get("card_type")` - requestData := record.Get("card_type_json") - requestJSON, err := json.Marshal(requestData) // Convert interface{} to JSON string - if err != nil { - fmt.Println("Error encoding request data:", err) - } - - // Decode JSON into an array of `utils.Request` - var requests []utils.CardCalculationRequest - err = json.Unmarshal(requestJSON, &requests) - if err != nil { - fmt.Println("Error decoding request data:", err) - } - - if record.GetString("custom_calculation") == "true" { - - customCalculationReq := record.Get("chart_custom_calculation_json") - - var chartCustomCalculationRequest utils.ChartCalculationRequest - - jsonData, err := json.Marshal(customCalculationReq) - if err != nil { - log.Println("Error marshaling to JSON:", err) - } - - err = json.Unmarshal(jsonData, &chartCustomCalculationRequest) - if err != nil { - log.Println("Error unmarshalling JSON:", err) - } - customResult, err := utils.CalculateGroupedSum(cRes, chartCustomCalculationRequest) - if err != nil { - fmt.Println("Error:", err) - } - - cRes = customResult - - } - - // Process each request and store results - for _, req := range requests { - result, err := utils.CardCalculation(cRes, req) - if err != nil { - fmt.Println("Error processing data:", err) - continue // Skip this request and move to the next - } - cardResults = append(cardResults, result) - } - - recordResult["response"] = cardResults - - } else { - - if record.GetString("custom_calculation") == "false" { - recordResult["response"] = cRes - } else { - - // fmt.Println("cRes-------", cRes) - - customCalculationReq := record.Get("chart_custom_calculation_json") - - var chartCustomCalculationRequest utils.ChartCalculationRequest - - jsonData, err := json.Marshal(customCalculationReq) - if err != nil { - log.Println("Error marshaling to JSON:", err) - } - - err = json.Unmarshal(jsonData, &chartCustomCalculationRequest) - if err != nil { - log.Println("Error unmarshalling JSON:", err) - } - customResult, err := utils.CalculateGroupedSum(cRes, chartCustomCalculationRequest) - if err != nil { - fmt.Println("Error:", err) - } - - recordResult["response"] = customResult - - } - - } - - // Add the result to the aggregated response - aggregatedResponse = append(aggregatedResponse, recordResult) - - } - //-------------------- - - // get all filter based on kpi - var dataSetKpis []struct { - Kpi string `db:"kpi" json:"kpi"` - } - err = app.DB().Select("kpi").From("charts").Where(dbx.HashExp{"dataset": dataset}).GroupBy("kpi").All(&dataSetKpis) - if err != nil { - log.Fatalf("Failed to get kpi groupby data: %v", err) - } - - aggregatedFilterResponse := make(map[string]interface{}) - for _, dataSetKpi := range dataSetKpis { - - kpi := dataSetKpi.Kpi - filterData, err := fetchfilterJSON(kpi) - - // Translate filter data using the desired dataset and language key - fRes, err := TranslateFilters(app, dataset, filterData, langKey) - if err != nil { - log.Fatalf("Error translating filters: %v", err) - } - - aggregatedFilterResponse[kpi] = fRes - - } - //-------------------- - - //mainTopic kpi heading and screen color - headingAndColor := struct { - DataSet string `db:"data_set" json:"data_set"` - MainTopic string `db:"main_topic" json:"main_topic"` - SubTopic string `db:"sub_topic" json:"sub_topic"` - DataSetTileHeading string `db:"data_set_tile_heading" json:"data_set_tile_heading"` - HeaderColor string `db:"header_color" json:"header_color"` - BodyColor string `db:"body_color" json:"body_color"` - BorderColor string `db:"border_color" json:"border_color"` - }{} - - header_color := "header_color_" + colorMode - body_color := "body_color_" + colorMode - border_color := "border_color_" + colorMode - data_set_tile_heading := "data_set_tile_heading_" + language - main_topic := "main_topic_" + language - sub_topic := "sub_topic_" + language - query := ` SELECT id,data_set,` + data_set_tile_heading + ` AS data_set_tile_heading,` + main_topic + ` AS main_topic,` + sub_topic + ` AS sub_topic, ` + header_color + ` AS header_color,` + body_color + ` AS body_color, ` + border_color + ` AS border_color FROM uae_numbers_screen WHERE data_set = {:data_set} ` - err = app.DB().NewQuery(query).Bind(dbx.Params{"data_set": dataset}).One(&headingAndColor) - if err != nil { - log.Printf("Failed to fetch data-set for sub topic %s: %v", dataset, err) - } - //--------------------- - - // Create the response structure - response := map[string]interface{}{ - "new_filter_data": aggregatedFilterResponse, - "data": aggregatedResponse, - "screen_heading_and_color": headingAndColor, - "kpi": requestData.KPI, - "filter_data": requestData.FilterData, - } - - if len(aggregatedResponse) == 0 { - return c.JSON(http.StatusInternalServerError, map[string]string{"error": "No data could be fetched from the provided URLs"}) - } - - return c.JSON(http.StatusOK, response) - }) - - e.Router.GET("/api/getHomePageData", func(c echo.Context) error { - - apiKey := c.Request().Header.Get("APP_SIGNATURE") - if apiKey != os.Getenv("APP_SIGNATURE") { - return c.JSON(http.StatusForbidden, map[string]string{"error": "Invalid API Key"}) - } - - language := c.QueryParam("language") - colorMode := c.QueryParam("color_mode") - - // Determine the column name dynamically - colorPatternColumn := "color_pattern_" + colorMode - - // Define the struct for holding the query results - mainTopics := []struct { - MainTopicEn string `db:"main_topic_en" json:"main_topic_en"` - MainTopicAr string `db:"main_topic_ar" json:"main_topic_ar"` - MainTopicListOrder string `db:"main_topic_list_order" json:"main_topic_list_order"` - ColorPattern string `db:"color_pattern" json:"color_pattern"` - }{} - - // Execute the query - query := fmt.Sprintf(` - SELECT - main_topic_en, - main_topic_ar, - main_topic_list_order, - %s AS color_pattern - FROM home_screen - GROUP BY main_topic_en, main_topic_ar, main_topic_list_order, %s - ORDER BY main_topic_list_order ASC - `, colorPatternColumn, colorPatternColumn) - - err := app.DB().NewQuery(query).All(&mainTopics) - - // err := app.DB(). - // Select("main_topic_en", "main_topic_ar", "main_topic_list_order", "color_pattern"). - // From("home_screen"). - // GroupBy("main_topic_en", "main_topic_ar", "color_pattern"). - // OrderBy("main_topic_list_order ASC"). - // All(&mainTopics) - - if err != nil { - log.Printf("Failed to fetch home_screen data: %v", err) - return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to fetch records"}) - } - - // If no records are found - if len(mainTopics) == 0 { - return c.JSON(http.StatusNotFound, map[string]string{"error": "No records found for home screen"}) - } - - // Final result structure - result := []map[string]interface{}{} - - // Loop through main topics and fetch sub-topic data for each - for _, mainTopic := range mainTopics { - - dataSets := []struct { - DataSet string `db:"data_set" json:"data_set"` - DataSetTileHeading string `db:"data_set_tile_heading" json:"data_set_tile_heading"` - ValueSource string `db:"value_source" json:"value_source"` - Value string `db:"value" json:"value"` - DataSetListOrder string `db:"data_set_list_order" json:"data_set_list_order"` - }{} - - languageSpecificColumn := "data_set_tile_heading_" + language - query := ` SELECT data_set,` + languageSpecificColumn + ` AS data_set_tile_heading,value_source,value, data_set_list_order FROM home_screen WHERE main_topic_en = {:topic}` - err := app.DB(). - NewQuery(query). - Bind(dbx.Params{ - "topic": mainTopic.MainTopicEn, - }). - All(&dataSets) - - if err != nil { - log.Printf("Error fetching data sets: %v", err) - continue - } - - var MainTopic string - if language == "en" { - MainTopic = mainTopic.MainTopicEn - } else { - MainTopic = mainTopic.MainTopicAr - } - - // Add the main topic and its sub-topics to the result - result = append(result, map[string]interface{}{ - "main_topic": MainTopic, - "main_topic_list_order": mainTopic.MainTopicListOrder, - "color_pattern": mainTopic.ColorPattern, - "tile_data": dataSets, - }) - } - - // Send the combined result as JSON - return c.JSON(http.StatusOK, result) - - }) - - e.Router.GET("/api/getUAENumbersData", func(c echo.Context) error { - - apiKey := c.Request().Header.Get("APP_SIGNATURE") - if apiKey != os.Getenv("APP_SIGNATURE") { - return c.JSON(http.StatusForbidden, map[string]string{"error": "Invalid API Key"}) - } - - language := c.QueryParam("language") - colorMode := c.QueryParam("color_mode") - - // Determine the column name dynamically - colorPatternColumn := "color_pattern_" + colorMode - - // Define the struct for holding the query results - mainTopics := []struct { - MainTopicEn string `db:"main_topic_en" json:"main_topic_en"` - MainTopicAr string `db:"main_topic_ar" json:"main_topic_ar"` - MainTopicListOrder string `db:"main_topic_list_order" json:"main_topic_list_order"` - ColorPattern string `db:"color_pattern" json:"color_pattern"` - }{} - - // Execute the query - - // Execute the query - query := fmt.Sprintf(` - SELECT - main_topic_en, - main_topic_ar, - main_topic_list_order, - %s AS color_pattern - FROM uae_numbers_screen - GROUP BY main_topic_en, main_topic_ar, %s - ORDER BY main_topic_list_order ASC - `, colorPatternColumn, colorPatternColumn) - - err := app.DB().NewQuery(query).All(&mainTopics) - - // err := app.DB(). - // Select("main_topic_en", "main_topic_ar", "main_topic_list_order", "color_pattern"). - // From("uae_numbers_screen"). - // GroupBy("main_topic_en", "main_topic_ar", "color_pattern"). - // OrderBy("main_topic_list_order ASC"). - // All(&mainTopics) - - if err != nil { - log.Printf("Failed to fetch uae_numbers_screen data: %v", err) - return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to fetch records"}) - } - - // If no records are found - if len(mainTopics) == 0 { - return c.JSON(http.StatusNotFound, map[string]string{"error": "No records found for home screen"}) - } - - // Final result structure - result := []map[string]interface{}{} - - // Loop through main topics and fetch sub-topic data for each - for _, mainTopic := range mainTopics { - - subTopics := []struct { - SubTopicEn string `db:"sub_topic_en" json:"sub_topic_en"` - SubTopicAr string `db:"sub_topic_ar" json:"sub_topic_ar"` - SubTopicListOrder string `db:"sub_topic_list_order" json:"sub_topic_list_order"` - }{} - - // Query to get sub-topics for the current main topic - err := app.DB(). - Select("sub_topic_en", "sub_topic_ar", "sub_topic_list_order"). - From("uae_numbers_screen"). - Where(dbx.HashExp{"main_topic_en": mainTopic.MainTopicEn}). - GroupBy("sub_topic_en", "sub_topic_ar"). - OrderBy("sub_topic_list_order ASC"). - All(&subTopics) - - if err != nil { - log.Printf("Failed to fetch sub-topics for main topic %s: %v", mainTopic.MainTopicEn, err) - continue - } - - result2 := []map[string]interface{}{} - // Loop through sub Topics topics and fetch dataSet data for each - for _, subTopic := range subTopics { - - dataSets := []struct { - DataSet string `db:"data_set" json:"data_set"` - DataSetTileHeading string `db:"data_set_tile_heading" json:"data_set_tile_heading"` - ValueSource string `db:"value_source" json:"value_source"` - Value string `db:"value" json:"value"` - DataSetListOrder string `db:"data_set_list_order" json:"data_set_list_order"` - }{} - - languageSpecificColumn := "data_set_tile_heading_" + language - query := ` SELECT data_set,` + languageSpecificColumn + ` AS data_set_tile_heading,value_source,value, data_set_list_order FROM uae_numbers_screen WHERE sub_topic_en = {:topic}` - err := app.DB(). - NewQuery(query). - Bind(dbx.Params{ - "topic": subTopic.SubTopicEn, - }). - All(&dataSets) - - // languageSpecificColumn := "data_set_tile_heading_" + language - // // Query to get data-set for the current sub topic - // err := app.DB(). - // Select("data_set", languageSpecificColumn, "value_source", "value", "data_set_list_order"). - // From("uae_numbers_screen"). - // Where(dbx.HashExp{"sub_topic_en": subTopic.SubTopicEn}). - // OrderBy("data_set_list_order ASC"). - // All(&dataSets) - - if err != nil { - log.Printf("Failed to fetch data-set for sub topic %s: %v", subTopic.SubTopicEn, err) - continue - } - - var SubTopic string - if language == "en" { - SubTopic = subTopic.SubTopicEn - } else { - SubTopic = subTopic.SubTopicAr - } - - result2 = append(result2, map[string]interface{}{ - "sub_topic": SubTopic, - "sub_topic_list_order": subTopic.SubTopicListOrder, - "tile_data": dataSets, - }) - } - - var MainTopic string - if language == "en" { - MainTopic = mainTopic.MainTopicEn - } else { - MainTopic = mainTopic.MainTopicAr - } - - // Add the main topic and its sub-topics to the result - result = append(result, map[string]interface{}{ - "main_topic": MainTopic, - "main_topic_list_order": mainTopic.MainTopicListOrder, - "color_pattern": mainTopic.ColorPattern, - "sub_topics": result2, - }) - } - - // Send the combined result as JSON - return c.JSON(http.StatusOK, result) - - }) - - e.Router.GET("/api/getUserBookmark", func(c echo.Context) error { - - apiKey := c.Request().Header.Get("APP_SIGNATURE") - if apiKey != os.Getenv("APP_SIGNATURE") { - return c.JSON(http.StatusForbidden, map[string]string{"error": "Invalid API Key"}) - } - - language := c.QueryParam("language") - user_id := c.QueryParam("user_id") - colorMode := c.QueryParam("color_mode") - - // Define the struct for holding the query results - bookmarks := []struct { - Id string `db:"id" json:"id"` - UserId string `db:"user_id" json:"user_id"` - Dataset string `db:"dataset" json:"dataset"` - }{} - - // Execute the query - err := app.DB(). - Select("id", "user_id", "dataset"). - From("bookmark"). - Where(dbx.HashExp{"user_id": user_id}). - All(&bookmarks) - // return c.JSON(http.StatusOK, bookmarks) - if err != nil { - log.Printf("Failed to fetch bookmark data: %v", err) - return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to fetch records"}) - } - - // If no records are found - if len(bookmarks) == 0 { - return c.JSON(http.StatusNotFound, map[string]string{"error": "No records found for Bookmark"}) - } - - // Final result structure - result := []map[string]interface{}{} - - // Loop through main topics and fetch sub-topic data for each - for _, bookmark := range bookmarks { - dataSets := struct { - DataSet string `db:"data_set" json:"data_set"` - MainTopic string `db:"main_topic" json:"main_topic"` - SubTopic string `db:"sub_topic" json:"sub_topic"` - DataSetTileHeading string `db:"data_set_tile_heading" json:"data_set_tile_heading"` - ValueSource string `db:"value_source" json:"value_source"` - Value string `db:"value" json:"value"` - ColorPattern string `db:"color_pattern" json:"color_pattern"` - }{} - - data_set_tile_heading := "data_set_tile_heading_" + language - main_topic := "main_topic_" + language - sub_topic := "sub_topic_" + language - color_pattern := "color_pattern_" + colorMode - query := ` SELECT id,data_set,` + data_set_tile_heading + ` AS data_set_tile_heading,` + main_topic + ` AS main_topic,` + sub_topic + ` AS sub_topic,` + color_pattern + ` AS color_pattern,value_source,value FROM uae_numbers_screen WHERE data_set = {:bookmark} ` - err := app.DB(). - NewQuery(query). - Bind(dbx.Params{ - "bookmark": bookmark.Dataset, - }). - One(&dataSets) - - log.Printf("query %s: %v", bookmark.Dataset, query) - if err != nil { - log.Printf("Failed to fetch data-set for sub topic %s: %v", bookmark.Dataset, err) - continue - } - - // Add the main topic and its sub-topics to the result - result = append(result, map[string]interface{}{ - "id": bookmark.Id, - "data_set": dataSets.DataSet, - "data_set_tile_heading": dataSets.DataSetTileHeading, - "value_source": dataSets.ValueSource, - "value": dataSets.Value, - "main_topic": dataSets.MainTopic, - "sub_topic": dataSets.SubTopic, - "color_pattern": dataSets.ColorPattern, - }) - - } - - // Send the combined result as JSON - return c.JSON(http.StatusOK, result) - - }) - - type DownloadRequest struct { - Username string `json:"username"` - Password string `json:"password"` - Collection string `json:"collection"` - SyncWithLive bool `json:"syncWithLive"` - LiveUsername string `json:"liveUsername"` - LivePassword string `json:"livePassword"` - } - - type AdminLoginResponse struct { - Token string `json:"token"` - Admin map[string]interface{} `json:"admin"` - } - - type DownloadResponse struct { - Collection string `json:"collection"` - Records []map[string]interface{} `json:"records"` - } - - e.Router.POST("/api/downloadWithLogin", func(c echo.Context) error { - reqBody := new(DownloadRequest) - if err := c.Bind(reqBody); err != nil { - return c.JSON(400, map[string]string{"error": "invalid request"}) - } - - if reqBody.Username == "" || reqBody.Password == "" || reqBody.Collection == "" { - return c.JSON(400, map[string]string{"error": "username, password, and collection are required"}) - } - - // 1️⃣ Login to PocketBase - loginPayload := map[string]string{ - "identity": reqBody.Username, - "password": reqBody.Password, - } - data, _ := json.Marshal(loginPayload) - - loginReq, _ := http.NewRequest("POST", fmt.Sprintf("%s/api/admins/auth-with-password", baseUrl), bytes.NewBuffer(data)) - loginReq.Header.Set("Content-Type", "application/json") - - loginResp, err := http.DefaultClient.Do(loginReq) - if err != nil { - return c.JSON(500, map[string]string{"error": "failed to connect to PB"}) - } - defer loginResp.Body.Close() - - loginBody, _ := ioutil.ReadAll(loginResp.Body) - - if loginResp.StatusCode != 200 { - return c.JSON(401, map[string]string{"error": "invalid admin credentials", "details": string(loginBody)}) - } - - var loginRes AdminLoginResponse - if err := json.Unmarshal(loginBody, &loginRes); err != nil { - return c.JSON(500, map[string]string{"error": "failed to parse login response"}) - } - - // 2️⃣ Use token to fetch records - url := fmt.Sprintf("%s/api/collections/%s/records?perPage=500", baseUrl, reqBody.Collection) - fmt.Println("Fetching collection from URL:", url) - - req, _ := http.NewRequest("GET", url, nil) - req.Header.Set("Authorization", loginRes.Token) - fmt.Println("Using token:", loginRes.Token) - - resp, err := http.DefaultClient.Do(req) - if err != nil { - fmt.Println("Error fetching collection:", err) - return c.JSON(500, map[string]string{"error": "failed to fetch collection"}) - } - defer resp.Body.Close() - - body, _ := ioutil.ReadAll(resp.Body) - fmt.Println("HTTP Status Code:", resp.StatusCode) - - // parse JSON - var parsed map[string]interface{} - if err := json.Unmarshal(body, &parsed); err != nil { - fmt.Println("JSON Unmarshal error:", err) - return c.JSON(500, map[string]string{"error": "invalid JSON from PocketBase"}) - } - - items, ok := parsed["items"].([]interface{}) - if !ok { - return c.JSON(500, map[string]string{"error": "no items found"}) - } - - records := []map[string]interface{}{} - for _, i := range items { - rec := i.(map[string]interface{}) - records = append(records, rec) - } - - syncResult := map[string]interface{}{"synced": []string{}, "errors": []string{}} - - // 3️⃣ If syncWithLive=true, sync to live PB - if reqBody.SyncWithLive { - if reqBody.LiveUsername == "" || reqBody.LivePassword == "" { - return c.JSON(400, map[string]string{"error": "live username and password required for sync"}) - } - - // Login to live - livePayload := map[string]string{ - "identity": reqBody.LiveUsername, - "password": reqBody.LivePassword, - } - data, _ := json.Marshal(livePayload) - liveReq, _ := http.NewRequest("POST", fmt.Sprintf("%s/api/admins/auth-with-password", liveBaseUrl), bytes.NewBuffer(data)) - liveReq.Header.Set("Content-Type", "application/json") - liveResp, err := http.DefaultClient.Do(liveReq) - fmt.Println("Live url:", liveBaseUrl) - fmt.Println("Live req:", liveReq) - if err != nil { - return c.JSON(500, map[string]string{"error": "failed to connect to Live PB"}) - } - defer liveResp.Body.Close() - - liveBody, _ := ioutil.ReadAll(liveResp.Body) - if liveResp.StatusCode != 200 { - return c.JSON(401, map[string]string{"error": "invalid live admin credentials", "details": string(liveBody)}) - } - - var liveLogin AdminLoginResponse - if err := json.Unmarshal(liveBody, &liveLogin); err != nil { - return c.JSON(500, map[string]string{"error": "failed to parse live login response"}) - } - - // Sync each record - for _, rec := range records { - id := rec["id"].(string) - data, _ := json.Marshal(rec) - - // Try PATCH first - patchURL := fmt.Sprintf("%s/api/collections/%s/records/%s", liveBaseUrl, reqBody.Collection, id) - req, _ := http.NewRequest("PATCH", patchURL, bytes.NewBuffer(data)) - req.Header.Set("Authorization", liveLogin.Token) - req.Header.Set("Content-Type", "application/json") - - resp, err := http.DefaultClient.Do(req) - if err != nil { - syncResult["errors"] = append(syncResult["errors"].([]string), fmt.Sprintf("%s: %s", id, err.Error())) - continue - } - defer resp.Body.Close() - - if resp.StatusCode == 404 { - // Create instead - postURL := fmt.Sprintf("%s/api/collections/%s/records", liveBaseUrl, reqBody.Collection) - req, _ := http.NewRequest("POST", postURL, bytes.NewBuffer(data)) - req.Header.Set("Authorization", liveLogin.Token) - req.Header.Set("Content-Type", "application/json") - - resp, err = http.DefaultClient.Do(req) - if err != nil { - syncResult["errors"] = append(syncResult["errors"].([]string), fmt.Sprintf("%s: %s", id, err.Error())) - continue - } - defer resp.Body.Close() - - if resp.StatusCode >= 300 { - body, _ := ioutil.ReadAll(resp.Body) - syncResult["errors"] = append(syncResult["errors"].([]string), fmt.Sprintf("%s: create failed: %s", id, string(body))) - continue - } - } - - syncResult["synced"] = append(syncResult["synced"].([]string), id) - } - } - - // 4️⃣ Return dev records + optional sync result - return c.JSON(200, map[string]interface{}{ - "collection": reqBody.Collection, - "records": records, - "sync": syncResult, - }) - }) - - return nil - }) - - //send mail while status changed from app admin - app.OnRecordBeforeUpdateRequest().Add(func(e *core.RecordUpdateEvent) error { - if e.Collection.Name == "users" { - originalRecord, err := app.Dao().FindRecordById("users", e.Record.GetString("id")) - if err != nil { - return fmt.Errorf("failed to fetch original record: %v", err) - } - - oldStatus := originalRecord.GetString("status") - newStatus := e.Record.GetString("status") - - // Check if the status has changed - if oldStatus != newStatus { - err := sendStatusChangeEmail(app, e.Record, newStatus) - if err != nil { - return err - } - } - } - return nil - }) - - //Hook to send a verification link to register user - app.OnRecordAfterCreateRequest().Add(func(e *core.RecordCreateEvent) error { - if e.Record.Collection().Name == "users" { - // Retrieve user details - name := e.Record.GetString("uname") - email := e.Record.GetString("email") - userID := e.Record.Id // User ID - - // imageRecord, err := app.Dao().FindFirstRecordByData("images", "file_name", "app_logo") - // if err != nil { - // log.Printf("Failed to fetch logo from images collection: %v", err) - // return nil - // } - - // Construct the logo URL - // logoURL := fmt.Sprintf("%s/api/files/images/%s/%s", - // baseUrl, - // imageRecord.Id, - // imageRecord.GetString("file"), - // ) - - // Email subject - subject := "FCSC Verify Your Email to Complete Registration" - - // Generate a verification URL (example: using user ID or token) - verificationURL := fmt.Sprintf("%s/verify-email?userId=%s", baseUrl, userID) - - // Email body - body := fmt.Sprintf(` - - - - - - Registration Update – UAE Stats App - - - - - - -
- - - - - - - - - - - - - - - - -
- - تطبيق إحصاءات الإمارات العربية المتحدة - يرجى التحقق من عنوان بريدك الإلكتروني
-

Please Verify Your Email Address – UAE Stats App -


-
- - - - - -
- Dear %s,

- Thank you for registering. Please click the link below to verify your email address: -

- Verify Email -

- If you did not register for this account, please ignore this email. -
- عزيزي %s،

- شكرًا لتسجيلك. يُرجى النقر على الرابط أدناه للتحقق من عنوان بريدك الإلكتروني: -

- تحقق من البريد الإلكتروني -

- إذا لم تكن قد قمت بتسجيل هذا الحساب، يُرجى تجاهل هذا البريد الإلكتروني. -
-
- We The UAE 2031 -
- تنبيه:
هذه الرسالة الإلكترونية و أيٌ من مرفقاتها قد تحتوي على معلومات سرية وهامة موجهه للشخص/الأشخاص المعنيين وعليه يرجى من المتلقي في حال تلقي الرسالة الإلكترونية عن طريق
الخطأ ولم يكن المعني بها، إخطار المرسل وحذفها من بريده الإلكتروني و كذلك إتلاف أي نسخ مطبوعة عنها حيث أنه يحظر عليه قراءة ونسخ ونشر أو توزيع أو استخدام هذه الرسالة
إلكترونية و أيٌ من مرفقاتها بأي شكل من الأشكال -
- Disclaimer:
The information contained in and transmitted with this e-mail message is PRIVILEGED AND/OR CONFIDENTIAL.If you are not the intended recipient, or have received the message by error, please notify the sender via E-Mail or over the telephone and delete this e-mail. You are not authorized to read, copy, disseminate, distribute or use this E-Mail or any of its attachments in any way. -
-
- - - `, name, verificationURL, name, verificationURL) - - // Create the email message - message := &mailer.Message{ - From: mail.Address{ - Name: "FCSC", - Address: app.Settings().Meta.SenderAddress, - }, - To: []mail.Address{ - { - Name: name, - Address: email, - }, - }, - Subject: subject, - HTML: body, - } - - // Send the email - err := app.NewMailClient().Send(message) - if err != nil { - log.Printf("Failed to send verification email to user: %v", err) - return err - } - - log.Println("Verification email sent successfully to the user.") - } - return nil - }) - - // Hook to send an email after a user is created - app.OnBeforeServe().Add(func(e *core.ServeEvent) error { - - e.Router.GET("/api/custom/approve", func(c echo.Context) error { - userId := c.QueryParam("userId") - if userId == "" { - return c.JSON(400, map[string]interface{}{ - "code": 400, - "message": "Missing userId query parameter.", - }) - } - - // Find the user record - record, err := app.Dao().FindRecordById("users", userId) - if err != nil { - return c.JSON(500, map[string]interface{}{ - "code": 500, - "message": "Error finding user.", - }) - } - - // Check if the email is already verified - if record.GetBool("reviewed") { - return c.HTML(http.StatusOK, ` - - - - - -

-
-

Your Already Reviewed

-

You have already been reviewed. No further action is required.

-
- - - `) - } - - // Update the status - newStatus := "Approved" - record.Set("verified", true) - record.Set("status", newStatus) - record.Set("reviewed", true) - if err := app.Dao().SaveRecord(record); err != nil { - return c.JSON(500, map[string]interface{}{ - "code": 500, - "message": "Failed to verify user.", - }) - } - - // Send the status change email - err = sendStatusChangeEmail(app, record, newStatus) - if err != nil { - return c.JSON(500, map[string]interface{}{ - "code": 500, - "message": "Failed to send approval email.", - }) - } - - return c.HTML(http.StatusOK, ` - - - - - -
- -

User Approved Successfully

-

The user has been approved, and the approval email has been sent.

-
- - - `) - }) - - return nil - }) - - app.OnBeforeServe().Add(func(e *core.ServeEvent) error { - - // Approve Endpoint - e.Router.GET("/api/custom/reject", func(c echo.Context) error { - userId := c.QueryParam("userId") - if userId == "" { - return c.JSON(400, map[string]interface{}{ - "code": 400, - "message": "Missing userId query parameter.", - }) - } - - // Find the user record in PocketBase by userId - record, err := app.Dao().FindRecordById("users", userId) - if err != nil { - return c.JSON(500, map[string]interface{}{ - "code": 500, - "message": "Error finding user.", - }) - } - - // Check if the email is already verified - if record.GetBool("reviewed") { - return c.HTML(http.StatusOK, ` - - - - - -
-
-

Your Already Reviewed

-

You have already been reviewed. No further action is required.

-
- - - `) - } - - // Check if the status is already "Denied" to avoid duplicate email - if record.GetString("status") == "Denied" { - return c.JSON(400, map[string]interface{}{ - "code": 400, - "message": "User is already rejected.", - }) - } - - // Update the status - newStatus := "Denied" - record.Set("verified", false) - record.Set("status", newStatus) - record.Set("reviewed", true) - if err := app.Dao().SaveRecord(record); err != nil { - return c.JSON(500, map[string]interface{}{ - "code": 500, - "message": "Failed to verify user.", - }) - } - - // Send the status change email - err = sendStatusChangeEmail(app, record, newStatus) - if err != nil { - return c.JSON(500, map[string]interface{}{ - "code": 500, - "message": "Failed to send rejection email.", - }) - } - - // Render a success message as an HTML response - return c.HTML(http.StatusOK, ` - - - - - - User Rejected - - - -
- -

User Rejected

-

Rejection email sent successfully.

-
- - - `) - }) - - return nil - }) - - // Serve static files from the provided public directory (if exists) - app.OnBeforeServe().Add(func(e *core.ServeEvent) error { - // e.Router.GET("/verify-email", verifyEmailHandler) - - e.Router.GET("/verify-email", func(c echo.Context) error { - return verifyEmailHandler(c, baseUrl) - }) - e.Router.GET("/*", apis.StaticDirectoryHandler(os.DirFS("./pb_public"), false)) - return nil - }) - - //Feedback Mail Trigger - app.OnRecordAfterCreateRequest().Add(func(e *core.RecordCreateEvent) error { - if e.Record.Collection().Name == "feedback" { - // Retrieve feedback details - feedback := e.Record - userId := feedback.GetString("userId") - emojiRating := feedback.GetString("emoji_rating") - easeOfUse := feedback.GetString("ease_of_use") - quality := feedback.GetString("quality") - design := feedback.GetString("design") - redundancy := feedback.GetString("redundancy") - additionalFeedback := feedback.GetString("feedback") - - if additionalFeedback == "" { - additionalFeedback = "No additional feedback provided." - } - - // Fetch user details using userId - user, err := app.Dao().FindRecordById("users", userId) - if err != nil { - log.Printf("Error fetching user details: %v\n", err) - return err - } - - userName := user.GetString("uname") - userEmail := user.GetString("email") - - // Extract and format the creation time - createdTime := e.Record.Get("created").(types.DateTime) // Extract created time - feedbackDateTime := createdTime.Time().UTC() // Convert to time.Time in UTC - - // Format the date and time - formattedDate := feedbackDateTime.Format("02-01-2006") // DD-MM-YYYY - // formattedDateTime := feedbackDateTime.Format("02.01.2006 15:04 UTC") // DD.MM.YYYY HH:mm UTC - - // Admin details - adminName := "FCSC" - - // Email subject - subject := "UAE Stats Feedback" - - // imageRecord, err := app.Dao().FindFirstRecordByData("images", "file_name", "app_logo") - // if err != nil { - // log.Printf("Failed to fetch logo from images collection: %v", err) - // return nil - // } - - // // Construct the logo URL - // logoURL := fmt.Sprintf("%s/api/files/images/%s/%s", - // baseUrl, - // imageRecord.Id, - // imageRecord.GetString("file"), - // ) - - // Email body in HTML format - body := fmt.Sprintf(` - - - - - - Registration Update – UAE Stats App - - - - - - -
- - - - - - - - - - - - - - - - -
- - ملاحظات حول التطبيق الهاتفي للإحصاءات الإماراتي UAE
-

UAE Stats Mobile Application Feedback -


-
- - - - - -
- Hi Team,

- We have received new feedback from %s. Below are the details:

- Feedback Details: -
    -
  • User Name: %s
  • -
  • Email: %s
  • -
  • Date: %s
  • -


- Feedback: -
-										1. How was your experience with us today : %s
-										2. How good did we do in this aspect?
-										    * Ease of Use: %s
-										    * Quality: %s
-										    * Design: %s
-										    * Redundancy: %s
-										3. Tell us how we can improve  
-										    %s
-										
-
- مرحبا فريق العمل،

- لقد تلقينا تعليقات جديدة من %s. فيما يلي التفاصيل:

-
- تفاصيل الملاحظات: -
    -
  • اسم المستخدم: %s
  • -
  • البريد الإلكتروني: %s
  • -
  • التاريخ : %s
  • -


- تعليق : -
-										1. كيف كانت تجربتك معنا اليوم؟ : %s										
-										2. ما مدى نجاحنا في هذا الجانب؟
-										    * سهولة الاستخدام: %s
-										    * الجودة: %s
-										    * التصميم: %s
-										    * التكرار: %s
-										3. أخبرنا كيف يمكننا تحسين خدماتنا.؟
-										    %s
-										
-
-
- We The UAE 2031 -
- تنبيه:
هذه الرسالة الإلكترونية و أيٌ من مرفقاتها قد تحتوي على معلومات سرية وهامة موجهه للشخص/الأشخاص المعنيين وعليه يرجى من المتلقي في حال تلقي الرسالة الإلكترونية عن طريق
الخطأ ولم يكن المعني بها، إخطار المرسل وحذفها من بريده الإلكتروني و كذلك إتلاف أي نسخ مطبوعة عنها حيث أنه يحظر عليه قراءة ونسخ ونشر أو توزيع أو استخدام هذه الرسالة
إلكترونية و أيٌ من مرفقاتها بأي شكل من الأشكال -
- Disclaimer:
The information contained in and transmitted with this e-mail message is PRIVILEGED AND/OR CONFIDENTIAL.If you are not the intended recipient, or have received the message by error, please notify the sender via E-Mail or over the telephone and delete this e-mail. You are not authorized to read, copy, disseminate, distribute or use this E-Mail or any of its attachments in any way. -
-
- -`, userName, userName, userEmail, formattedDate, emojiRating, easeOfUse, quality, design, redundancy, additionalFeedback, userName, userName, userEmail, formattedDate, emojiRating, easeOfUse, quality, design, redundancy, additionalFeedback) - - // Define the target type (e.g., "smtp") - targetType := "feedback_receive_mail" - - // Fetch the email configuration record where type matches targetType - emailConfig, err := app.Dao().FindFirstRecordByData("email_configuration", "type", targetType) - if err != nil { - log.Printf("Failed to fetch email configuration for type '%s': %v", targetType, err) - return fmt.Errorf("email configuration not found for type '%s'", targetType) - } - - // Get the email address - feedbackEmail := emailConfig.GetString("email") - if feedbackEmail == "" { - log.Printf("No email address configured for type '%s'", targetType) - return fmt.Errorf("no email address configured for type '%s'", targetType) - } - // Create the email message - message := &mailer.Message{ - From: mail.Address{ - Name: "FCSC", - Address: app.Settings().Meta.SenderAddress, - }, - To: []mail.Address{ - { - Name: adminName, - Address: feedbackEmail, - }, - }, - Subject: subject, - HTML: body, - } - - // Send the email - err = app.NewMailClient().Send(message) - if err != nil { - log.Printf("Failed to send feedback email to admin: %v", err) - return err - } - - log.Println("Feedback email sent successfully to the admin.") - } - return nil - }) - - app.OnRecordAfterCreateRequest("otp_requests").Add(func(e *core.RecordCreateEvent) error { - // Ensure the record has an email field - email, ok := e.Record.Get("email").(string) - if !ok || email == "" { - log.Println("Invalid or missing email field.") - return nil // or handle this case appropriately - } - - // Generate the OTP - otp, err := generateOTP() - if err != nil { - log.Printf("Failed to generate OTP: %v\n", err) - return err - } - - // Calculate the expiration time (5 minutes from now) - expiresAt := time.Now().Add(5 * time.Minute) - - // Create a mail.Address for the sender - sender := mail.Address{ - Name: "FCSC", - Address: app.Settings().Meta.SenderAddress, // Ensure this is a valid email - } - - // Create the email message - message := &mailer.Message{ - From: sender, - To: []mail.Address{{Address: email}}, // Wrap the recipient email - Subject: "YOUR VERIFICATION CODE", - HTML: fmt.Sprintf( - `

Thanks for verifying your %s account!

-

Your code is: %d

-

Sincerely,
Support team.

`, - email, otp, - ), - } - - // Send the email - if err := app.NewMailClient().Send(message); err != nil { - log.Printf("Failed to send email: %v\n", err) - return err - } - - // Update the record with the OTP - e.Record.Set("otp", fmt.Sprintf("%04d", otp)) - e.Record.Set("expires_at", expiresAt) // Set the expiration time - - if err := app.Dao().SaveRecord(e.Record); err != nil { - log.Printf("Failed to update record with OTP and expiration time: %v\n", err) - return err - } - - log.Printf("OTP sent to %s expires at and saved successfully.\n", email) - return nil - }) - - // Start the PocketBase server - go func() { - if err := app.Start(); err != nil { - log.Fatal(err) - } - }() - - // Start your custom HTTP server on port 8091 - log.Println("Starting custom HTTP server on :8091") - if err := http.ListenAndServe(":8091", nil); err != nil { - log.Fatal("Failed to start server: ", err) - } -} - -// generateOTP generates a random 4-digit OTP -func generateOTP() (int, error) { - max := big.NewInt(10000) // 4-digit numbers range: 0000 to 9999 - n, err := rand.Int(rand.Reader, max) - if err != nil { - return 0, err - } - return int(n.Int64()), nil -} - -// Email verification handler -func verifyEmailHandler(c echo.Context, baseUrl string) error { - log.Println("Starting verifyEmailHandler") - - // Get the userId from the query parameter - userID := c.QueryParam("userId") - - if userID == "" { - return c.JSON(http.StatusBadRequest, map[string]string{ - "message": "User ID not provided", - }) - } - - // Check if app is properly initialized - if app == nil { - log.Fatal("PocketBase app is not initialized") - return c.JSON(http.StatusInternalServerError, map[string]string{ - "message": "Internal server error", - }) - } - - // Find the user record in PocketBase by userId - record, err := app.Dao().FindRecordById("users", userID) - if err != nil { - log.Printf("Error finding user: %v", err) - return c.JSON(500, map[string]interface{}{ - "code": 500, - "message": "Error finding user.", - }) - } - - // Check if the email is already verified - if record.GetBool("user_mail_verify") { - return c.HTML(http.StatusOK, ` - - - - - -
-
-

Email Already Verified

-

Your email has already been verified. No further action is required.

-
- - - `) - } - - // Set the verified status to true - record.Set("user_mail_verify", true) - // log.Printf("Failed to verify user: %v", err) - if err := app.Dao().SaveRecord(record); err != nil { - log.Printf("Failed to update email verification status: %v", err) - return c.JSON(http.StatusInternalServerError, map[string]string{ - "error": "Failed to update email verification status", - }) - } - - response := c.HTML(http.StatusOK, ` - - - - - -
-
-

Your registration is pending for Admin Approval

-

Access will be granted once your account is approved.

-
- - - `) - - // Fetch user details for email notification - username := record.GetString("uname") - userEmail := record.GetString("email") - - // Send admin email asynchronously - go func() { - err = sendAdminEmail(app, userID, username, userEmail, baseUrl) - if err != nil { - log.Printf("Failed to send admin email: %v", err) - } - }() - - return response - -} - -func sendAdminEmail(app *pocketbase.PocketBase, userID, username, userEmail string, baseUrl string) error { - - adminNameEn := "Admin" - adminNameAr := "المدير" - subject := "New User Registration Pending Review" - - // Fetch the email configuration from PocketBase - emailConfig, err := app.Dao().FindFirstRecordByData("email_configuration", "type", "admin_receive_mail") - if err != nil { - log.Printf("Failed to fetch email configuration: %v", err) - return err - } - adminEmail := emailConfig.GetString("email") - - // Construct email body - body := fmt.Sprintf(` - - - - - - Registration Update – UAE Stats App - - - - - - -
- - - - - - - - - - - - - - - - -
- - تسجيل مستخدم جديد - تطبيق إحصاءات الإمارات العربية المتحدة - قيد المراجعة
-
-
New User Registration - UAE Stats App - Pending Review -


-
- - - - - -
- Dear %s,

- A new user has registered on UAE Stats
Mobile app and is awaiting your review. -

- Details
- Name: %s
- Email: %s -

- Please review the registration and take
appropriate action -

- -
- عزيزي %s،

- تم تسجيل مستخدم جديد في تطبيق إحصاءات الإمارات
- للهواتف المحمولة، وهو بانتظار مراجعتك. -

- التفاصيل
- الاسم: %s
- البريد الإلكتروني: %s -

- يرجى مراجعة التسجيل واتخاذ الإجراء المناسب
- الموافقة أو الرفض. -

- -
-
- We The UAE 2031 -
- تنبيه:
هذه الرسالة الإلكترونية و أيٌ من مرفقاتها قد تحتوي على معلومات سرية وهامة موجهه للشخص/الأشخاص المعنيين وعليه يرجى من المتلقي في حال تلقي الرسالة الإلكترونية عن طريق
الخطأ ولم يكن المعني بها، إخطار المرسل وحذفها من بريده الإلكتروني و كذلك إتلاف أي نسخ مطبوعة عنها حيث أنه يحظر عليه قراءة ونسخ ونشر أو توزيع أو استخدام هذه الرسالة
إلكترونية و أيٌ من مرفقاتها بأي شكل من الأشكال -
- Disclaimer:
The information contained in and transmitted with this e-mail message is PRIVILEGED AND/OR CONFIDENTIAL.If you are not the intended recipient, or have received the message by error, please notify the sender via E-Mail or over the telephone and delete this e-mail. You are not authorized to read, copy, disseminate, distribute or use this E-Mail or any of its attachments in any way. -
-
- - - `, - adminNameEn, username, userEmail, userEmail, baseUrl, userID, baseUrl, baseUrl, userID, baseUrl, - adminNameAr, username, userEmail, userEmail, baseUrl, userID, baseUrl, baseUrl, userID, baseUrl) - - // Send email - message := &mailer.Message{ - From: mail.Address{ - Name: "FCSC", - Address: app.Settings().Meta.SenderAddress, - }, - To: []mail.Address{ - {Name: adminNameEn, Address: adminEmail}, - }, - Subject: subject, - HTML: body, - } - - if err := app.NewMailClient().Send(message); err != nil { - log.Printf("Failed to send admin email: %v", err) - return err - } - - log.Println("Admin email sent successfully.") - return nil -} - -func sendStatusChangeEmail(app *pocketbase.PocketBase, record *models.Record, newStatus string) error { - userEmail := record.GetString("email") - userName := record.GetString("uname") - log.Printf("userName: %s", userName) - - if userEmail == "" { - return fmt.Errorf("user email is empty") - } - - // imageRecord, err := app.Dao().FindFirstRecordByData("images", "file_name", "app_logo") - // if err != nil { - // log.Printf("Failed to fetch logo from images collection: %v", err) - // return nil - // } - - // // Construct the logo URL - // logoURL := fmt.Sprintf("%s/api/files/images/%s/%s", - // baseUrl, - // imageRecord.Id, - // imageRecord.GetString("file"), - // ) - - var subject, body string - - if newStatus == "Approved" { - subject = "Registration Approved – FCSC" - body = fmt.Sprintf(` - - - - - - Registration Update – UAE Stats App - - - - - - -
- - - - - - - - - - - - - - - - - - -
- - تمت الموافقة على التسجيل - إحصاءات الإمارات العربية المتحدة
-

Registration Approved – UAE Statistics App -


-
- - - - - -
- Dear %s,

- We are pleased to inform you that your
registration with UAE Stats app has been
approved. You can now log in using the
credentials that you had created. -

- Please access your account using the
following link: Login. -

- If you have any questions or believe this
decision was made in error, please feel free
to contact us for further clarification. -
- عزيزي %s،

- يسرنا إبلاغك بأنه قد تمت الموافقة على تسجيلك
في تطبيق إحصاءات الإمارات. يمكنك الآن تسجيل
الدخول باستخدام بيانات الاعتماد التي أنشأتها. -

-
- يرجى الوصول إلى حسابك عبر الرابط التالي
تسجيل الدخول. -

- إذا كانت لديك أي استفسارات أو كنت تعتقد أن هذا
القرار خاطئ، فلا تتردد في التواصل معنا لمزيد
من التوضيح. -
-
- We The UAE 2031 -
- تنبيه:
هذه الرسالة الإلكترونية و أيٌ من مرفقاتها قد تحتوي على معلومات سرية وهامة موجهه للشخص/الأشخاص المعنيين وعليه يرجى من المتلقي في حال تلقي الرسالة الإلكترونية عن طريق
الخطأ ولم يكن المعني بها، إخطار المرسل وحذفها من بريده الإلكتروني و كذلك إتلاف أي نسخ مطبوعة عنها حيث أنه يحظر عليه قراءة ونسخ ونشر أو توزيع أو استخدام هذه الرسالة
إلكترونية و أيٌ من مرفقاتها بأي شكل من الأشكال -
- Disclaimer:
The information contained in and transmitted with this e-mail message is PRIVILEGED AND/OR CONFIDENTIAL.If you are not the intended recipient, or have received the message by error, please notify the sender via E-Mail or over the telephone and delete this e-mail. You are not authorized to read, copy, disseminate, distribute or use this E-Mail or any of its attachments in any way. -
-
- - - `, userName, userName) - } else if newStatus == "Denied" { - subject = "Registration Update – FCSC" - body = fmt.Sprintf(` - - - - - - Registration Update – UAE Stats App - - - - - - -
- - - - - - - - - - - - - - - - -
- - تحديث التسجيل – تطبيق إحصاءات الإمارات العربية المتحدة
-

Registration Update – UAE Stats App -


-
- - - - - -
- Dear %s,

- Thank you for registering with UAE Stats
Mobile App. After careful review, we regret
to inform you that your registration has not
been approved at this time. -

- If you have any questions or believe this
decision was made in error, please feel free
to contact us for further clarification. -

- We appreciate your understanding and thank
you for your interest. -
- عزيزي %s،

- نشكرك على تسجيلك في تطبيق إحصاءات
الإمارات للهواتف المحمولة. بعد مراجعة دقيقة،
يؤسفنا إبلاغك بأنه لم تتم الموافقة على تسجيلك
حتى الآن. -

- إذا كانت لديك أي أسئلة أو كنت تعتقد أن هذا القرار
خاطئ، فلا تتردد في التواصل معنا لمزيد من
التوضيح. -

- نقدّر تفهمك ونشكرك على اهتمامك. -
-
- We The UAE 2031 -
- تنبيه:
هذه الرسالة الإلكترونية و أيٌ من مرفقاتها قد تحتوي على معلومات سرية وهامة موجهه للشخص/الأشخاص المعنيين وعليه يرجى من المتلقي في حال تلقي الرسالة الإلكترونية عن طريق
الخطأ ولم يكن المعني بها، إخطار المرسل وحذفها من بريده الإلكتروني و كذلك إتلاف أي نسخ مطبوعة عنها حيث أنه يحظر عليه قراءة ونسخ ونشر أو توزيع أو استخدام هذه الرسالة
إلكترونية و أيٌ من مرفقاتها بأي شكل من الأشكال -
- Disclaimer:
The information contained in and transmitted with this e-mail message is PRIVILEGED AND/OR CONFIDENTIAL.If you are not the intended recipient, or have received the message by error, please notify the sender via E-Mail or over the telephone and delete this e-mail. You are not authorized to read, copy, disseminate, distribute or use this E-Mail or any of its attachments in any way. -
-
- - - `, userName, userName) - } else { - // If the status is neither "Approved" nor "Denied", skip sending an email - return nil - } - - // Create and send the email - message := &mailer.Message{ - From: mail.Address{ - Name: "FCSC", - Address: app.Settings().Meta.SenderAddress, - }, - To: []mail.Address{ - { - Name: userName, - Address: userEmail, - }, - }, - Subject: subject, - HTML: body, - } - - err := app.NewMailClient().Send(message) - if err != nil { - return fmt.Errorf("failed to send email: %v", err) - } - - log.Printf("Status change email sent successfully to: %s", userEmail) - return nil -} diff --git a/pb/utils/calculation.go b/pb/utils/calculation.go deleted file mode 100755 index b52680b..0000000 --- a/pb/utils/calculation.go +++ /dev/null @@ -1,825 +0,0 @@ -package utils - -import ( - "encoding/json" - "errors" - "fmt" - "math" - "regexp" - "sort" - "strconv" - "strings" -) - -// Observation struct with json.Number for flexible value handling -type Observation struct { - ObsKey map[string]string `json:"ObsKey"` - ObsValue struct { - Value json.Number `json:"Value"` - } `json:"ObsValue"` -} - -// ------------------------------------------------------------ card calculation--------------------------------------------------- -type GroupByAndSum struct { - GroupByAndSum string `json:"groupby_and_sum"` - GetTop int `json:"get_top"` - Conversion int `json:"conversion"` -} -type CountCalculation struct { - Key string `json:"key"` - GetCountOf string `json:"get_count_of"` -} -type CardCalculationRequest struct { - Formula string `json:"formula"` - RecordCount string `json:"record_count"` - RecodeIndex string `json:"record_index"` - RecordKey string `json:"record_key"` - AdditionalRecordKey string `json:"additional_record_key"` - NumberFormat string `json:"number_format"` - Conversion string `json:"conversion"` - GroupByAndSum GroupByAndSum `json:"groupby_and_sum"` - CountCalculation CountCalculation `json:"count_calculation"` -} - -func CardCalculation(data []Observation, req CardCalculationRequest) (map[string]string, error) { - // Sort the data in descending order based on TimePeriod - sort.Slice(data, func(i, j int) bool { - return data[i].ObsKey["TIME_PERIOD"] > data[j].ObsKey["TIME_PERIOD"] - }) - - var selectedData []Observation - if req.RecodeIndex != "" { - - if req.RecodeIndex == "last" { - index := len(data) - if index < 0 || index >= len(data) { - selectedData = []Observation{} - } else { - selectedData = []Observation{data[index]} - } - - } else { - // Convert RecodeIndex to an integer - index, err := strconv.Atoi(req.RecodeIndex) - if err != nil || index < 0 || index >= len(data) { - selectedData = []Observation{} - } else { - selectedData = []Observation{data[index]} - } - - } - - } else { - // Determine the number of records to use - - if req.RecordCount == "all" { - if len(data) > 0 { - selectedData = data - } else { - selectedData = []Observation{} - } - } else if req.RecordCount == "first_pair" { - if len(data) >= 2 { - selectedData = data[:2] - } else { - selectedData = []Observation{} - } - } else if req.RecordCount == "second_pair" { - if len(data) >= 3 { - selectedData = data[1:3] - } else { - selectedData = []Observation{} - } - } else { - n := len(data) // array leangth - var err error - n, err = strconv.Atoi(req.RecordCount) - if err != nil || n >= len(data) { - // return nil, errors.New("invalid record count"); - selectedData = []Observation{} - } else { - selectedData = data[:n] - } - - } - - } - - var result float64 - switch req.Formula { - case "none": - result = none(selectedData) - case "total": - result = sum(selectedData) - case "average": - result = average(selectedData) - case "highest": - result = highest(selectedData) - case "lowest": - result = lowest(selectedData) - case "different": - result = difference(selectedData) - case "groupby_and_sum": - customCalculationReq := req.GroupByAndSum - - var chartCustomCalculationRequest ChartCalculationRequest - - jsonData, err := json.Marshal(customCalculationReq) - if err != nil { - fmt.Println("Error:", err) - } - - err = json.Unmarshal(jsonData, &chartCustomCalculationRequest) - if err != nil { - fmt.Println("Error:", err) - } - - selectedData, err = CalculateGroupedSum(selectedData, chartCustomCalculationRequest) - if err != nil { - fmt.Println("Error:", err) - } - - if len(selectedData) > 0 { - value, err := selectedData[0].ObsValue.Value.Float64() - if err == nil { - result = value - } else { - fmt.Println("Error converting first observation value to float64:", err) - } - } else { - fmt.Println("No observations returned from CalculateGroupedSum") - } - case "count_calculation": - countCalculationReq := req.CountCalculation - - var cardCountCalculationRequest CountCalculation - - jsonData, err := json.Marshal(countCalculationReq) - if err != nil { - fmt.Println("Error:", err) - } - - err = json.Unmarshal(jsonData, &cardCountCalculationRequest) - if err != nil { - fmt.Println("Error:", err) - } - - selectedData, err = CalculateCount(selectedData, cardCountCalculationRequest) - if err != nil { - fmt.Println("Error:", err) - } - - if len(selectedData) > 0 { - result = float64(len(selectedData)) - selectedData = selectedData[:1] - } else { - fmt.Println("No observations returned from CalculateGroupedSum") - } - - default: - return nil, errors.New("invalid formula") - } - - convertedValue := applyConversion(result, req.Conversion, req.NumberFormat) - - displayValue := extractRecordKeyValue(selectedData, req.RecordKey, req.AdditionalRecordKey, req.Formula, result) - - color := "" - if req.Formula == "different" || req.Conversion == "%" { - // Remove the percentage sign if present - convertedValueStr := strings.TrimSuffix(convertedValue, "%") - - // Convert to float - convertedValue, err := strconv.ParseFloat(convertedValueStr, 64) - if err != nil { - return nil, err // Handle error if conversion fails - } - if convertedValue > 0 { - color = "#11AF22" - } else if convertedValue < 0 { - color = "#D83731" - } - } - - return map[string]string{ - "value": convertedValue, - "display_value": displayValue, - "calculation": req.Formula, - "font_color": color, - }, nil -} - -// Helper function to convert json.Number to float64 safely -func getFloatValue(num json.Number) float64 { - val, err := num.Float64() - if err != nil { - fmt.Println("Error converting value:", err) - return 0 - } - return val -} - -func none(data []Observation) float64 { - total := 0.0 - for _, obs := range data { - total += getFloatValue(obs.ObsValue.Value) - } - return total -} - -func sum(data []Observation) float64 { - total := 0.0 - for _, obs := range data { - total += getFloatValue(obs.ObsValue.Value) - } - return total -} - -func average(data []Observation) float64 { - if len(data) == 0 { - return 0 - } - return sum(data) / float64(len(data)) -} - -func highest(data []Observation) float64 { - if len(data) == 0 { - return 0 - } - - max := getFloatValue(data[0].ObsValue.Value) - for _, obs := range data[1:] { - val := getFloatValue(obs.ObsValue.Value) - if val > max { - max = val - } - } - return max - -} - -func lowest(data []Observation) float64 { - if len(data) == 0 { - return 0 - } - - min := getFloatValue(data[0].ObsValue.Value) - for _, obs := range data { - val := getFloatValue(obs.ObsValue.Value) - if val < min { - min = val - } - } - return min -} - -func difference(data []Observation) float64 { - if len(data) < 2 { - return 0 - } - latest := getFloatValue(data[0].ObsValue.Value) - previous := getFloatValue(data[1].ObsValue.Value) - if previous == 0 { - return 0 - } - return ((latest - previous) / previous) * 100 -} - -// Function to add commas to a number string -func addCommas(num string, suffix string) string { - n := len(num) - if n <= 3 { - return num + suffix // Attach suffix directly if no commas needed - } - var sb strings.Builder - start := n % 3 - if start == 0 { - start = 3 - } - sb.WriteString(num[:start]) - - for i := start; i < n; i += 3 { - sb.WriteString("," + num[i:i+3]) - } - return sb.String() + suffix -} - -func formatNumberWithCommas(value float64, suffix string) string { - // Convert the value to a string with 2 decimal places - formatted := fmt.Sprintf("%.2f", value) - - // Split integer and decimal parts - parts := strings.Split(formatted, ".") - integerPart := parts[0] - decimalPart := parts[1] - - // Add commas to the integer part - var result strings.Builder - n := len(integerPart) - for i, c := range integerPart { - if i > 0 && (n-i)%3 == 0 { - result.WriteRune(',') - } - result.WriteRune(c) - } - - // Append decimal part if needed - if decimalPart != "00" { - return result.String() + "." + decimalPart + suffix - } - return result.String() + suffix -} - -func applyConversion(value float64, conversion string, numberFormat string) string { - if numberFormat == "T" { - value *= 1000000000000 - } else if numberFormat == "B" { - value *= 1000000000 - } else if numberFormat == "M" { - value *= 1000000 - } else if numberFormat == "K" { - value *= 1000 - } - - // Helper function to format numbers with a thousands separator only in the default case - formatNumber := func(v float64, suffix string) string { - if suffix == "" || suffix == "K" || suffix == "M" || suffix == "B" || suffix == "T" { - if v == float64(int(v)) { // No decimal part - return addCommas(strconv.Itoa(int(v)), suffix) - } - return formatNumberWithCommas(v, suffix) - } - if v == float64(int(v)) { // No decimal part - return fmt.Sprintf("%d%s", int(v), suffix) - } - return fmt.Sprintf("%.2f%s", v, suffix) - } - - switch conversion { - case "T": - return formatNumber(value/1000000000000, "T") - case "B": - return formatNumber(value/1000000000, "B") - case "M": - return formatNumber(value/1000000, "M") - case "K": - return formatNumber(value/1000, "K") - case "%": - return formatNumber(value, "%") - default: - return formatNumber(value, conversion) - } -} - -func extractRecordKeyValue(data []Observation, key string, additionalkey string, formula string, targetValue float64) string { - if len(data) == 0 { - return "" - } - - switch formula { - case "highest", "lowest", "none", "groupby_and_sum": - for _, obs := range data { - value, err := obs.ObsValue.Value.Float64() - if err == nil && value == targetValue { - if val, exists := obs.ObsKey[key]; exists { - - // Check if additionalkey exists in obs.ObsKey - if val2, exists2 := obs.ObsKey[additionalkey]; exists2 { - - return val + " - " + val2 - } else { - return val - } - } - } - } - - case "total", "average", "different": - if val1, exists1 := data[0].ObsKey[key]; exists1 { - if val2, exists2 := data[len(data)-1].ObsKey[key]; exists2 { - if val1 == val2 { - return val1 - } else { - if val1 > val2 { - return val2 + " - " + val1 - } else { - return val1 + " - " + val2 - } - } - } - return val1 - } - case "count_calculation": - for _, obs := range data { - - if val, exists := obs.ObsKey[key]; exists { - - // Check if additionalkey exists in obs.ObsKey - if val2, exists2 := obs.ObsKey[additionalkey]; exists2 { - - return val + " - " + val2 - } else { - return val - } - } - - } - } - - return "" -} - -// ------------------------------------------------------------ chart calculation--------------------------------------------------- -type ChartCalculationRequest struct { - GroupByAndSum string `json:"groupby_and_sum"` - AdditionalForGroupByAndSum string `json:"additional_for_groupby_and_sum"` - GetTop string `json:"get_top"` - SortBy string `json:"sort_by"` - SortAction string `json:"sort_action"` - Conversion string `json:"conversion"` -} - -func CalculateGroupedSum(observations []Observation, request ChartCalculationRequest) ([]Observation, error) { - // Map to store the summed values - summedData := make(map[string]float64) - - if request.GroupByAndSum == "none" { - // If "none", treat each observation separately - - // Collect observations with their original keys - var valueList []Observation - - for _, obs := range observations { - value, err := obs.ObsValue.Value.Float64() - if err != nil { - return nil, fmt.Errorf("invalid number format: %v", err) - } - - // Preserve the original ObsKey - valueList = append(valueList, Observation{ - ObsKey: obs.ObsKey, // Keep all keys - ObsValue: struct { - Value json.Number `json:"Value"` - }{ - Value: json.Number(strconv.FormatFloat(convertValue(value, request.Conversion), 'f', 6, 64)), - }, - }) - } - - // Sort by value in descending order or given key - sortByKey := request.SortBy - var nonDigit = regexp.MustCompile(`\D`) - if sortByKey != "" { - // Sort by given key - sort.Slice(valueList, func(i, j int) bool { - val1Str := valueList[i].ObsKey[sortByKey] - val2Str := valueList[j].ObsKey[sortByKey] - - // remove non-digits - val1Digits := nonDigit.ReplaceAllString(val1Str, "") - val2Digits := nonDigit.ReplaceAllString(val2Str, "") - - val1, _ := strconv.ParseInt(val1Digits, 10, 64) - val2, _ := strconv.ParseInt(val2Digits, 10, 64) - - sortActionKey := request.SortAction - if sortActionKey == "DESC" { - return val1 > val2 // Descending order - } - return val1 < val2 // Ascending order - - }) - } else { - // Default sorting by ObsValue.Value - sort.Slice(valueList, func(i, j int) bool { - val1, _ := valueList[i].ObsValue.Value.Float64() - val2, _ := valueList[j].ObsValue.Value.Float64() - return val1 > val2 // Descending order - }) - } - - // Get the top N values - topN, err := strconv.Atoi(request.GetTop) - if err != nil || topN <= 0 || topN > len(valueList) { - topN = len(valueList) // If invalid input, return all - } - - return valueList[:topN], nil - - } else { - // Iterate through observations and sum values based on GroupBy key and Additional key - for _, obs := range observations { - groupKey, exists := obs.ObsKey[request.GroupByAndSum] - if !exists { - continue // Skip if the key does not exist in ObsKey - } - - additionalKey := "" - if request.AdditionalForGroupByAndSum != "" { - additionalKey, _ = obs.ObsKey[request.AdditionalForGroupByAndSum] - } - - combinedKey := groupKey - if additionalKey != "" { - combinedKey += "_" + additionalKey - } - - value, err := obs.ObsValue.Value.Float64() - if err != nil { - return nil, fmt.Errorf("invalid number format: %v", err) - } - - summedData[combinedKey] += value - } - } - - // Convert map to slice of Observation - var result []Observation - for key, sum := range summedData { - keyParts := strings.Split(key, "_") - obsKey := map[string]string{ - request.GroupByAndSum: keyParts[0], - } - if len(keyParts) > 1 { - obsKey[request.AdditionalForGroupByAndSum] = keyParts[1] - } - - result = append(result, Observation{ - ObsKey: obsKey, - ObsValue: struct { - Value json.Number `json:"Value"` - }{ - Value: json.Number(strconv.FormatFloat(convertValue(sum, request.Conversion), 'f', 6, 64)), - }, - }) - } - - // Sort by value in descending order or given key - sortByKey := request.SortBy - var nonDigit = regexp.MustCompile(`\D`) - if sortByKey != "" { - // Sort by given key - sort.Slice(result, func(i, j int) bool { - val1Str := result[i].ObsKey[sortByKey] - val2Str := result[j].ObsKey[sortByKey] - - // remove non-digits - val1Digits := nonDigit.ReplaceAllString(val1Str, "") - val2Digits := nonDigit.ReplaceAllString(val2Str, "") - - val1, _ := strconv.ParseInt(val1Digits, 10, 64) - val2, _ := strconv.ParseInt(val2Digits, 10, 64) - - sortActionKey := request.SortAction - if sortActionKey == "DESC" { - return val1 > val2 // Descending order - } - return val1 < val2 // Ascending order - }) - } else { - // Default sorting by ObsValue.Value - sort.Slice(result, func(i, j int) bool { - val1, _ := result[i].ObsValue.Value.Float64() - val2, _ := result[j].ObsValue.Value.Float64() - return val1 > val2 // Descending order - }) - } - // Sort by summed values in descending order - // sort.Slice(result, func(i, j int) bool { - // val1, _ := result[i].ObsValue.Value.Float64() - // val2, _ := result[j].ObsValue.Value.Float64() - // return val1 > val2 // Sort descending - // }) - - // Get the top N values - topN, err := strconv.Atoi(request.GetTop) - if err != nil || topN <= 0 || topN > len(result) { - topN = len(result) // If invalid input, return all - } - - return result[:topN], nil -} - -// CalculateGroupedSum processes the observations based on the given request -// func CalculateGroupedSum(observations []Observation, request ChartCalculationRequest) ([]Observation, error) { - -// // Map to store the summed values -// summedData := make(map[string]float64) - -// if request.GroupByAndSum == "none" { -// // If "none", treat each observation separately -// for _, obs := range observations { -// value, err := obs.ObsValue.Value.Float64() -// if err != nil { -// return nil, fmt.Errorf("invalid number format: %v", err) -// } -// // Use a unique key (e.g., index-based) to preserve order -// summedData[strconv.Itoa(len(summedData))] = value -// } -// } else { -// // Iterate through observations and sum values based on GroupBy key -// for _, obs := range observations { -// groupKey, exists := obs.ObsKey[request.GroupByAndSum] -// if !exists { -// continue // Skip if the key does not exist in ObsKey -// } - -// value, err := obs.ObsValue.Value.Float64() -// if err != nil { -// return nil, fmt.Errorf("invalid number format: %v", err) -// } - -// summedData[groupKey] += value -// } - -// } - -// // Convert map to slice of Observation -// var result []Observation -// for key, sum := range summedData { -// result = append(result, Observation{ -// ObsKey: map[string]string{ -// request.GroupByAndSum: key, -// }, -// ObsValue: struct { -// Value json.Number `json:"Value"` -// }{ -// // Value: json.Number(fmt.Sprintf("%.0f", sum)), -// Value: json.Number(strconv.FormatFloat(convertValue(sum, request.Conversion), 'f', 6, 64)), -// }, -// }) -// } - -// // Sort by summed values in descending order -// sort.Slice(result, func(i, j int) bool { -// val1, _ := result[i].ObsValue.Value.Float64() -// val2, _ := result[j].ObsValue.Value.Float64() -// return val1 > val2 // Sort descending -// }) - -// // Get the top N values -// topN, err := strconv.Atoi(request.GetTop) -// if err != nil || topN <= 0 || topN > len(result) { -// topN = len(result) // If invalid input, return all -// } - -// return result[:topN], nil -// } - -// Convert values based on conversion type -func convertValue(value float64, conversion string) float64 { - switch conversion { - case "T": // Trillion - return round(value/1000000000000, 2) - case "B": // Billion - return round(value/1000000000, 2) - case "M": // Million - return round(value/1000000, 2) - case "K": // Thousand - return round(value/1000, 2) - default: // No conversion - return round(value, 2) - } -} - -// Function to round a float to 2 decimal places -func round(val float64, precision int) float64 { - p := math.Pow(10, float64(precision)) - return math.Round(val*p) / p -} - -//------------------------------------------------------------- default filter --------------------------------------------------- - -func DefaultYearFilter(data []Observation, latestYearCount string) ([]Observation, error) { - // Convert latestYearCount to integer - yearCount, err := strconv.Atoi(latestYearCount) - if err != nil { - return nil, err - } - if yearCount == 0 { - yearCount = 1 - } - - // Sort the data in descending order based on TIME_PERIOD - sort.Slice(data, func(i, j int) bool { - return data[i].ObsKey["TIME_PERIOD"] > data[j].ObsKey["TIME_PERIOD"] - }) - - // Collect unique years in sorted order - uniqueYears := make([]string, 0) - yearMap := make(map[string]bool) - - for _, item := range data { - year := item.ObsKey["TIME_PERIOD"] - if !yearMap[year] { - yearMap[year] = true - uniqueYears = append(uniqueYears, year) - } - } - - // Adjust year count if there are fewer available years - if len(uniqueYears) < yearCount { - yearCount = len(uniqueYears) // Set to available years - } - - // Create a set for only the required latest years - latestYears := make(map[string]bool) - for i := 0; i < yearCount; i++ { - latestYears[uniqueYears[i]] = true - } - - // Filter the data to keep only records from the selected latest N years - var filteredData []Observation - for _, item := range data { - if latestYears[item.ObsKey["TIME_PERIOD"]] { - filteredData = append(filteredData, item) - } - } - - return filteredData, nil -} - -type Filter struct { - FilterKey string `json:"filter_key"` - FilterData []string `json:"filter_data"` -} - -func FilterByMultipleKeys(data []Observation, filters []Filter) ([]Observation, error) { - var filteredData []Observation - - // Convert filter conditions into a map for quick lookup - filterMap := make(map[string]map[string]bool) - for _, filter := range filters { - if len(filter.FilterData) == 0 { - continue // Skip empty filter_data - } - if filterMap[filter.FilterKey] == nil { - filterMap[filter.FilterKey] = make(map[string]bool) - } - for _, value := range filter.FilterData { - filterMap[filter.FilterKey][value] = true - } - } - fmt.Println("filterMap :", filterMap) - // Iterate through data and apply the filters - for _, item := range data { - matches := true - for key, allowedValues := range filterMap { - if _, exists := item.ObsKey[key]; exists { - if !allowedValues[item.ObsKey[key]] { // If value not in allowed list - matches = false - break - } - } else { - matches = false // Key not present in data - break - } - } - - if matches { - filteredData = append(filteredData, item) - } - } - - return filteredData, nil -} - -//-------------------------------------------------------------- Count Calculation ------------------------------------------------- - -func CalculateCount(observations []Observation, request CountCalculation) ([]Observation, error) { - - if request.GetCountOf == "highest" || request.GetCountOf == "lowest" { - var extremeValue int - var filteredObservations []Observation - first := true - isHighest := request.GetCountOf == "highest" - - for _, obs := range observations { - if valStr, exists := obs.ObsKey[request.Key]; exists { - val, err := strconv.Atoi(valStr) - if err != nil { - continue // Skip if conversion fails - } - - if first || (isHighest && val > extremeValue) || (!isHighest && val < extremeValue) { - extremeValue = val - first = false - } - } - } - - // Filter observations that match the extreme value - for _, obs := range observations { - if valStr, exists := obs.ObsKey[request.Key]; exists { - val, err := strconv.Atoi(valStr) - if err == nil && val == extremeValue { - filteredObservations = append(filteredObservations, obs) - } - } - } - - return filteredObservations, nil - } - - return nil, fmt.Errorf("unsupported get_count_of value: %s", request.GetCountOf) -} diff --git a/pb/utils/exclude_filter_data.go b/pb/utils/exclude_filter_data.go deleted file mode 100755 index ae5de08..0000000 --- a/pb/utils/exclude_filter_data.go +++ /dev/null @@ -1,65 +0,0 @@ -package utils - -type FilterJsonData struct { - Order int `json:"order"` - En string `json:"en"` - Ar string `json:"ar"` -} - -type FilterGroup struct { - FilterKey string `json:"filter_key"` - FilterData []interface{} `json:"filter_data"` - FilterTextAndOrder FilterJsonData `json:"filter_text_and_order"` -} - -// RemoveExcludedFilters removes specified filter_data values from filters. -func RemoveExcludedFilters(filters []FilterGroup, exclusions []FilterGroup) []FilterGroup { - // Create a map for quick lookup of exclusion filter data - exclusionMap := make(map[string]map[interface{}]bool) - - // Populate the exclusion map - for _, excl := range exclusions { - if _, exists := exclusionMap[excl.FilterKey]; !exists { - exclusionMap[excl.FilterKey] = make(map[interface{}]bool) - } - for _, value := range excl.FilterData { - exclusionMap[excl.FilterKey][value] = true - } - } - - // Debug: Print exclusionMap to verify exclusions - // log.Println("Exclusion Map: ", exclusionMap) - - // Process the filters and remove excluded values - var filteredFilters []FilterGroup - - for _, filter := range filters { - if excludedValues, found := exclusionMap[filter.FilterKey]; found { - // Debug: Print current filter data and exclusions - // log.Printf("Checking filter: %s with data: %v", filter.FilterKey, filter.FilterData) - - // Remove excluded values - var newFilterData []interface{} - for _, value := range filter.FilterData { - // Debug: Check if value is excluded - if _, exists := excludedValues[value]; exists { - // log.Printf("Excluding value: %v", value) // Debug log for excluded value - } else { - newFilterData = append(newFilterData, value) - } - } - if len(newFilterData) > 0 { - filter.FilterData = newFilterData - filteredFilters = append(filteredFilters, filter) - } - } else { - // No exclusions, add filter as is - filteredFilters = append(filteredFilters, filter) - } - } - - // Debug: Print final filtered result - // log.Println("Filtered Filters: ", filteredFilters) - - return filteredFilters -} diff --git a/pb/utils/translate_chart_data.go b/pb/utils/translate_chart_data.go deleted file mode 100755 index dac68a3..0000000 --- a/pb/utils/translate_chart_data.go +++ /dev/null @@ -1,48 +0,0 @@ -package utils - -// TranslateChartData processes chart data with language source data to produce the result based on the language key. -func TranslateChartData(chartData []map[string]interface{}, languageSource []map[string]interface{}, langKey string) []map[string]interface{} { - // Helper function to find translation - findTranslation := func(key string, value string) string { - for _, langEntry := range languageSource { - if langEntry["key"] == key && langEntry["value"] == value { - if translatedValue, ok := langEntry[langKey]; ok { - return translatedValue.(string) - } - } - } - return value // Return the original value if no translation is found - } - - // Extract unique translation keys from languageSource - translationKeys := map[string]bool{} - for _, langEntry := range languageSource { - if key, ok := langEntry["key"].(string); ok { - translationKeys[key] = true - } - } - - // Process each chart data entry - result := []map[string]interface{}{} - for _, entry := range chartData { - obsKey := entry["ObsKey"].(map[string]interface{}) - translatedObsKey := map[string]interface{}{} - - // Translate fields in ObsKey dynamically based on extracted translation keys - for key, value := range obsKey { - if translationKeys[key] { - translatedObsKey[key] = findTranslation(key, value.(string)) - } else { - translatedObsKey[key] = value - } - } - - // Append the translated entry to the result - result = append(result, map[string]interface{}{ - "ObsKey": translatedObsKey, - "ObsValue": entry["ObsValue"], - }) - } - - return result -}