diff --git a/controllers/adminController.go b/controllers/adminController.go index 05c754f..daa7fcb 100644 --- a/controllers/adminController.go +++ b/controllers/adminController.go @@ -7,6 +7,7 @@ import ( "fmt" "math" "strconv" + "strings" "time" "doormile/config" @@ -442,6 +443,79 @@ func GetTenantCustomers(c *fiber.Ctx) error { 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 { req := new(dto.TenantCustomerCreateRequest) if err := c.BodyParser(req); err != nil { @@ -891,11 +965,31 @@ func AssignMilerVehicle(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 - 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.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 { @@ -1207,6 +1301,59 @@ func AdminUpdateBookingStatus(c *fiber.Ctx) error { 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 // -------------------- diff --git a/routes/routes.go b/routes/routes.go index 3809d75..ae442bb 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -162,6 +162,9 @@ func RegisterRoutes(app *fiber.App, cfg *config.Config) { adminAuth.Put("/tenantcustomers/:id", controllers.UpdateTenantCustomer) adminAuth.Delete("/tenantcustomers/:id", controllers.DeleteTenantCustomer) + // B2C App customers + adminAuth.Get("/customers", controllers.GetAdminCustomers) + // Hubs adminAuth.Get("/hubs", controllers.GetHubs) 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-vehicle", controllers.AdminAssignVehicle) adminAuth.Put("/bookings/:id/status", controllers.AdminUpdateBookingStatus) + adminAuth.Post("/bookings/:id/cancel", controllers.AdminCancelBooking) // Consignments adminAuth.Get("/consignments", controllers.GetAdminConsignments) diff --git a/scratch/check_users.go b/scratch/check_users.go deleted file mode 100644 index 42a51e3..0000000 --- a/scratch/check_users.go +++ /dev/null @@ -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) - } - } -}