diff --git a/main.go b/main.go
index 164745c..d75f467 100755
--- a/main.go
+++ b/main.go
@@ -475,6 +475,22 @@ func main() {
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")
diff --git a/pb.zip b/pb.zip
new file mode 100644
index 0000000..0bec093
Binary files /dev/null and b/pb.zip differ
diff --git a/pb/.env b/pb/.env
new file mode 100644
index 0000000..2c4392e
--- /dev/null
+++ b/pb/.env
@@ -0,0 +1,3 @@
+APP_SIGNATURE = fcsc.gov.ae.X7pL9qZm2A
+
+BASE_URL = https://pocket.fcsc.gov.ae
diff --git a/pb/main.go b/pb/main.go
new file mode 100755
index 0000000..164745c
--- /dev/null
+++ b/pb/main.go
@@ -0,0 +1,2976 @@
+package main
+
+import (
+ "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")
+
+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
+ }
+
+ // 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")
+
+ app = pocketbase.New()
+
+ // 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.
+ |
+
+ مرحبًا،
+ يُرجى الضغط على الرابط أدناه لإعادة تعيين كلمة المرور الخاصة بك:
+ إعادة تعيين كلمة المرور
+
+ إذا لم تطلب إعادة تعيين كلمة المرور، يمكنك تجاهل هذا البريد الإلكتروني.
+ |
+
+
+ |
+
+
+
+
+ |
+
+
+
+ تنبيه: هذه الرسالة الإلكترونية و أيٌ من مرفقاتها قد تحتوي على معلومات سرية وهامة موجهه للشخص/الأشخاص المعنيين وعليه يرجى من المتلقي في حال تلقي الرسالة الإلكترونية عن طريق الخطأ ولم يكن المعني بها، إخطار المرسل وحذفها من بريده الإلكتروني و كذلك إتلاف أي نسخ مطبوعة عنها حيث أنه يحظر عليه قراءة ونسخ ونشر أو توزيع أو استخدام هذه الرسالة إلكترونية و أيٌ من مرفقاتها بأي شكل من الأشكال
+ |
+
+
+
+ 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/getHomePageData", func(c echo.Context) error {
+
+ // language := c.QueryParam("language")
+ // colorMode := c.QueryParam("color_mode")
+
+ // var colorPatternColumn string
+ // if colorMode == "dark" {
+ // colorPatternColumn = "color_pattern_dark"
+ // } else {
+ // colorPatternColumn = "color_pattern_light"
+ // }
+
+ // var languageSpecificColumn string
+ // if language == "en" {
+ // languageSpecificColumn = "data_set_tile_heading_en"
+ // } else {
+ // languageSpecificColumn = "data_set_tile_heading_ar"
+ // }
+
+ // // 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"`
+ // }{}
+
+ // 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)
+
+ })
+
+ 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،
+ شكرًا لتسجيلك. يُرجى النقر على الرابط أدناه للتحقق من عنوان بريدك الإلكتروني:
+
+ تحقق من البريد الإلكتروني
+
+ إذا لم تكن قد قمت بتسجيل هذا الحساب، يُرجى تجاهل هذا البريد الإلكتروني.
+ |
+
+
+ |
+
+
+
+
+ |
+
+
+
+ تنبيه: هذه الرسالة الإلكترونية و أيٌ من مرفقاتها قد تحتوي على معلومات سرية وهامة موجهه للشخص/الأشخاص المعنيين وعليه يرجى من المتلقي في حال تلقي الرسالة الإلكترونية عن طريق الخطأ ولم يكن المعني بها، إخطار المرسل وحذفها من بريده الإلكتروني و كذلك إتلاف أي نسخ مطبوعة عنها حيث أنه يحظر عليه قراءة ونسخ ونشر أو توزيع أو استخدام هذه الرسالة إلكترونية و أيٌ من مرفقاتها بأي شكل من الأشكال
+ |
+
+
+
+ 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
+
+ |
+
+
+ |
+
+
+
+
+ |
+
+
+
+ تنبيه: هذه الرسالة الإلكترونية و أيٌ من مرفقاتها قد تحتوي على معلومات سرية وهامة موجهه للشخص/الأشخاص المعنيين وعليه يرجى من المتلقي في حال تلقي الرسالة الإلكترونية عن طريق الخطأ ولم يكن المعني بها، إخطار المرسل وحذفها من بريده الإلكتروني و كذلك إتلاف أي نسخ مطبوعة عنها حيث أنه يحظر عليه قراءة ونسخ ونشر أو توزيع أو استخدام هذه الرسالة إلكترونية و أيٌ من مرفقاتها بأي شكل من الأشكال
+ |
+
+
+
+ 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
+
+ يرجى مراجعة التسجيل واتخاذ الإجراء المناسب
+ الموافقة أو الرفض.
+
+
+ |
+
+
+ |
+
+
+
+
+ |
+
+
+
+ تنبيه: هذه الرسالة الإلكترونية و أيٌ من مرفقاتها قد تحتوي على معلومات سرية وهامة موجهه للشخص/الأشخاص المعنيين وعليه يرجى من المتلقي في حال تلقي الرسالة الإلكترونية عن طريق الخطأ ولم يكن المعني بها، إخطار المرسل وحذفها من بريده الإلكتروني و كذلك إتلاف أي نسخ مطبوعة عنها حيث أنه يحظر عليه قراءة ونسخ ونشر أو توزيع أو استخدام هذه الرسالة إلكترونية و أيٌ من مرفقاتها بأي شكل من الأشكال
+ |
+
+
+
+ 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،
+ يسرنا إبلاغك بأنه قد تمت الموافقة على تسجيلك في تطبيق إحصاءات الإمارات. يمكنك الآن تسجيل الدخول باستخدام بيانات الاعتماد التي أنشأتها.
+
+
+ يرجى الوصول إلى حسابك عبر الرابط التالي تسجيل الدخول.
+
+ إذا كانت لديك أي استفسارات أو كنت تعتقد أن هذا القرار خاطئ، فلا تتردد في التواصل معنا لمزيد من التوضيح.
+ |
+
+
+ |
+
+
+
+
+ |
+
+
+
+ تنبيه: هذه الرسالة الإلكترونية و أيٌ من مرفقاتها قد تحتوي على معلومات سرية وهامة موجهه للشخص/الأشخاص المعنيين وعليه يرجى من المتلقي في حال تلقي الرسالة الإلكترونية عن طريق الخطأ ولم يكن المعني بها، إخطار المرسل وحذفها من بريده الإلكتروني و كذلك إتلاف أي نسخ مطبوعة عنها حيث أنه يحظر عليه قراءة ونسخ ونشر أو توزيع أو استخدام هذه الرسالة إلكترونية و أيٌ من مرفقاتها بأي شكل من الأشكال
+ |
+
+
+
+ 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،
+ نشكرك على تسجيلك في تطبيق إحصاءات الإمارات للهواتف المحمولة. بعد مراجعة دقيقة، يؤسفنا إبلاغك بأنه لم تتم الموافقة على تسجيلك حتى الآن.
+
+ إذا كانت لديك أي أسئلة أو كنت تعتقد أن هذا القرار خاطئ، فلا تتردد في التواصل معنا لمزيد من التوضيح.
+
+ نقدّر تفهمك ونشكرك على اهتمامك.
+ |
+
+
+ |
+
+
+
+
+ |
+
+
+
+ تنبيه: هذه الرسالة الإلكترونية و أيٌ من مرفقاتها قد تحتوي على معلومات سرية وهامة موجهه للشخص/الأشخاص المعنيين وعليه يرجى من المتلقي في حال تلقي الرسالة الإلكترونية عن طريق الخطأ ولم يكن المعني بها، إخطار المرسل وحذفها من بريده الإلكتروني و كذلك إتلاف أي نسخ مطبوعة عنها حيث أنه يحظر عليه قراءة ونسخ ونشر أو توزيع أو استخدام هذه الرسالة إلكترونية و أيٌ من مرفقاتها بأي شكل من الأشكال
+ |
+
+
+
+ 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/pb_public/approve.png b/pb/pb_public/approve.png
new file mode 100644
index 0000000..c800e66
Binary files /dev/null and b/pb/pb_public/approve.png differ
diff --git a/pb/pb_public/reject.png b/pb/pb_public/reject.png
new file mode 100644
index 0000000..c5165e6
Binary files /dev/null and b/pb/pb_public/reject.png differ
diff --git a/pb_data/logs.db b/pb_data/logs.db
index f3dc718..8d06f50 100644
Binary files a/pb_data/logs.db and b/pb_data/logs.db differ
diff --git a/pb_public/privacy/index.html b/pb_public/privacy/index.html
new file mode 100644
index 0000000..5a6491c
--- /dev/null
+++ b/pb_public/privacy/index.html
@@ -0,0 +1,97 @@
+
+
+
+
+ Privacy Policy - FCSC
+
+
+
+
+
+ Privacy Policy
+
+
+
+ Welcome to the Federal Competitiveness and Statistics Centre (FCSC) Mobile Application.
+ We value your privacy and data protection and are committed to safeguarding your personal
+ information. This Privacy Policy explains how we collect, use, share, and protect your information
+ when using this App.
+
+
+
+ By using the App, you consent to the practices described in this Privacy Policy.
+
+
+
1. Information We Collect
+
1.1 Personal Information
+
+ - Name, email address, phone number (when registering an account).
+ - User credentials (such as username and password).
+ - Demographic data (if provided by the user).
+
+
+
1.2 Usage Data
+
+ - Device information (IP address, operating system, device type).
+ - App interactions (pages viewed, features used, actions performed).
+ - Log data (error reports, login timestamps).
+
+
+
1.3 Location Data
+
If enabled, we may collect location-based data (to enhance certain features).
+
+
1.4 Cookies and Tracking Technologies
+
We may use cookies, analytics tools, and tracking technologies to improve App functionality and user experience.
+
+
2. How We Use Your Information
+
+ - Provide and improve services.
+ - Deliver insights, statistics, and reports.
+ - Personalize user experience.
+ - Ensure security and prevent unauthorized access.
+ - Comply with UAE laws and regulations.
+ - Communicate with users (inquiries, updates, support).
+
+
+
3. How We Share Your Information
+
FCSC does not sell or trade your personal information. However, we may share data in the following cases:
+
+ - Government and Legal Compliance (UAE authorities, legal obligations).
+ - Third-Party Service Providers (hosting, analytics, technical support).
+ - Security and Fraud Prevention.
+
+
+
4. Data Storage and Security
+
+ - AES-256 encryption to protect user data.
+ - Stored on government-approved secure servers.
+ - Access only by authorized personnel.
+ - Data retained only as long as legally necessary.
+
+
+
5. User Rights and Choices
+
+ - Access & Review your stored personal data.
+ - Correction & Deletion of personal data.
+ - Opt-Out of marketing communications.
+ - Disable location services via device settings.
+
+
To exercise these rights, contact us at [Insert Official FCSC Email].
+
+
6. Third-Party Links and Services
+
The App may contain links to external websites or services (e.g., UAE government portals).
+ FCSC is not responsible for third-party privacy practices. Users should review their respective Privacy Policies.
+
+
7. Children's Privacy
+
The App is not intended for children under 13 years. We do not knowingly collect data from minors.
+ Parents or guardians may request removal of a child’s data by contacting FCSC.
+
+
8. Changes to This Privacy Policy
+
We may update this Privacy Policy periodically to reflect changes in laws, technology, or user feedback.
+ The latest version will always be available in the App.
+
+
9. Contact Information
+
For privacy-related inquiries, you may contact: info@fcsc.gov.ae
+
+
+