fix: admin console endpoints
- Remove scratch/check_users.go - GetAdminBookings: LIMIT/OFFSET enforced, returns pageno/pagesize/pages - GetAdminCustomers: new B2C appcustomers list with booking count - AdminCancelBooking: cancel + miler release + FCM + NATS event
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"doormile/config"
|
"doormile/config"
|
||||||
@@ -442,6 +443,79 @@ func GetTenantCustomers(c *fiber.Ctx) error {
|
|||||||
return utils.List(c, customers, int64(len(customers)))
|
return utils.List(c, customers, int64(len(customers)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------
|
||||||
|
// B2C APP CUSTOMERS
|
||||||
|
// --------------------
|
||||||
|
|
||||||
|
func GetAdminCustomers(c *fiber.Ctx) error {
|
||||||
|
pageno := max(1, c.QueryInt("pageno", 1))
|
||||||
|
pagesize := min(100, max(1, c.QueryInt("pagesize", 20)))
|
||||||
|
offset := (pageno - 1) * pagesize
|
||||||
|
keyword := c.Query("keyword")
|
||||||
|
|
||||||
|
query := db.DB.Model(&models.AppCustomer{})
|
||||||
|
if keyword != "" {
|
||||||
|
like := "%" + keyword + "%"
|
||||||
|
query = query.Where("firstname ILIKE ? OR lastname ILIKE ? OR phone ILIKE ?", like, like, like)
|
||||||
|
}
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
if err := query.Count(&total).Error; err != nil {
|
||||||
|
return utils.Internal(c, "failed to count customers")
|
||||||
|
}
|
||||||
|
|
||||||
|
var customers []models.AppCustomer
|
||||||
|
if err := query.Offset(offset).Limit(pagesize).Find(&customers).Error; err != nil {
|
||||||
|
return utils.Internal(c, "failed to fetch customers")
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := make([]int, 0, len(customers))
|
||||||
|
for _, cust := range customers {
|
||||||
|
ids = append(ids, cust.Appcustomerid)
|
||||||
|
}
|
||||||
|
|
||||||
|
bookingCounts := make(map[int]int64, len(ids))
|
||||||
|
if len(ids) > 0 {
|
||||||
|
type countRow struct {
|
||||||
|
Appcustomerid int
|
||||||
|
Cnt int64
|
||||||
|
}
|
||||||
|
var rows []countRow
|
||||||
|
if err := db.DB.Model(&models.PickupBooking{}).
|
||||||
|
Select("appcustomerid, count(*) as cnt").
|
||||||
|
Where("appcustomerid IN ?", ids).
|
||||||
|
Group("appcustomerid").
|
||||||
|
Scan(&rows).Error; err == nil {
|
||||||
|
for _, r := range rows {
|
||||||
|
bookingCounts[r.Appcustomerid] = r.Cnt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make([]fiber.Map, 0, len(customers))
|
||||||
|
for _, cust := range customers {
|
||||||
|
data = append(data, fiber.Map{
|
||||||
|
"appcustomerid": cust.Appcustomerid,
|
||||||
|
"name": strings.TrimSpace(cust.Firstname + " " + cust.Lastname),
|
||||||
|
"phone": cust.Phone,
|
||||||
|
"email": cust.Email,
|
||||||
|
"createdat": cust.Createdat,
|
||||||
|
"totalbookings": bookingCounts[cust.Appcustomerid],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pages := int(math.Ceil(float64(total) / float64(pagesize)))
|
||||||
|
|
||||||
|
return c.JSON(fiber.Map{
|
||||||
|
"success": true,
|
||||||
|
"data": data,
|
||||||
|
"total": total,
|
||||||
|
"pageno": pageno,
|
||||||
|
"pagesize": pagesize,
|
||||||
|
"pages": pages,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func CreateTenantCustomer(c *fiber.Ctx) error {
|
func CreateTenantCustomer(c *fiber.Ctx) error {
|
||||||
req := new(dto.TenantCustomerCreateRequest)
|
req := new(dto.TenantCustomerCreateRequest)
|
||||||
if err := c.BodyParser(req); err != nil {
|
if err := c.BodyParser(req); err != nil {
|
||||||
@@ -891,11 +965,31 @@ func AssignMilerVehicle(c *fiber.Ctx) error {
|
|||||||
// --------------------
|
// --------------------
|
||||||
|
|
||||||
func GetAdminBookings(c *fiber.Ctx) error {
|
func GetAdminBookings(c *fiber.Ctx) error {
|
||||||
|
pageno := max(1, c.QueryInt("pageno", 1))
|
||||||
|
pagesize := min(100, max(1, c.QueryInt("pagesize", 20)))
|
||||||
|
offset := (pageno - 1) * pagesize
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
if err := db.DB.Model(&models.PickupBooking{}).Count(&total).Error; err != nil {
|
||||||
|
return utils.Internal(c, "failed to count bookings")
|
||||||
|
}
|
||||||
|
|
||||||
var bookings []models.PickupBooking
|
var bookings []models.PickupBooking
|
||||||
if err := db.DB.Preload("Parcels").Preload("ServiceOptions").Find(&bookings).Error; err != nil {
|
if err := db.DB.Preload("Parcels").Preload("ServiceOptions").
|
||||||
|
Offset(offset).Limit(pagesize).Find(&bookings).Error; err != nil {
|
||||||
return utils.Internal(c, "failed to fetch bookings")
|
return utils.Internal(c, "failed to fetch bookings")
|
||||||
}
|
}
|
||||||
return utils.List(c, bookings, int64(len(bookings)))
|
|
||||||
|
pages := int(math.Ceil(float64(total) / float64(pagesize)))
|
||||||
|
|
||||||
|
return c.JSON(fiber.Map{
|
||||||
|
"success": true,
|
||||||
|
"data": bookings,
|
||||||
|
"total": total,
|
||||||
|
"pageno": pageno,
|
||||||
|
"pagesize": pagesize,
|
||||||
|
"pages": pages,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateCRMBooking(c *fiber.Ctx) error {
|
func CreateCRMBooking(c *fiber.Ctx) error {
|
||||||
@@ -1207,6 +1301,59 @@ func AdminUpdateBookingStatus(c *fiber.Ctx) error {
|
|||||||
return utils.OK(c, booking)
|
return utils.OK(c, booking)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AdminCancelBooking cancels a booking on behalf of operations, frees up the
|
||||||
|
// assigned miler (if any), and notifies downstream systems (NATS + customer push).
|
||||||
|
// A booking that has already been converted to a consignment (shipped) or is
|
||||||
|
// already cancelled cannot be cancelled again.
|
||||||
|
func AdminCancelBooking(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return utils.BadRequest(c, "invalid booking ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
var booking models.PickupBooking
|
||||||
|
if err := db.DB.First(&booking, id).Error; err != nil {
|
||||||
|
return utils.NotFound(c, "booking not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if booking.Status == constants.BookingConvertedConsignment || booking.Status == constants.BookingCancelled {
|
||||||
|
return utils.BadRequest(c, "cannot cancel a delivered or already cancelled booking")
|
||||||
|
}
|
||||||
|
|
||||||
|
booking.Status = constants.BookingCancelled
|
||||||
|
booking.Updatedat = time.Now()
|
||||||
|
if err := db.DB.Save(&booking).Error; err != nil {
|
||||||
|
return utils.Internal(c, "failed to cancel booking")
|
||||||
|
}
|
||||||
|
|
||||||
|
if booking.Assignedmileruserid != nil {
|
||||||
|
db.DB.Model(&models.MilerProfile{}).
|
||||||
|
Where("userid = ?", *booking.Assignedmileruserid).
|
||||||
|
Update("availabilitystatus", constants.MilerAvailable)
|
||||||
|
}
|
||||||
|
|
||||||
|
if db.Js != nil {
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"bookingid": booking.Bookingid,
|
||||||
|
"reason": "admin_cancelled",
|
||||||
|
}
|
||||||
|
if data, err := json.Marshal(payload); err == nil {
|
||||||
|
if _, err := db.Js.Publish("booking.cancelled", data); err != nil {
|
||||||
|
utils.Warn("AdminCancelBooking: NATS publish failed", "booking_id", booking.Bookingid, "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var customer models.AppCustomer
|
||||||
|
if err := db.DB.Where("appcustomerid = ?", booking.Appcustomerid).First(&customer).Error; err == nil && customer.Devicetoken != "" {
|
||||||
|
if err := notify.SendToDevice(customer.Devicetoken, "Booking Cancelled", "Your booking has been cancelled by operations", nil); err != nil {
|
||||||
|
utils.Warn("AdminCancelBooking: failed to notify customer", "booking_id", booking.Bookingid, "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return utils.Message(c, "booking cancelled")
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------
|
// --------------------
|
||||||
// CONSIGNMENTS
|
// CONSIGNMENTS
|
||||||
// --------------------
|
// --------------------
|
||||||
|
|||||||
@@ -162,6 +162,9 @@ func RegisterRoutes(app *fiber.App, cfg *config.Config) {
|
|||||||
adminAuth.Put("/tenantcustomers/:id", controllers.UpdateTenantCustomer)
|
adminAuth.Put("/tenantcustomers/:id", controllers.UpdateTenantCustomer)
|
||||||
adminAuth.Delete("/tenantcustomers/:id", controllers.DeleteTenantCustomer)
|
adminAuth.Delete("/tenantcustomers/:id", controllers.DeleteTenantCustomer)
|
||||||
|
|
||||||
|
// B2C App customers
|
||||||
|
adminAuth.Get("/customers", controllers.GetAdminCustomers)
|
||||||
|
|
||||||
// Hubs
|
// Hubs
|
||||||
adminAuth.Get("/hubs", controllers.GetHubs)
|
adminAuth.Get("/hubs", controllers.GetHubs)
|
||||||
adminAuth.Post("/hubs", controllers.CreateHub)
|
adminAuth.Post("/hubs", controllers.CreateHub)
|
||||||
@@ -191,6 +194,7 @@ func RegisterRoutes(app *fiber.App, cfg *config.Config) {
|
|||||||
adminAuth.Post("/bookings/:id/assign-miler", controllers.AdminAssignMiler)
|
adminAuth.Post("/bookings/:id/assign-miler", controllers.AdminAssignMiler)
|
||||||
adminAuth.Post("/bookings/:id/assign-vehicle", controllers.AdminAssignVehicle)
|
adminAuth.Post("/bookings/:id/assign-vehicle", controllers.AdminAssignVehicle)
|
||||||
adminAuth.Put("/bookings/:id/status", controllers.AdminUpdateBookingStatus)
|
adminAuth.Put("/bookings/:id/status", controllers.AdminUpdateBookingStatus)
|
||||||
|
adminAuth.Post("/bookings/:id/cancel", controllers.AdminCancelBooking)
|
||||||
|
|
||||||
// Consignments
|
// Consignments
|
||||||
adminAuth.Get("/consignments", controllers.GetAdminConsignments)
|
adminAuth.Get("/consignments", controllers.GetAdminConsignments)
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"gorm.io/driver/postgres"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AppUser struct {
|
|
||||||
Userid int `gorm:"primaryKey;column:userid"`
|
|
||||||
Email string `gorm:"column:email"`
|
|
||||||
Password string `gorm:"column:password"`
|
|
||||||
Roleid int `gorm:"column:roleid"`
|
|
||||||
Authname string `gorm:"column:authname"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (AppUser) TableName() string {
|
|
||||||
return "appusers"
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
dsn := "host=31.97.228.132 user=admin password=Package@321# dbname=logistics port=5433 sslmode=disable"
|
|
||||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("failed to connect database: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var users []AppUser
|
|
||||||
result := db.Find(&users)
|
|
||||||
if result.Error != nil {
|
|
||||||
log.Fatalf("failed to query users: %v", result.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(users) == 0 {
|
|
||||||
fmt.Println("DATABASE IS EMPTY. No users found.")
|
|
||||||
// Create default admin user
|
|
||||||
hash, _ := bcrypt.GenerateFromPassword([]byte("admin123"), bcrypt.DefaultCost)
|
|
||||||
|
|
||||||
admin := AppUser{
|
|
||||||
Email: "admin@doormile.com",
|
|
||||||
Password: string(hash),
|
|
||||||
Roleid: 1, // Admin
|
|
||||||
Authname: "Super Admin",
|
|
||||||
}
|
|
||||||
if err := db.Create(&admin).Error; err != nil {
|
|
||||||
log.Fatalf("Failed to create admin: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Println("SUCCESS: Created default admin: admin@doormile.com / admin123")
|
|
||||||
} else {
|
|
||||||
fmt.Printf("Found %d users:\n", len(users))
|
|
||||||
for _, u := range users {
|
|
||||||
fmt.Printf("- %s (Email: %s, RoleID: %d)\n", u.Authname, u.Email, u.Roleid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user