- 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
2208 lines
60 KiB
Go
2208 lines
60 KiB
Go
package controllers
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
"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)))
|
|
}
|
|
|
|
// --------------------
|
|
// 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 {
|
|
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 {
|
|
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").
|
|
Offset(offset).Limit(pagesize).Find(&bookings).Error; err != nil {
|
|
return utils.Internal(c, "failed to fetch 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 {
|
|
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)
|
|
}
|
|
|
|
// 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
|
|
// --------------------
|
|
|
|
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,
|
|
})
|
|
}
|