Compare commits

...

3 Commits

Author SHA1 Message Date
5d9879657b auto lat and long 2026-07-27 11:42:32 +05:30
84dfa8e640 Add a synthetic "All" category to getappcategories
The mobile app needs an "All" tile to browse every product regardless of
category. Rather than inserting a real app_category row (which would break
once any category-scoped product filter treats it as an actual, empty
category), the service now prepends a synthetic entry with categoryid=0 —
reusing the "0 = no category filter" convention GetAllProducts already
implements in FetchFilteredProducts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 11:04:37 +05:30
7db81c0e68 Return the created location from CreateTenantLocation so its locationid is available immediately
The store/branch QR code the app scans is just {tenantid, locationid} JSON, but the
onboarding response previously discarded the DB-assigned locationid, so the frontend
had no way to render a store's QR right after onboarding without a separate lookup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 11:08:57 +05:30
4 changed files with 181 additions and 9 deletions

View File

@@ -25,7 +25,7 @@ type TenantRepository interface {
GetStaffs(tid int) ([]models.StaffInfo, error) GetStaffs(tid int) ([]models.StaffInfo, error)
CreateStaff(user models.User) error CreateStaff(user models.User) error
UpdateStaff(user models.User) error UpdateStaff(user models.User) error
CreateTenantLocation(data models.Tenantlocations) error CreateTenantLocation(data models.Tenantlocations) (models.Tenantlocations, error)
UpdateTenantLocation(data models.Tenantlocations) error UpdateTenantLocation(data models.Tenantlocations) error
CheckTenantByNo(cno string) int CheckTenantByNo(cno string) int
CreateTenantUser(data models.Tenants) (bool, error) CreateTenantUser(data models.Tenants) (bool, error)
@@ -346,7 +346,7 @@ func (r *tenantRepository) UpdateStaff(user models.User) error {
return nil return nil
} }
func (r *tenantRepository) CreateTenantLocation(data models.Tenantlocations) error { func (r *tenantRepository) CreateTenantLocation(data models.Tenantlocations) (models.Tenantlocations, error) {
var user models.Tenantuser var user models.Tenantuser
tx := r.db.Begin() tx := r.db.Begin()
@@ -360,10 +360,12 @@ func (r *tenantRepository) CreateTenantLocation(data models.Tenantlocations) err
data.Status = "Active" data.Status = "Active"
} }
// Step 1: Insert into tenantlocations // Step 1: Insert into tenantlocations. GORM writes the DB-assigned
// locationid back onto data, which callers need to build the store's
// QR code (payload is just {tenantid, locationid}) right after onboarding.
if err := tx.Create(&data).Error; err != nil { if err := tx.Create(&data).Error; err != nil {
tx.Rollback() tx.Rollback()
return err return models.Tenantlocations{}, err
} }
// Step 2: Insert into app_users // Step 2: Insert into app_users
@@ -389,15 +391,15 @@ func (r *tenantRepository) CreateTenantLocation(data models.Tenantlocations) err
if err := tx.Table("app_users").Create(&user).Error; err != nil { if err := tx.Table("app_users").Create(&user).Error; err != nil {
tx.Rollback() tx.Rollback()
return err return models.Tenantlocations{}, err
} }
// Commit // Commit
if err := tx.Commit().Error; err != nil { if err := tx.Commit().Error; err != nil {
return err return models.Tenantlocations{}, err
} }
return nil return data, nil
} }
func (r *tenantRepository) UpdateTenantLocation(input models.Tenantlocations) error { func (r *tenantRepository) UpdateTenantLocation(input models.Tenantlocations) error {

View File

@@ -0,0 +1,146 @@
//go:build ignore
package main
import (
"database/sql"
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"strings"
"time"
_ "github.com/jackc/pgx/v5/stdlib"
)
// One-off backfill for tenants/tenantlocations rows that were onboarded
// before the admin console geocoded addresses automatically, so their
// latitude/longitude columns were left blank. Geocodes each row's existing
// address via OpenStreetMap Nominatim (same keyless provider
// AddressAutocomplete.tsx already uses on the frontend) and writes the
// result back. Run manually: `go run scratch/backfill_location_coordinates.go`
// — do not wire this into any request path, it's a single pass over
// historical rows.
const nominatimURL = "https://nominatim.openstreetmap.org/search"
// Nominatim's usage policy caps free lookups at ~1 request/sec.
const rateLimit = 1100 * time.Millisecond
type nominatimRow struct {
Lat string `json:"lat"`
Lon string `json:"lon"`
}
func geocode(client *http.Client, address string) (lat, lon string, ok bool) {
q := url.Values{}
q.Set("format", "json")
q.Set("limit", "1")
q.Set("q", address)
req, err := http.NewRequest("GET", nominatimURL+"?"+q.Encode(), nil)
if err != nil {
return "", "", false
}
// Required by Nominatim's usage policy — identifies the calling app.
req.Header.Set("User-Agent", "fiesta-backend-backfill/1.0 (care@nearle.in)")
resp, err := client.Do(req)
if err != nil {
fmt.Printf(" geocode request error: %v\n", err)
return "", "", false
}
defer resp.Body.Close()
var rows []nominatimRow
if err := json.NewDecoder(resp.Body).Decode(&rows); err != nil || len(rows) == 0 {
return "", "", false
}
return rows[0].Lat, rows[0].Lon, true
}
type row struct {
id int
address string
}
func backfillTable(db *sql.DB, client *http.Client, table, idCol string) {
fmt.Printf("\n=== %s ===\n", table)
query := fmt.Sprintf(`
SELECT %s,
TRIM(BOTH ', ' FROM CONCAT_WS(', ', address, suburb, city, state, postcode))
FROM %s
WHERE (latitude IS NULL OR latitude = '' OR latitude = '0')
AND (address IS NOT NULL AND address <> '')`, idCol, table)
rows, err := db.Query(query)
if err != nil {
log.Fatalf("%s: query error: %v", table, err)
}
var targets []row
for rows.Next() {
var r row
if err := rows.Scan(&r.id, &r.address); err != nil {
log.Fatalf("%s: scan error: %v", table, err)
}
targets = append(targets, r)
}
rows.Close()
fmt.Printf("Found %d row(s) missing coordinates.\n", len(targets))
updated, skipped := 0, 0
for i, r := range targets {
if i > 0 {
time.Sleep(rateLimit)
}
lat, lon, ok := geocode(client, r.address)
if !ok {
fmt.Printf(" [%s=%d] %q — geocode FAILED, left untouched\n", idCol, r.id, r.address)
skipped++
continue
}
res, err := db.Exec(
fmt.Sprintf(`UPDATE %s SET latitude = $1, longitude = $2 WHERE %s = $3`, table, idCol),
lat, lon, r.id,
)
if err != nil {
fmt.Printf(" [%s=%d] update error: %v\n", idCol, r.id, err)
skipped++
continue
}
n, _ := res.RowsAffected()
if n > 0 {
fmt.Printf(" [%s=%d] %q -> lat=%s lon=%s\n", idCol, r.id, r.address, lat, lon)
updated++
}
}
fmt.Printf("%s: updated=%d skipped=%d\n", table, updated, skipped)
}
func main() {
dsn := "host=66.116.207.225 port=5433 user=admin password=Package@123# dbname=nearledb sslmode=disable"
db, err := sql.Open("pgx", dsn)
if err != nil {
log.Fatalf("open error: %v", err)
}
defer db.Close()
if err := db.Ping(); err != nil {
log.Fatalf("ping error: %v", err)
}
fmt.Println("Connected to nearledb.")
client := &http.Client{Timeout: 10 * time.Second}
backfillTable(db, client, "tenants", "tenantid")
backfillTable(db, client, "tenantlocations", "locationid")
fmt.Println(strings.Repeat("-", 40))
fmt.Println("Done.")
}

View File

@@ -107,7 +107,7 @@ func (s *tenantService) UpdateStaff(user models.User) error {
} }
func (s *tenantService) CreateTenantLocation(data models.Tenantlocations) map[string]interface{} { func (s *tenantService) CreateTenantLocation(data models.Tenantlocations) map[string]interface{} {
err := s.repo.CreateTenantLocation(data) created, err := s.repo.CreateTenantLocation(data)
if err != nil { if err != nil {
return map[string]interface{}{ return map[string]interface{}{
"code": http.StatusConflict, "code": http.StatusConflict,
@@ -116,10 +116,14 @@ func (s *tenantService) CreateTenantLocation(data models.Tenantlocations) map[st
} }
} }
// "details" carries back the DB-assigned locationid so the frontend can
// build the store's QR code (tenantid+locationid) immediately after
// onboarding, instead of having to look the new location up separately.
return map[string]interface{}{ return map[string]interface{}{
"code": http.StatusCreated, "code": http.StatusCreated,
"message": "Tenant Location Successfully Created", "message": "Tenant Location Successfully Created",
"status": true, "status": true,
"details": created,
} }
} }

View File

@@ -48,5 +48,25 @@ func (s *utilsService) GetAppConfig(configID int) (models.Appconfig, error) {
} }
func (s *utilsService) GetAppCategory() ([]models.AppCategory, error) { func (s *utilsService) GetAppCategory() ([]models.AppCategory, error) {
return s.repo.GetAppCategory() categories, err := s.repo.GetAppCategory()
if err != nil {
return nil, err
}
// "All" is a synthetic pseudo-category, not a real app_category row — it
// must never be inserted into app_category itself, since product-listing
// filters (e.g. ProductController.GetAllProducts) already treat
// categoryid=0 as "no category filter". Keeping this row's id at 0 reuses
// that existing convention instead of adding a new one.
all := models.AppCategory{
Categoryid: 0,
Categoryname: "All",
Categorytype: 7,
Sortorder: 0,
Crossaxis: 1,
Mainaxis: 1,
Status: "Active",
}
return append([]models.AppCategory{all}, categories...), nil
} }