package controllers import ( "context" "crypto/rand" "encoding/json" "fmt" "math" "strconv" "time" "doormile/config" "doormile/constants" "doormile/db" "doormile/dto" "doormile/internal/assignment" "doormile/internal/notify" "doormile/models" "doormile/utils" "github.com/gofiber/fiber/v2" "github.com/redis/go-redis/v9" ) // Helper to generate tripsheet number func generateTripsheetNo() string { b := make([]byte, 4) rand.Read(b) return fmt.Sprintf("DM-TS-%X-%d", b, time.Now().Unix()%100000) } func LoginAdmin(cfg *config.Config) fiber.Handler { return func(c *fiber.Ctx) error { req := new(dto.AdminLoginRequest) if err := c.BodyParser(req); err != nil { return utils.BadRequest(c, "invalid request body") } if req.Email == "" || req.Password == "" { return utils.BadRequest(c, "email and password are required") } var auth models.DoormileAuth if err := db.DB.Where("email = ?", req.Email).First(&auth).Error; err != nil { return utils.Unauthorized(c, "incorrect email or password") } if auth.Role != "admin" && auth.Role != "manager" && auth.Role != "executive" { return utils.Forbidden(c, "access restricted to admin console users") } if !utils.CheckPasswordHash(req.Password, auth.PasswordHash) { return utils.Unauthorized(c, "incorrect email or password") } roleId := 3 if auth.Role == "admin" { roleId = 1 } else if auth.Role == "executive" { roleId = 4 } var appUser models.AppUser db.DB.Where("email = ?", req.Email).First(&appUser) userName := "Admin" if appUser.Authname != "" { userName = appUser.Authname } // Important: use appUser.Userid instead of auth.ID to ensure consistent IDs across the system token, err := utils.GenerateToken(int(appUser.Userid), auth.Email, roleId, 0, 1, cfg.JWTSecret) if err != nil { return utils.Internal(c, "failed to generate token") } return c.JSON(fiber.Map{ "success": true, "token": token, "user": fiber.Map{ "id": appUser.Userid, "name": userName, "email": auth.Email, "role": auth.Role, }, }) } } func GetAdminDashboard(c *fiber.Ctx) error { var totalTenants int64 var totalCustomers int64 var totalMilers int64 var totalBookings int64 var totalConsignments int64 var openExceptions int64 db.DB.Model(&models.Tenant{}).Count(&totalTenants) db.DB.Model(&models.AppCustomer{}).Count(&totalCustomers) db.DB.Model(&models.AppUser{}).Where("roleid = 5").Count(&totalMilers) db.DB.Model(&models.PickupBooking{}).Count(&totalBookings) db.DB.Model(&models.Consignment{}).Count(&totalConsignments) db.DB.Model(&models.ConsignmentException{}).Where("status = ?", "Open").Count(&openExceptions) return utils.OK(c, fiber.Map{ "tenants": totalTenants, "customers": totalCustomers, "milers": totalMilers, "bookings": totalBookings, "consignments": totalConsignments, "exceptions": openExceptions, }) } // -------------------- // APP USERS MANAGEMENT // -------------------- func GetAppUsers(c *fiber.Ctx) error { var users []models.AppUser // Exclude Milers (Roleid = 5) from the CRM user list if err := db.DB.Where("roleid != ?", 5).Find(&users).Error; err != nil { return utils.Internal(c, "failed to fetch users") } response := make([]fiber.Map, 0, len(users)) for _, u := range users { roleName := "unknown" if u.Roleid == 1 { roleName = "admin" } else if u.Roleid == 3 { roleName = "manager" } else if u.Roleid == 4 { roleName = "rep" } else if u.Roleid == 5 { roleName = "miler" } response = append(response, fiber.Map{ "id": u.Userid, "first_name": u.Authname, "email": u.Email, "phone": u.Contactno, "role": roleName, "status": u.Status, }) } return utils.List(c, response, int64(len(response))) } func CreateAppUser(c *fiber.Ctx) error { type UserRequest struct { FirstName string `json:"first_name"` Email string `json:"email"` Phone string `json:"phone"` Role string `json:"role"` Password string `json:"password"` } req := new(UserRequest) if err := c.BodyParser(req); err != nil { return utils.BadRequest(c, "invalid request body") } roleId := 4 if req.Role == "admin" { roleId = 1 } else if req.Role == "manager" { roleId = 3 } else if req.Role == "rep" { roleId = 4 } passHash, _ := utils.HashPassword(req.Password) if passHash == "" { passHash, _ = utils.HashPassword("defaultPassword123") } user := models.AppUser{ Authname: req.FirstName, Email: req.Email, Contactno: req.Phone, Password: passHash, Roleid: roleId, Status: "Active", Applocationid: 1, } if err := db.DB.Create(&user).Error; err != nil { return utils.Internal(c, "failed to create user") } return utils.Created(c, fiber.Map{ "id": user.Userid, "first_name": user.Authname, "email": user.Email, "phone": user.Contactno, "role": req.Role, }) } func UpdateAppUser(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) var user models.AppUser if err := db.DB.First(&user, id).Error; err != nil { return utils.NotFound(c, "user not found") } type UserRequest struct { FirstName string `json:"first_name"` Email string `json:"email"` Phone string `json:"phone"` Role string `json:"role"` } req := new(UserRequest) if err := c.BodyParser(req); err != nil { return utils.BadRequest(c, "invalid request body") } if req.FirstName != "" { user.Authname = req.FirstName } if req.Email != "" { user.Email = req.Email } if req.Phone != "" { user.Contactno = req.Phone } if req.Role != "" { if req.Role == "admin" { user.Roleid = 1 } else if req.Role == "manager" { user.Roleid = 3 } else if req.Role == "rep" { user.Roleid = 4 } } user.Updatedat = time.Now() if err := db.DB.Save(&user).Error; err != nil { return utils.Internal(c, "failed to update user") } return utils.OK(c, fiber.Map{ "id": user.Userid, "first_name": user.Authname, "email": user.Email, "phone": user.Contactno, "role": req.Role, }) } func DeleteAppUser(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) if err := db.DB.Delete(&models.AppUser{}, id).Error; err != nil { return utils.Internal(c, "failed to delete user") } return utils.Message(c, "user deleted successfully") } // -------------------- // TENANT MANAGEMENT // -------------------- func GetTenants(c *fiber.Ctx) error { var tenants []models.Tenant if err := db.DB.Find(&tenants).Error; err != nil { return utils.Internal(c, "failed to fetch tenants") } return utils.List(c, tenants, int64(len(tenants))) } func CreateTenant(c *fiber.Ctx) error { req := new(dto.TenantCreateRequest) if err := c.BodyParser(req); err != nil { return utils.BadRequest(c, "invalid request body") } tenant := models.Tenant{ Tenantname: req.Tenantname, Primaryemail: req.Primaryemail, Primarycontact: req.Primarycontact, Status: req.Status, } if tenant.Status == "" { tenant.Status = "Active" } if err := db.DB.Create(&tenant).Error; err != nil { return utils.Internal(c, "failed to create tenant") } return utils.Created(c, tenant) } func GetTenantDetails(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) var tenant models.Tenant if err := db.DB.First(&tenant, id).Error; err != nil { return utils.NotFound(c, "tenant not found") } return utils.OK(c, tenant) } func UpdateTenant(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) var tenant models.Tenant if err := db.DB.First(&tenant, id).Error; err != nil { return utils.NotFound(c, "tenant not found") } req := new(dto.TenantCreateRequest) if err := c.BodyParser(req); err != nil { return utils.BadRequest(c, "invalid request body") } if req.Tenantname != "" { tenant.Tenantname = req.Tenantname } if req.Primaryemail != "" { tenant.Primaryemail = req.Primaryemail } if req.Primarycontact != "" { tenant.Primarycontact = req.Primarycontact } if req.Status != "" { tenant.Status = req.Status } tenant.Updatedat = time.Now() if err := db.DB.Save(&tenant).Error; err != nil { return utils.Internal(c, "failed to update tenant") } return utils.OK(c, tenant) } func DeleteTenant(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) var tenant models.Tenant if err := db.DB.First(&tenant, id).Error; err != nil { return utils.NotFound(c, "tenant not found") } if err := db.DB.Delete(&tenant).Error; err != nil { return utils.Internal(c, "failed to delete tenant") } return utils.Message(c, "tenant deleted successfully") } func GetTenantLocations(c *fiber.Ctx) error { tenantID, _ := strconv.Atoi(c.Params("id")) var locations []models.TenantLocation if err := db.DB.Where("tenantid = ?", tenantID).Find(&locations).Error; err != nil { return utils.Internal(c, "failed to fetch locations") } return utils.List(c, locations, int64(len(locations))) } func CreateTenantLocation(c *fiber.Ctx) error { tenantID, _ := strconv.Atoi(c.Params("id")) req := new(dto.TenantLocationCreateRequest) if err := c.BodyParser(req); err != nil { return utils.BadRequest(c, "invalid request body") } location := models.TenantLocation{ Tenantid: tenantID, Address: req.Address, City: req.City, State: req.State, Pincode: req.Pincode, Latitude: req.Latitude, Longitude: req.Longitude, Isprimary: req.Isprimary, Status: req.Status, } if location.Status == "" { location.Status = "Active" } if req.Isprimary { db.DB.Model(&models.TenantLocation{}).Where("tenantid = ?", tenantID).Update("isprimary", false) } if err := db.DB.Create(&location).Error; err != nil { return utils.Internal(c, "failed to create location") } return utils.Created(c, location) } func UpdateTenantLocation(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) var location models.TenantLocation if err := db.DB.First(&location, id).Error; err != nil { return utils.NotFound(c, "tenant location not found") } req := new(dto.TenantLocationCreateRequest) if err := c.BodyParser(req); err != nil { return utils.BadRequest(c, "invalid request body") } if req.Address != "" { location.Address = req.Address } if req.City != "" { location.City = req.City } if req.State != "" { location.State = req.State } if req.Pincode != "" { location.Pincode = req.Pincode } if req.Latitude != 0 { location.Latitude = req.Latitude } if req.Longitude != 0 { location.Longitude = req.Longitude } location.Isprimary = req.Isprimary if req.Status != "" { location.Status = req.Status } location.Updatedat = time.Now() if req.Isprimary { db.DB.Model(&models.TenantLocation{}).Where("tenantid = ?", location.Tenantid).Update("isprimary", false) } if err := db.DB.Save(&location).Error; err != nil { return utils.Internal(c, "failed to update location") } return utils.OK(c, location) } // -------------------- // TENANT CUSTOMERS // -------------------- func GetTenantCustomers(c *fiber.Ctx) error { var customers []models.Customer if err := db.DB.Find(&customers).Error; err != nil { return utils.Internal(c, "failed to fetch customers") } return utils.List(c, customers, int64(len(customers))) } func CreateTenantCustomer(c *fiber.Ctx) error { req := new(dto.TenantCustomerCreateRequest) if err := c.BodyParser(req); err != nil { return utils.BadRequest(c, "invalid request body") } customer := models.Customer{ Firstname: req.Firstname, Lastname: req.Lastname, Contactno: req.Phone, Email: req.Email, Status: 1, Createdat: time.Now(), Updatedat: time.Now(), } if err := db.DB.Create(&customer).Error; err != nil { return utils.Internal(c, "failed to create customer") } return utils.Created(c, customer) } func GetTenantCustomerDetails(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) var customer models.Customer if err := db.DB.First(&customer, id).Error; err != nil { return utils.NotFound(c, "customer not found") } return utils.OK(c, customer) } func UpdateTenantCustomer(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) var customer models.Customer if err := db.DB.First(&customer, id).Error; err != nil { return utils.NotFound(c, "customer not found") } req := new(dto.TenantCustomerCreateRequest) if err := c.BodyParser(req); err != nil { return utils.BadRequest(c, "invalid request body") } if req.Firstname != "" { customer.Firstname = req.Firstname } if req.Lastname != "" { customer.Lastname = req.Lastname } if req.Phone != "" { customer.Contactno = req.Phone } if req.Email != "" { customer.Email = req.Email } customer.Updatedat = time.Now() if err := db.DB.Save(&customer).Error; err != nil { return utils.Internal(c, "failed to update customer") } return utils.OK(c, customer) } func DeleteTenantCustomer(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) var customer models.Customer if err := db.DB.First(&customer, id).Error; err != nil { return utils.NotFound(c, "customer not found") } if err := db.DB.Delete(&customer).Error; err != nil { return utils.Internal(c, "failed to delete customer") } return utils.Message(c, "customer deleted successfully") } // -------------------- // HUBS CRUD // -------------------- func GetHubs(c *fiber.Ctx) error { var hubs []models.Hub query := db.DB.Where("deletedat IS NULL") if appLocationID := c.Query("applocationid"); appLocationID != "" { query = query.Where("applocationid = ?", appLocationID) } if status := c.Query("status"); status != "" { query = query.Where("status = ?", status) } if hubType := c.Query("hubtype"); hubType != "" { query = query.Where("hubtype = ?", hubType) } if err := query.Find(&hubs).Error; err != nil { return utils.Internal(c, "failed to fetch hubs") } return utils.List(c, hubs, int64(len(hubs))) } func CreateHub(c *fiber.Ctx) error { req := new(dto.HubCreateRequest) if err := c.BodyParser(req); err != nil { return utils.BadRequest(c, "invalid request body") } hub := models.Hub{ Hubname: req.Hubname, Hubtype: req.Hubtype, Applocationid: req.Applocationid, Contactno: req.Contactno, Address: req.Address, Latitude: req.Latitude, Longitude: req.Longitude, Pincode: req.Pincode, Status: req.Status, } if hub.Status == "" { hub.Status = "Active" } if err := db.DB.Create(&hub).Error; err != nil { return utils.Internal(c, "failed to create hub") } return utils.Created(c, hub) } func GetHubDetails(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) var hub models.Hub if err := db.DB.Where("hubid = ? AND deletedat IS NULL", id).First(&hub).Error; err != nil { return utils.NotFound(c, "hub not found") } return utils.OK(c, hub) } func UpdateHub(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) var hub models.Hub if err := db.DB.Where("hubid = ? AND deletedat IS NULL", id).First(&hub).Error; err != nil { return utils.NotFound(c, "hub not found") } req := new(dto.HubCreateRequest) if err := c.BodyParser(req); err != nil { return utils.BadRequest(c, "invalid request body") } if req.Hubname != "" { hub.Hubname = req.Hubname } if req.Hubtype != "" { hub.Hubtype = req.Hubtype } if req.Applocationid != 0 { hub.Applocationid = req.Applocationid } if req.Address != "" { hub.Address = req.Address } if req.Contactno != "" { hub.Contactno = req.Contactno } if req.Latitude != 0 { hub.Latitude = req.Latitude } if req.Longitude != 0 { hub.Longitude = req.Longitude } if req.Pincode != "" { hub.Pincode = req.Pincode } if req.Status != "" { hub.Status = req.Status } hub.Updatedat = time.Now() db.DB.Save(&hub) return utils.OK(c, hub) } func DeleteHub(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) var hub models.Hub if err := db.DB.Where("hubid = ? AND deletedat IS NULL", id).First(&hub).Error; err != nil { return utils.NotFound(c, "hub not found") } now := time.Now() hub.Deletedat = &now db.DB.Save(&hub) return utils.Message(c, "hub deleted successfully") } // -------------------- // VEHICLES CRUD // -------------------- func GetVehicles(c *fiber.Ctx) error { var vehicles []models.Vehicle if err := db.DB.Where("deletedat IS NULL").Find(&vehicles).Error; err != nil { return utils.Internal(c, "failed to fetch vehicles") } return utils.List(c, vehicles, int64(len(vehicles))) } func CreateVehicle(c *fiber.Ctx) error { req := new(dto.VehicleCreateRequest) if err := c.BodyParser(req); err != nil { return utils.BadRequest(c, "invalid request body") } vehicle := models.Vehicle{ Vehicleno: req.Vehicleno, Vehicletype: req.Vehicletype, Maxweight: req.Maxweight, Maxvolume: req.Maxvolume, Partnerid: req.Partnerid, Batterypercentage: req.Batterypercentage, Status: req.Status, } if vehicle.Status == "" { vehicle.Status = "Available" } if err := db.DB.Create(&vehicle).Error; err != nil { return utils.Internal(c, "failed to create vehicle") } return utils.Created(c, vehicle) } func GetVehicleDetails(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) var vehicle models.Vehicle if err := db.DB.Where("vehicleid = ? AND deletedat IS NULL", id).First(&vehicle).Error; err != nil { return utils.NotFound(c, "vehicle not found") } return utils.OK(c, vehicle) } func UpdateVehicle(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) var vehicle models.Vehicle if err := db.DB.Where("vehicleid = ? AND deletedat IS NULL", id).First(&vehicle).Error; err != nil { return utils.NotFound(c, "vehicle not found") } req := new(dto.VehicleCreateRequest) if err := c.BodyParser(req); err != nil { return utils.BadRequest(c, "invalid request body") } if req.Vehicleno != "" { vehicle.Vehicleno = req.Vehicleno } if req.Vehicletype != "" { vehicle.Vehicletype = req.Vehicletype } if req.Maxweight != 0 { vehicle.Maxweight = req.Maxweight } if req.Maxvolume != 0 { vehicle.Maxvolume = req.Maxvolume } vehicle.Partnerid = req.Partnerid if req.Batterypercentage != 0 { vehicle.Batterypercentage = req.Batterypercentage } if req.Status != "" { vehicle.Status = req.Status } vehicle.Updatedat = time.Now() if err := db.DB.Save(&vehicle).Error; err != nil { return utils.Internal(c, "failed to update vehicle") } return utils.OK(c, vehicle) } func DeleteVehicle(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) var vehicle models.Vehicle if err := db.DB.Where("vehicleid = ? AND deletedat IS NULL", id).First(&vehicle).Error; err != nil { return utils.NotFound(c, "vehicle not found") } now := time.Now() vehicle.Deletedat = &now db.DB.Save(&vehicle) return utils.Message(c, "vehicle deleted successfully") } // -------------------- // MILERS MANAGEMENT // -------------------- func GetMilers(c *fiber.Ctx) error { var profiles []models.MilerProfile query := db.DB if appLocStr := c.Query("applocationid"); appLocStr != "" { query = query.Where("applocationid = ?", appLocStr) } if err := query.Find(&profiles).Error; err != nil { return utils.Internal(c, "failed to fetch milers") } return utils.List(c, profiles, int64(len(profiles))) } func CreateMiler(c *fiber.Ctx) error { req := new(dto.MilerCreateRequest) if err := c.BodyParser(req); err != nil { return utils.BadRequest(c, "invalid request body") } passHash, _ := utils.HashPassword(req.Password) tx := db.DB.Begin() appLocID := req.Applocationid if appLocID == 0 { appLocID = 1 } user := models.AppUser{ Authname: req.Authname, Email: req.Email, Contactno: req.Contactno, Password: passHash, Roleid: 5, // Miler Status: "Active", Applocationid: appLocID, } if err := tx.Create(&user).Error; err != nil { tx.Rollback() return utils.Internal(c, "failed to create miler account") } profile := models.MilerProfile{ Userid: user.Userid, Displayname: req.Displayname, Phone: req.Contactno, Defaultvehicletype: req.Defaultvehicletype, Availabilitystatus: constants.MilerOffline, Rating: 5.00, Applocationid: appLocID, } if err := tx.Create(&profile).Error; err != nil { tx.Rollback() return utils.Internal(c, "failed to create miler profile") } tx.Commit() return utils.Created(c, profile) } func GetMilerDetails(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) var profile models.MilerProfile if err := db.DB.Where("milerprofileid = ?", id).First(&profile).Error; err != nil { return utils.NotFound(c, "miler not found") } return utils.OK(c, profile) } func UpdateMiler(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) var profile models.MilerProfile if err := db.DB.Where("milerprofileid = ?", id).First(&profile).Error; err != nil { return utils.NotFound(c, "miler not found") } type MilerUpdate struct { Displayname string `json:"displayname"` Defaultvehicletype string `json:"defaultvehicletype"` Hubid *int `json:"hubid"` } req := new(MilerUpdate) if err := c.BodyParser(req); err != nil { return utils.BadRequest(c, "invalid request body") } if req.Displayname != "" { profile.Displayname = req.Displayname } if req.Defaultvehicletype != "" { profile.Defaultvehicletype = req.Defaultvehicletype } if req.Hubid != nil { profile.Hubid = req.Hubid } profile.Updatedat = time.Now() if err := db.DB.Save(&profile).Error; err != nil { return utils.Internal(c, "failed to update miler") } return utils.OK(c, profile) } func BlockMiler(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) tx := db.DB.Begin() var profile models.MilerProfile if err := tx.Where("milerprofileid = ?", id).First(&profile).Error; err != nil { tx.Rollback() return utils.NotFound(c, "miler not found") } profile.Availabilitystatus = constants.MilerBlocked profile.Updatedat = time.Now() tx.Save(&profile) tx.Model(&models.AppUser{}).Where("userid = ?", profile.Userid).Update("status", "Blocked") tx.Commit() return utils.Message(c, "miler blocked successfully") } func AssignMilerVehicle(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) type VehicleAssign struct { Vehicleid int `json:"vehicleid"` } req := new(VehicleAssign) if err := c.BodyParser(req); err != nil { return utils.BadRequest(c, "invalid request body") } var profile models.MilerProfile if err := db.DB.Where("milerprofileid = ?", id).First(&profile).Error; err != nil { return utils.NotFound(c, "miler not found") } profile.Vehicleid = &req.Vehicleid profile.Updatedat = time.Now() db.DB.Save(&profile) return utils.OK(c, profile) } // -------------------- // BOOKINGS MANAGEMENT // -------------------- func GetAdminBookings(c *fiber.Ctx) error { var bookings []models.PickupBooking if err := db.DB.Preload("Parcels").Preload("ServiceOptions").Find(&bookings).Error; err != nil { return utils.Internal(c, "failed to fetch bookings") } return utils.List(c, bookings, int64(len(bookings))) } func CreateCRMBooking(c *fiber.Ctx) error { type AdminBookingRequest struct { Appcustomerid int `json:"appcustomerid"` CustomerPhone string `json:"customer_phone"` CustomerName string `json:"customer_name"` Pickupaddress string `json:"pickupaddress"` Pickuppincode string `json:"pickuppincode"` Pickuplatitude float64 `json:"pickuplatitude"` Pickuplongitude float64 `json:"pickuplongitude"` Deliveryaddress string `json:"deliveryaddress"` Deliverypincode string `json:"deliverypincode"` Deliverycity string `json:"deliverycity"` Deliverylatitude float64 `json:"deliverylatitude"` Deliverylongitude float64 `json:"deliverylongitude"` Providercompany string `json:"providercompany"` Providerlocation string `json:"providerlocation"` Notes string `json:"notes"` ServiceOption string `json:"service_option"` Finalprice float64 `json:"finalprice"` Insuranceamount float64 `json:"insuranceamount"` Preferredpickupfrom *time.Time `json:"preferredpickupfrom"` Preferredpickupto *time.Time `json:"preferredpickupto"` Parcels []dto.ParcelRequest `json:"parcels"` } req := new(AdminBookingRequest) if err := c.BodyParser(req); err != nil { return utils.BadRequest(c, "invalid request body") } if req.Pickupaddress == "" || req.Pickuppincode == "" { return utils.BadRequest(c, "pickup address and pincode are required") } if len(req.Parcels) == 0 { return utils.BadRequest(c, "at least one parcel is required") } tx := db.DB.Begin() customerID := req.Appcustomerid if customerID == 0 && req.CustomerPhone != "" { // Try to find customer by phone var customer models.AppCustomer if err := tx.Where("phone = ?", req.CustomerPhone).First(&customer).Error; err == nil { customerID = customer.Appcustomerid } else { // Create dummy customer newCustomer := models.AppCustomer{ Firstname: req.CustomerName, Phone: req.CustomerPhone, Status: "Active", Configid: 1001, } if req.CustomerName == "" { newCustomer.Firstname = "Guest" } if err := tx.Create(&newCustomer).Error; err != nil { tx.Rollback() return utils.Internal(c, "failed to create customer record") } customerID = newCustomer.Appcustomerid } } booking := models.PickupBooking{ Bookingno: generateBookingNo(), Appcustomerid: customerID, Pickupaddress: req.Pickupaddress, Pickuppincode: req.Pickuppincode, Pickuplatitude: req.Pickuplatitude, Pickuplongitude: req.Pickuplongitude, Deliveryaddress: req.Deliveryaddress, Deliverypincode: req.Deliverypincode, Deliverycity: req.Deliverycity, Deliverylatitude: req.Deliverylatitude, Deliverylongitude: req.Deliverylongitude, Bookingsource: "CRM_Console", Providercompany: req.Providercompany, Providerlocation: req.Providerlocation, Notes: req.Notes, Status: constants.BookingPendingPickup, Preferredpickupfrom: req.Preferredpickupfrom, Preferredpickupto: req.Preferredpickupto, } if err := tx.Create(&booking).Error; err != nil { tx.Rollback() return utils.Internal(c, "failed to create booking") } var totalWeight float64 var totalVolume float64 var requiresLargeVehicle bool for _, p := range req.Parcels { volumetric := calculateVolumetricWeight(p.Length, p.Width, p.Height) totalWeight += math.Max(p.Weight, volumetric) totalVolume += p.Length * p.Width * p.Height parcel := models.BookingParcel{ Bookingid: booking.Bookingid, Itemcategory: p.Itemcategory, Itemdescription: p.Itemdescription, Declaredvalue: p.Declaredvalue, Weight: p.Weight, Length: p.Length, Width: p.Width, Height: p.Height, Isfragile: p.Isfragile, Needsinsurance: p.Needsinsurance, Requireslargevehicle: p.Requireslargevehicle, } if p.Requireslargevehicle { requiresLargeVehicle = true } if p.Needsinsurance { parcel.Insuranceamount = p.Declaredvalue * 0.01 } // If explicit insurance amount is provided from CRM, apply it to the first parcel if req.Insuranceamount > 0 && totalWeight == math.Max(p.Weight, calculateVolumetricWeight(p.Length, p.Width, p.Height)) { parcel.Insuranceamount = req.Insuranceamount parcel.Needsinsurance = true } if err := tx.Create(&parcel).Error; err != nil { tx.Rollback() return utils.Internal(c, "failed to save parcel details") } } var distance float64 if booking.Deliverylatitude != 0 && booking.Deliverylongitude != 0 { distance = calculateDistance(booking.Pickuplatitude, booking.Pickuplongitude, booking.Deliverylatitude, booking.Deliverylongitude) } var pricing models.Pricing err := tx.Where("status = ? AND ? BETWEEN effectivefrom AND effectiveto", "Active", time.Now()).Order("priority DESC").First(&pricing).Error var estimatedPrice float64 var pricingID *int if err == nil { pricingID = &pricing.Pricingid kmExtra := math.Max(0, distance-pricing.Basedistance) kgExtra := math.Max(0, totalWeight-pricing.Baseweight) estimatedPrice = pricing.Baseprice + (kmExtra * pricing.Priceperkm) + (kgExtra * pricing.Priceperkg) + pricing.Handlingcharges } else { estimatedPrice = 50.0 + (distance * 5.0) + (totalWeight * 10.0) } if req.Finalprice > 0 { estimatedPrice = req.Finalprice } serviceType := req.ServiceOption if serviceType == "" { serviceType = "Normal" } if serviceType == "Fast" { estimatedPrice *= 1.25 } else if serviceType == "Superfast" { estimatedPrice *= 1.5 } now := time.Now() estDelivery := now.Add(24 * time.Hour) slaDue := now.Add(36 * time.Hour) if serviceType == "Fast" { estDelivery = now.Add(12 * time.Hour) slaDue = now.Add(18 * time.Hour) } else if serviceType == "Superfast" { estDelivery = now.Add(6 * time.Hour) slaDue = now.Add(9 * time.Hour) } srvOption := models.BookingServiceOption{ Bookingid: booking.Bookingid, Servicetype: serviceType, Estimatedprice: estimatedPrice, Estimateddeliveryat: &estDelivery, Sladueat: &slaDue, Pricingid: pricingID, } if err := tx.Create(&srvOption).Error; err != nil { tx.Rollback() return utils.Internal(c, "failed to save service option") } if requiresLargeVehicle || totalWeight > 20.0 { reqVeh := models.BookingVehicleRequirement{ Bookingid: booking.Bookingid, Requiredvehicletype: "truck", Reason: "Oversized package / heavy weight", Status: "Required", } tx.Create(&reqVeh) } tx.Commit() go assignment.AssignCRMMiler(booking.Bookingid) if db.Js != nil { payload := map[string]interface{}{ "booking_id": booking.Bookingid, "booking_no": booking.Bookingno, "customer_id": booking.Appcustomerid, "pickup_address": booking.Pickupaddress, "pickup_pincode": booking.Pickuppincode, "status": constants.BookingPendingPickup, "created_at": time.Now().UnixMilli(), } if data, err := json.Marshal(payload); err == nil { db.Js.Publish("api.v1.bookings.create", data) } } db.DB.Preload("Parcels").Preload("ServiceOptions").First(&booking, booking.Bookingid) return utils.Created(c, booking) } func GetAdminBookingDetails(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) var booking models.PickupBooking if err := db.DB.Preload("Parcels").Preload("ServiceOptions").Preload("Payments").First(&booking, id).Error; err != nil { return utils.NotFound(c, "booking not found") } return utils.OK(c, booking) } func AdminAssignMiler(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) type MilerAssign struct { Mileruserid int `json:"mileruserid"` } req := new(MilerAssign) if err := c.BodyParser(req); err != nil { return utils.BadRequest(c, "invalid request body") } adminUserID := c.Locals("userid").(int) booking, err := AssignMilerToBooking(id, req.Mileruserid, &adminUserID) if err != nil { return utils.NotFound(c, "booking not found") } return utils.OK(c, booking) } func AdminAssignVehicle(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) type VehicleAssign struct { Vehicleid int `json:"vehicleid"` Driveruserid int `json:"driveruserid"` } req := new(VehicleAssign) if err := c.BodyParser(req); err != nil { return utils.BadRequest(c, "invalid request body") } var reqVeh models.BookingVehicleRequirement if err := db.DB.Where("bookingid = ? AND status = ?", id, "Required").First(&reqVeh).Error; err != nil { return utils.NotFound(c, "no active vehicle requirement found for this booking") } reqVeh.Assignedvehicleid = &req.Vehicleid reqVeh.Assigneddriveruserid = &req.Driveruserid reqVeh.Status = "Assigned" reqVeh.Updatedat = time.Now() db.DB.Save(&reqVeh) return utils.OK(c, reqVeh) } func AdminUpdateBookingStatus(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) type StatusUpdate struct { Status string `json:"status"` } req := new(StatusUpdate) if err := c.BodyParser(req); err != nil { return utils.BadRequest(c, "invalid request body") } if req.Status == "" { return utils.BadRequest(c, "status is required") } var booking models.PickupBooking if err := db.DB.First(&booking, id).Error; err != nil { return utils.NotFound(c, "booking not found") } booking.Status = req.Status booking.Updatedat = time.Now() db.DB.Save(&booking) return utils.OK(c, booking) } // -------------------- // CONSIGNMENTS // -------------------- func GetAdminConsignments(c *fiber.Ctx) error { var list []models.Consignment if err := db.DB.Find(&list).Error; err != nil { return utils.Internal(c, "failed to fetch consignments") } return utils.List(c, list, int64(len(list))) } func GetAdminConsignmentDetails(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) var csg models.Consignment if err := db.DB.First(&csg, id).Error; err != nil { return utils.NotFound(c, "consignment not found") } return utils.OK(c, csg) } func GetAdminConsignmentTracking(c *fiber.Ctx) error { trackingNo := c.Params("trackingno") var consignment models.Consignment if err := db.DB.Where("trackingno = ?", trackingNo).First(&consignment).Error; err != nil { return utils.NotFound(c, "consignment not found") } var history []models.ConsignmentHistory db.DB.Where("consignmentid = ?", consignment.Consignmentid).Order("createdat DESC").Find(&history) return utils.OK(c, fiber.Map{"consignment": consignment, "history": history}) } func AdminUpdateConsignmentStatus(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) type StatusUpdate struct { Status string `json:"status"` Remarks string `json:"remarks"` } req := new(StatusUpdate) if err := c.BodyParser(req); err != nil { return utils.BadRequest(c, "invalid request body") } if req.Status == "" { return utils.BadRequest(c, "status is required") } tx := db.DB.Begin() var consignment models.Consignment if err := tx.First(&consignment, id).Error; err != nil { tx.Rollback() return utils.NotFound(c, "consignment not found") } consignment.Status = req.Status consignment.Updatedat = time.Now() tx.Save(&consignment) adminUserID := c.Locals("userid").(int) history := models.ConsignmentHistory{ Consignmentid: consignment.Consignmentid, Userid: &adminUserID, Eventstatus: req.Status, Remarks: req.Remarks, } tx.Create(&history) tx.Commit() return utils.OK(c, consignment) } // -------------------- // TRIPSHEETS (MANIFEST) // -------------------- func GetTripsheets(c *fiber.Ctx) error { var list []models.Tripsheet if err := db.DB.Where("deletedat IS NULL").Find(&list).Error; err != nil { return utils.Internal(c, "failed to fetch tripsheets") } return utils.List(c, list, int64(len(list))) } func CreateTripsheet(c *fiber.Ctx) error { req := new(dto.TripsheetCreateRequest) if err := c.BodyParser(req); err != nil { return utils.BadRequest(c, "invalid request body") } adminUserID := c.Locals("userid").(int) tripsheet := models.Tripsheet{ Tripsheetno: generateTripsheetNo(), Sourcehubid: req.Sourcehubid, Destinationhubid: req.Destinationhubid, Vehicleid: req.Vehicleid, Driveruserid: req.Driveruserid, Status: constants.TripsheetDraft, Createdby: adminUserID, Updatedby: adminUserID, } if err := db.DB.Create(&tripsheet).Error; err != nil { return utils.Internal(c, "failed to create tripsheet") } return utils.Created(c, tripsheet) } func GetTripsheetDetails(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) var tripsheet models.Tripsheet if err := db.DB.Where("tripsheetid = ? AND deletedat IS NULL", id).First(&tripsheet).Error; err != nil { return utils.NotFound(c, "tripsheet not found") } var items []models.TripsheetItem db.DB.Where("tripsheetid = ?", id).Find(&items) return utils.OK(c, fiber.Map{"tripsheet": tripsheet, "items": items}) } func AddTripsheetItem(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) req := new(dto.TripsheetItemAddRequest) if err := c.BodyParser(req); err != nil { return utils.BadRequest(c, "invalid request body") } adminUserID := c.Locals("userid").(int) item := models.TripsheetItem{ Tripsheetid: id, Consignmentid: req.Consignmentid, Scanstatus: constants.ScanPending, Createdby: adminUserID, Updatedby: adminUserID, } if err := db.DB.Create(&item).Error; err != nil { return utils.Internal(c, "failed to add item to tripsheet") } return utils.Created(c, item) } func DeleteTripsheetItem(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) itemID, _ := strconv.Atoi(c.Params("itemid")) var item models.TripsheetItem if err := db.DB.Where("tripsheetid = ? AND tripsheetitemid = ?", id, itemID).First(&item).Error; err != nil { return utils.NotFound(c, "item not found on this tripsheet") } db.DB.Delete(&item) return utils.Message(c, "item removed from tripsheet") } func DispatchTripsheet(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) tx := db.DB.Begin() var tripsheet models.Tripsheet if err := tx.Where("tripsheetid = ?", id).First(&tripsheet).Error; err != nil { tx.Rollback() return utils.NotFound(c, "tripsheet not found") } now := time.Now() tripsheet.Status = constants.TripsheetDispatched tripsheet.Dispatchtime = &now tripsheet.Updatedat = now tx.Save(&tripsheet) // Fetch all loaded items var items []models.TripsheetItem tx.Where("tripsheetid = ?", id).Find(&items) adminUserID := c.Locals("userid").(int) for _, item := range items { // Update item scanning tx.Model(&item).Updates(map[string]interface{}{ "scanstatus": constants.ScanLoaded, "scannedat": &now, }) // Update consignment status to In_Transit tx.Model(&models.Consignment{}).Where("consignmentid = ?", item.Consignmentid).Updates(map[string]interface{}{ "status": constants.ConsignmentInTransit, "updatedat": now, }) // Log history history := models.ConsignmentHistory{ Consignmentid: item.Consignmentid, Tripsheetid: &id, Userid: &adminUserID, Eventstatus: constants.ConsignmentInTransit, Remarks: fmt.Sprintf("Consignment dispatched on Tripsheet %s", tripsheet.Tripsheetno), } tx.Create(&history) } tx.Commit() return utils.OK(c, tripsheet) } func ArriveTripsheet(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) tx := db.DB.Begin() var tripsheet models.Tripsheet if err := tx.Where("tripsheetid = ?", id).First(&tripsheet).Error; err != nil { tx.Rollback() return utils.NotFound(c, "tripsheet not found") } now := time.Now() tripsheet.Status = constants.TripsheetArrived tripsheet.Arrivaltime = &now tripsheet.Updatedat = now tx.Save(&tripsheet) // Fetch items var items []models.TripsheetItem tx.Where("tripsheetid = ?", id).Find(&items) adminUserID := c.Locals("userid").(int) for _, item := range items { tx.Model(&item).Updates(map[string]interface{}{ "scanstatus": constants.ScanUnloaded, "scannedat": &now, }) // Update consignment status back to Inwarded_at_Hub at destination tx.Model(&models.Consignment{}).Where("consignmentid = ?", item.Consignmentid).Updates(map[string]interface{}{ "status": constants.ConsignmentInwardedAtHub, "currenthubid": tripsheet.Destinationhubid, "updatedat": now, }) // Log history history := models.ConsignmentHistory{ Consignmentid: item.Consignmentid, Tripsheetid: &id, Hubid: &tripsheet.Destinationhubid, Userid: &adminUserID, Eventstatus: constants.ConsignmentInwardedAtHub, Remarks: fmt.Sprintf("Consignment arrived at Hub on Tripsheet %s", tripsheet.Tripsheetno), } tx.Create(&history) } tx.Commit() return utils.OK(c, tripsheet) } // -------------------- // PRICING CRUD // -------------------- func GetPricing(c *fiber.Ctx) error { var pricing []models.Pricing if err := db.DB.Where("deletedat IS NULL").Find(&pricing).Error; err != nil { return utils.Internal(c, "failed to fetch pricing schedules") } return utils.List(c, pricing, int64(len(pricing))) } func CreatePricing(c *fiber.Ctx) error { req := new(dto.PricingCreateRequest) if err := c.BodyParser(req); err != nil { return utils.BadRequest(c, "invalid request body") } adminUserID := c.Locals("userid").(int) pricing := models.Pricing{ Tenantid: req.Tenantid, Applocationid: req.Applocationid, Vehicletype: req.Vehicletype, Baseprice: req.Baseprice, Baseweight: req.Baseweight, Priceperkg: req.Priceperkg, Basedistance: req.Basedistance, Priceperkm: req.Priceperkm, Handlingcharges: req.Handlingcharges, Effectivefrom: req.Effectivefrom, Effectiveto: req.Effectiveto, Currency: req.Currency, Priority: req.Priority, Status: req.Status, Createdby: adminUserID, Updatedby: adminUserID, } if pricing.Currency == "" { pricing.Currency = "INR" } if pricing.Status == "" { pricing.Status = "Active" } if err := db.DB.Create(&pricing).Error; err != nil { return utils.Internal(c, "failed to create pricing schedule") } return utils.Created(c, pricing) } func UpdatePricing(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) var pricing models.Pricing if err := db.DB.Where("pricingid = ? AND deletedat IS NULL", id).First(&pricing).Error; err != nil { return utils.NotFound(c, "pricing schedule not found") } req := new(dto.PricingCreateRequest) if err := c.BodyParser(req); err != nil { return utils.BadRequest(c, "invalid request body") } if req.Baseprice != 0 { pricing.Baseprice = req.Baseprice } if req.Baseweight != 0 { pricing.Baseweight = req.Baseweight } if req.Priceperkg != 0 { pricing.Priceperkg = req.Priceperkg } if req.Basedistance != 0 { pricing.Basedistance = req.Basedistance } if req.Priceperkm != 0 { pricing.Priceperkm = req.Priceperkm } pricing.Handlingcharges = req.Handlingcharges if !req.Effectivefrom.IsZero() { pricing.Effectivefrom = req.Effectivefrom } if !req.Effectiveto.IsZero() { pricing.Effectiveto = req.Effectiveto } if req.Status != "" { pricing.Status = req.Status } pricing.Updatedat = time.Now() pricing.Updatedby = c.Locals("userid").(int) if err := db.DB.Save(&pricing).Error; err != nil { return utils.Internal(c, "failed to update pricing schedule") } return utils.OK(c, pricing) } func DeletePricing(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) var pricing models.Pricing if err := db.DB.Where("pricingid = ? AND deletedat IS NULL", id).First(&pricing).Error; err != nil { return utils.NotFound(c, "pricing schedule not found") } now := time.Now() pricing.Deletedat = &now db.DB.Save(&pricing) return utils.Message(c, "pricing schedule deleted successfully") } func GetPricingQuoteSimulate(c *fiber.Ctx) error { req := new(dto.PricingQuoteRequest) if err := c.BodyParser(req); err != nil { return utils.BadRequest(c, "invalid request body") } distance := calculateDistance(req.Pickuplatitude, req.Pickuplongitude, req.Deliverylatitude, req.Deliverylongitude) var totalWeight float64 for _, p := range req.Parcels { volumetric := calculateVolumetricWeight(p.Length, p.Width, p.Height) totalWeight += math.Max(p.Weight, volumetric) } var pricing models.Pricing err := db.DB.Where("status = ? AND ? BETWEEN effectivefrom AND effectiveto", "Active", time.Now()).Order("priority DESC").First(&pricing).Error var baseQuote float64 var pricingIDPtr *int if err == nil { pricingIDPtr = &pricing.Pricingid kmExtra := math.Max(0, distance-pricing.Basedistance) kgExtra := math.Max(0, totalWeight-pricing.Baseweight) baseQuote = pricing.Baseprice + (kmExtra * pricing.Priceperkm) + (kgExtra * pricing.Priceperkg) + pricing.Handlingcharges } else { // Fallback simple pricing engine baseQuote = 50.0 + (distance * 5.0) + (totalWeight * 10.0) } return utils.OK(c, fiber.Map{ "distance_km": distance, "chargeable_weight": totalWeight, "base_quote": baseQuote, "pricing_id": pricingIDPtr, }) } // -------------------- // EXCEPTIONS // -------------------- func GetExceptions(c *fiber.Ctx) error { var list []models.ConsignmentException if err := db.DB.Where("deletedat IS NULL").Find(&list).Error; err != nil { return utils.Internal(c, "failed to fetch exceptions") } return utils.List(c, list, int64(len(list))) } func CreateException(c *fiber.Ctx) error { req := new(dto.ExceptionCreateRequest) if err := c.BodyParser(req); err != nil { return utils.BadRequest(c, "invalid request body") } adminUserID := c.Locals("userid").(int) exception := models.ConsignmentException{ Consignmentid: req.Consignmentid, Tripsheetid: req.Tripsheetid, Hubid: req.Hubid, Reportedbyuserid: &adminUserID, Exceptiontype: req.Exceptiontype, Severity: req.Severity, Description: req.Description, Status: constants.ExceptionOpen, Createdby: adminUserID, Updatedby: adminUserID, } if exception.Severity == "" { exception.Severity = "Medium" } tx := db.DB.Begin() if err := tx.Create(&exception).Error; err != nil { tx.Rollback() return utils.Internal(c, "failed to record exception") } // Update consignment status to Missing or Damaged if critical exception type matches var cStatus string if req.Exceptiontype == constants.ExceptionLost { cStatus = constants.ConsignmentMissing } else if req.Exceptiontype == constants.ExceptionDamaged { cStatus = constants.ConsignmentDamaged } if cStatus != "" { tx.Model(&models.Consignment{}).Where("consignmentid = ?", req.Consignmentid).Update("status", cStatus) // Log history history := models.ConsignmentHistory{ Consignmentid: req.Consignmentid, Tripsheetid: req.Tripsheetid, Hubid: req.Hubid, Userid: &adminUserID, Eventstatus: cStatus, Remarks: fmt.Sprintf("Exception reported: %s. Description: %s", req.Exceptiontype, req.Description), } tx.Create(&history) } tx.Commit() return utils.Created(c, exception) } func GetExceptionDetails(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) var exception models.ConsignmentException if err := db.DB.Where("exceptionid = ? AND deletedat IS NULL", id).First(&exception).Error; err != nil { return utils.NotFound(c, "exception not found") } return utils.OK(c, exception) } func ResolveException(c *fiber.Ctx) error { id, _ := strconv.Atoi(c.Params("id")) req := new(dto.ExceptionResolveRequest) if err := c.BodyParser(req); err != nil { return utils.BadRequest(c, "invalid request body") } adminUserID := c.Locals("userid").(int) var exception models.ConsignmentException if err := db.DB.Where("exceptionid = ? AND deletedat IS NULL", id).First(&exception).Error; err != nil { return utils.NotFound(c, "exception not found") } exception.Resolution = req.Resolution exception.Status = req.Status exception.Updatedby = adminUserID exception.Updatedat = time.Now() if err := db.DB.Save(&exception).Error; err != nil { return utils.Internal(c, "failed to resolve exception") } return utils.OK(c, exception) } func CreateUserRedis(c *fiber.Ctx) error { ctx := context.Background() var userData models.CachedUser if err := c.BodyParser(&userData); err != nil { return utils.BadRequest(c, "invalid request body") } if userData.UserID == 0 || userData.Username == "" { return utils.BadRequest(c, "userid and username are required") } if db.Rdb == nil { return utils.Internal(c, "cache service unavailable") } jsonData, err := json.Marshal(userData) if err != nil { return utils.Internal(c, "failed to serialize user data") } userKey := fmt.Sprintf("user:%d", userData.UserID) if err := db.Rdb.Set(ctx, userKey, jsonData, 0).Err(); err != nil { return utils.Internal(c, "failed to store user in cache") } db.Rdb.ZAdd(ctx, "user", redis.Z{ Score: float64(time.Now().Unix()), Member: fmt.Sprintf("%d", userData.UserID), }) return utils.Created(c, userData) } func GetUserRedis(c *fiber.Ctx) error { ctx := context.Background() if db.Rdb == nil { return utils.Internal(c, "cache service unavailable") } userIDStr := c.Query("userid") if userIDStr != "" { userID, err := strconv.Atoi(userIDStr) if err != nil || userID == 0 { return utils.BadRequest(c, "invalid userid parameter") } userKey := fmt.Sprintf("user:%d", userID) userData, err := db.Rdb.Get(ctx, userKey).Result() if err != nil { return utils.NotFound(c, "user not found in cache") } var user map[string]interface{} json.Unmarshal([]byte(userData), &user) return utils.OK(c, user) } pageStr := c.Query("page") pageSizeStr := c.Query("pagesize") page, _ := strconv.Atoi(pageStr) pageSize, _ := strconv.Atoi(pageSizeStr) var start, end int64 if page > 0 && pageSize > 0 { offset := (page - 1) * pageSize start = int64(offset) end = int64(offset + pageSize - 1) } else { start = 0 end = -1 } userIDs, err := db.Rdb.ZRevRange(ctx, "user", start, end).Result() if err != nil { return utils.Internal(c, "failed to fetch user list") } if len(userIDs) == 0 { return utils.List(c, []interface{}{}, 0) } var keys []string for _, id := range userIDs { keys = append(keys, fmt.Sprintf("user:%s", id)) } values, err := db.Rdb.MGet(ctx, keys...).Result() if err != nil { return utils.Internal(c, "failed to retrieve user data") } var users []map[string]interface{} for _, val := range values { if val == nil { continue } var user map[string]interface{} json.Unmarshal([]byte(val.(string)), &user) users = append(users, user) } return utils.List(c, users, int64(len(users))) } func UpdateUserRedis(c *fiber.Ctx) error { ctx := context.Background() userID, err := strconv.Atoi(c.Params("userid")) if err != nil || userID == 0 { return utils.BadRequest(c, "invalid userid parameter") } var userData map[string]interface{} if err := c.BodyParser(&userData); err != nil { return utils.BadRequest(c, "invalid request body") } if db.Rdb == nil { return utils.Internal(c, "cache service unavailable") } userKey := fmt.Sprintf("user:%d", userID) exists, err := db.Rdb.Exists(ctx, userKey).Result() if err != nil || exists == 0 { return utils.NotFound(c, "user not found in cache") } userData["userid"] = userID jsonData, _ := json.Marshal(userData) if err := db.Rdb.Set(ctx, userKey, jsonData, 0).Err(); err != nil { return utils.Internal(c, "failed to update user in cache") } return utils.OK(c, userData) } func DeleteUserRedis(c *fiber.Ctx) error { ctx := context.Background() userID, err := strconv.Atoi(c.Params("userid")) if err != nil || userID == 0 { return utils.BadRequest(c, "invalid userid parameter") } if db.Rdb == nil { return utils.Internal(c, "cache service unavailable") } userKey := fmt.Sprintf("user:%d", userID) db.Rdb.Del(ctx, userKey) db.Rdb.ZRem(ctx, "user", fmt.Sprintf("%d", userID)) return utils.Message(c, "user removed from cache successfully") } func GetAllUsersRedis(c *fiber.Ctx) error { ctx := context.Background() if db.Rdb == nil { return utils.Internal(c, "cache service unavailable") } userIDs, err := db.Rdb.ZRange(ctx, "user", 0, -1).Result() if err != nil { return utils.Internal(c, "failed to fetch user index") } var users []map[string]interface{} for _, idStr := range userIDs { userKey := fmt.Sprintf("user:%s", idStr) userData, err := db.Rdb.Get(ctx, userKey).Result() if err != nil { continue } var user map[string]interface{} json.Unmarshal([]byte(userData), &user) users = append(users, user) } return utils.List(c, users, int64(len(users))) } func GetAdminProfile(c *fiber.Ctx) error { userID, ok := c.Locals("userid").(int) if !ok { return utils.Unauthorized(c, "authentication required") } var user models.AppUser if err := db.DB.Where("userid = ?", userID).First(&user).Error; err != nil { return utils.NotFound(c, "user not found") } return utils.OK(c, user) } // InternalNotify sends FCM push notifications on behalf of the Python agent system. // The caller specifies target = "customer", "miler", or "both". // Auth: X-Internal-Key header (see InternalKeyAuth middleware). // // POST /api/v1/internal/notify func InternalNotify(c *fiber.Ctx) error { type req struct { BookingID int `json:"booking_id"` Target string `json:"target"` Title string `json:"title"` Message string `json:"message"` Data map[string]string `json:"data"` } body := new(req) if err := c.BodyParser(body); err != nil { return utils.BadRequest(c, "invalid request body") } if body.BookingID == 0 { return utils.BadRequest(c, "booking_id is required") } if body.Target != "customer" && body.Target != "miler" && body.Target != "both" { return utils.BadRequest(c, "target must be customer, miler, or both") } if body.Title == "" || body.Message == "" { return utils.BadRequest(c, "title and message are required") } var booking models.PickupBooking if err := db.DB.First(&booking, body.BookingID).Error; err != nil { return utils.NotFound(c, "booking not found") } data := body.Data if data == nil { data = map[string]string{} } data["booking_id"] = strconv.Itoa(booking.Bookingid) sent := make([]string, 0, 2) if body.Target == "customer" || body.Target == "both" { 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, body.Title, body.Message, data); err != nil { utils.Warn("InternalNotify: failed to notify customer", "booking_id", booking.Bookingid, "error", err) } else { sent = append(sent, "customer") } } } if body.Target == "miler" || body.Target == "both" { if booking.Assignedmileruserid != nil { var profile models.MilerProfile if err := db.DB.Where("userid = ?", *booking.Assignedmileruserid).First(&profile).Error; err == nil && profile.Devicetoken != "" { if err := notify.SendToDevice(profile.Devicetoken, body.Title, body.Message, data); err != nil { utils.Warn("InternalNotify: failed to notify miler", "booking_id", booking.Bookingid, "error", err) } else { sent = append(sent, "miler") } } } } return utils.OK(c, fiber.Map{ "sent": true, "targets": sent, }) } // InternalReassign releases the current miler assignment and re-triggers the // auto-assignment engine for a stalled booking. Only valid when the booking is // in Miler_Assigned or Pickup_Scheduled state. // Auth: X-Internal-Key header (see InternalKeyAuth middleware). // // POST /api/v1/internal/bookings/:id/reassign func InternalReassign(c *fiber.Ctx) error { id, err := strconv.Atoi(c.Params("id")) if err != nil { return utils.BadRequest(c, "invalid booking ID") } tx := db.DB.Begin() var booking models.PickupBooking if err := tx.First(&booking, id).Error; err != nil { tx.Rollback() return utils.NotFound(c, "booking not found") } if booking.Status != constants.BookingMilerAssigned && booking.Status != constants.BookingPickupScheduled { tx.Rollback() return utils.BadRequest(c, "booking must be in Miler_Assigned or Pickup_Scheduled state to reassign") } now := time.Now() if booking.Assignedmileruserid != nil { tx.Model(&models.MilerProfile{}). Where("userid = ?", *booking.Assignedmileruserid). Updates(map[string]interface{}{ "availabilitystatus": constants.MilerAvailable, "updatedat": now, }) tx.Delete(&models.BookingAssignment{}, "bookingid = ? AND assignmentstatus IN ?", booking.Bookingid, []string{constants.AssignmentAssigned, constants.AssignmentAccepted}, ) } booking.Status = constants.BookingCreated booking.Assignedmileruserid = nil booking.Updatedat = now if err := tx.Save(&booking).Error; err != nil { tx.Rollback() return utils.Internal(c, "failed to reset booking") } tx.Commit() if booking.Bookingsource == "CRM_Console" { go assignment.AssignCRMMiler(booking.Bookingid) } else { go assignment.AssignCustomerMiler(booking.Bookingid) } utils.Info("InternalReassign: reassignment triggered", "booking_id", booking.Bookingid, "booking_source", booking.Bookingsource, ) return utils.OK(c, fiber.Map{ "reassignment": "triggered", "booking_id": booking.Bookingid, }) }