|
|
|
|
@@ -21,8 +21,72 @@ import (
|
|
|
|
|
|
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
|
|
|
"gorm.io/gorm"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// consoleTenantID returns the tenant a console login is restricted to, or 0
|
|
|
|
|
// for Doormile's own staff, who are unrestricted. Client logins carry their
|
|
|
|
|
// tenant in the JWT (see LoginAdmin); Doormile staff have DoormileAuth.Tenantid
|
|
|
|
|
// nil and so authenticate with 0.
|
|
|
|
|
func consoleTenantID(c *fiber.Ctx) int {
|
|
|
|
|
tenantID, ok := c.Locals("tenantid").(int)
|
|
|
|
|
if !ok {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
return tenantID
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// isDoormileConsoleStaff reports whether the caller sees every tenant's data.
|
|
|
|
|
func isDoormileConsoleStaff(c *fiber.Ctx) bool {
|
|
|
|
|
return consoleTenantID(c) == 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// scopeToOwnTenant restricts a query on a tenant-owned table to the requesting
|
|
|
|
|
// console user's own tenant. Doormile staff are unrestricted. This is the
|
|
|
|
|
// admin-console counterpart of scopeBookingsToOwnTenant in hubController.go —
|
|
|
|
|
// without it, any client given an express-console login reads every other
|
|
|
|
|
// client's data.
|
|
|
|
|
//
|
|
|
|
|
// The column name is taken as a parameter because the tenant key is not always
|
|
|
|
|
// literally "tenantid": on the tenants table itself it is the primary key.
|
|
|
|
|
func scopeToOwnTenant(c *fiber.Ctx, query *gorm.DB, column string) *gorm.DB {
|
|
|
|
|
tenantID := consoleTenantID(c)
|
|
|
|
|
if tenantID == 0 {
|
|
|
|
|
return query
|
|
|
|
|
}
|
|
|
|
|
return query.Where(column+" = ?", tenantID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// canAccessTenant reports whether the caller may act on the given tenant.
|
|
|
|
|
// Used where the tenant is addressed by a path parameter or request body rather
|
|
|
|
|
// than filtered in a query — scoping a WHERE clause does nothing when the
|
|
|
|
|
// caller names the tenant directly.
|
|
|
|
|
func canAccessTenant(c *fiber.Ctx, tenantID int) bool {
|
|
|
|
|
own := consoleTenantID(c)
|
|
|
|
|
return own == 0 || own == tenantID
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// assertBookingAccess checks that the caller may act on a booking addressed by
|
|
|
|
|
// id. Returns nil for Doormile staff. Mutating handlers take the booking id
|
|
|
|
|
// straight from the path, so a scoped SELECT elsewhere in the handler does not
|
|
|
|
|
// protect them — this has to run before the write.
|
|
|
|
|
func assertBookingAccess(c *fiber.Ctx, bookingID int) error {
|
|
|
|
|
own := consoleTenantID(c)
|
|
|
|
|
if own == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
var booking models.PickupBooking
|
|
|
|
|
if err := db.DB.Select("bookingid", "tenantid").First(&booking, bookingID).Error; err != nil {
|
|
|
|
|
return utils.NotFound(c, "booking not found")
|
|
|
|
|
}
|
|
|
|
|
// A booking with no tenant predates tenant attribution and can't be proven
|
|
|
|
|
// to belong to this client, so it stays invisible to them.
|
|
|
|
|
if booking.Tenantid == nil || *booking.Tenantid != own {
|
|
|
|
|
return utils.NotFound(c, "booking not found")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper to generate tripsheet number
|
|
|
|
|
func generateTripsheetNo() string {
|
|
|
|
|
b := make([]byte, 4)
|
|
|
|
|
@@ -68,8 +132,18 @@ func LoginAdmin(cfg *config.Config) fiber.Handler {
|
|
|
|
|
userName = appUser.Authname
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A client's console login carries their tenant so handlers can scope to
|
|
|
|
|
// it; Doormile's own staff have Tenantid nil and keep tenantID 0, which
|
|
|
|
|
// scopeToOwnTenant reads as "unrestricted". Emitting 0 unconditionally
|
|
|
|
|
// (as this did) is what left every console login able to read every
|
|
|
|
|
// tenant's data.
|
|
|
|
|
tenantID := 0
|
|
|
|
|
if auth.Tenantid != nil {
|
|
|
|
|
tenantID = *auth.Tenantid
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
token, err := utils.GenerateToken(int(appUser.Userid), auth.Email, roleId, tenantID, 1, cfg.JWTSecret)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return utils.Internal(c, "failed to generate token")
|
|
|
|
|
}
|
|
|
|
|
@@ -78,10 +152,11 @@ func LoginAdmin(cfg *config.Config) fiber.Handler {
|
|
|
|
|
"success": true,
|
|
|
|
|
"token": token,
|
|
|
|
|
"user": fiber.Map{
|
|
|
|
|
"id": appUser.Userid,
|
|
|
|
|
"name": userName,
|
|
|
|
|
"email": auth.Email,
|
|
|
|
|
"role": auth.Role,
|
|
|
|
|
"id": appUser.Userid,
|
|
|
|
|
"name": userName,
|
|
|
|
|
"email": auth.Email,
|
|
|
|
|
"role": auth.Role,
|
|
|
|
|
"tenantid": auth.Tenantid,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
@@ -95,12 +170,19 @@ func GetAdminDashboard(c *fiber.Ctx) error {
|
|
|
|
|
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)
|
|
|
|
|
scopeToOwnTenant(c, db.DB.Model(&models.Tenant{}), "tenantid").Count(&totalTenants)
|
|
|
|
|
scopeToOwnTenant(c, db.DB.Model(&models.PickupBooking{}), "tenantid").Count(&totalBookings)
|
|
|
|
|
scopeToOwnTenant(c, db.DB.Model(&models.Consignment{}), "tenantid").Count(&totalConsignments)
|
|
|
|
|
|
|
|
|
|
// Customers, milers and exceptions have no tenant column, so there is no
|
|
|
|
|
// way to attribute them to one client here. Rather than show a client
|
|
|
|
|
// Doormile-wide totals, these are reported as zero for client logins; the
|
|
|
|
|
// per-client versions need a join through bookings and are not built yet.
|
|
|
|
|
if isDoormileConsoleStaff(c) {
|
|
|
|
|
db.DB.Model(&models.AppCustomer{}).Count(&totalCustomers)
|
|
|
|
|
db.DB.Model(&models.AppUser{}).Where("roleid = 5").Count(&totalMilers)
|
|
|
|
|
db.DB.Model(&models.ConsignmentException{}).Where("status = ?", "Open").Count(&openExceptions)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return utils.OK(c, fiber.Map{
|
|
|
|
|
"tenants": totalTenants,
|
|
|
|
|
@@ -131,20 +213,26 @@ func GetAdminReports(c *fiber.Ctx) error {
|
|
|
|
|
tenantID := c.Query("tenantid")
|
|
|
|
|
hubID := c.Query("hubid")
|
|
|
|
|
|
|
|
|
|
// ownTenant is 0 for Doormile staff (whole-network view) and the client's
|
|
|
|
|
// tenant for a client login, which every figure below is restricted to.
|
|
|
|
|
ownTenant := consoleTenantID(c)
|
|
|
|
|
|
|
|
|
|
var totalBookings int64
|
|
|
|
|
db.DB.Model(&models.PickupBooking{}).Where("createdat BETWEEN ? AND ?", from, to).Count(&totalBookings)
|
|
|
|
|
scopeToOwnTenant(c, db.DB.Model(&models.PickupBooking{}), "tenantid").
|
|
|
|
|
Where("createdat BETWEEN ? AND ?", from, to).Count(&totalBookings)
|
|
|
|
|
|
|
|
|
|
var delivered int64
|
|
|
|
|
db.DB.Model(&models.PickupBooking{}).
|
|
|
|
|
scopeToOwnTenant(c, db.DB.Model(&models.PickupBooking{}), "tenantid").
|
|
|
|
|
Where("status = ? AND updatedat BETWEEN ? AND ?", constants.BookingConvertedConsignment, from, to).
|
|
|
|
|
Count(&delivered)
|
|
|
|
|
|
|
|
|
|
var cancelled int64
|
|
|
|
|
db.DB.Model(&models.PickupBooking{}).
|
|
|
|
|
scopeToOwnTenant(c, db.DB.Model(&models.PickupBooking{}), "tenantid").
|
|
|
|
|
Where("status = ? AND updatedat BETWEEN ? AND ?", constants.BookingCancelled, from, to).
|
|
|
|
|
Count(&cancelled)
|
|
|
|
|
|
|
|
|
|
consignmentQuery := db.DB.Model(&models.Consignment{}).Where("createdat BETWEEN ? AND ?", from, to)
|
|
|
|
|
consignmentQuery := scopeToOwnTenant(c, db.DB.Model(&models.Consignment{}), "tenantid").
|
|
|
|
|
Where("createdat BETWEEN ? AND ?", from, to)
|
|
|
|
|
if tenantID != "" {
|
|
|
|
|
consignmentQuery = consignmentQuery.Where("tenantid = ?", tenantID)
|
|
|
|
|
}
|
|
|
|
|
@@ -154,15 +242,26 @@ func GetAdminReports(c *fiber.Ctx) error {
|
|
|
|
|
var totalConsignments int64
|
|
|
|
|
consignmentQuery.Count(&totalConsignments)
|
|
|
|
|
|
|
|
|
|
// Payments and exceptions carry no tenant column, so they're restricted
|
|
|
|
|
// through the bookings/consignments they belong to. COD in particular is a
|
|
|
|
|
// figure a client genuinely needs, so it's joined rather than suppressed.
|
|
|
|
|
var codCollected float64
|
|
|
|
|
db.DB.Model(&models.BookingPayment{}).
|
|
|
|
|
Where("paymentstatus = ? AND createdat BETWEEN ? AND ?", constants.PaymentStatusPaid, from, to).
|
|
|
|
|
Select("COALESCE(SUM(amount), 0)").Scan(&codCollected)
|
|
|
|
|
codQuery := db.DB.Model(&models.BookingPayment{}).
|
|
|
|
|
Where("paymentstatus = ? AND createdat BETWEEN ? AND ?", constants.PaymentStatusPaid, from, to)
|
|
|
|
|
if ownTenant != 0 {
|
|
|
|
|
codQuery = codQuery.Where("bookingid IN (?)",
|
|
|
|
|
db.DB.Model(&models.PickupBooking{}).Select("bookingid").Where("tenantid = ?", ownTenant))
|
|
|
|
|
}
|
|
|
|
|
codQuery.Select("COALESCE(SUM(amount), 0)").Scan(&codCollected)
|
|
|
|
|
|
|
|
|
|
var openExceptions int64
|
|
|
|
|
db.DB.Model(&models.ConsignmentException{}).
|
|
|
|
|
Where("status != ? AND createdat BETWEEN ? AND ?", constants.ExceptionClosed, from, to).
|
|
|
|
|
Count(&openExceptions)
|
|
|
|
|
excQuery := db.DB.Model(&models.ConsignmentException{}).
|
|
|
|
|
Where("status != ? AND createdat BETWEEN ? AND ?", constants.ExceptionClosed, from, to)
|
|
|
|
|
if ownTenant != 0 {
|
|
|
|
|
excQuery = excQuery.Where("consignmentid IN (?)",
|
|
|
|
|
db.DB.Model(&models.Consignment{}).Select("consignmentid").Where("tenantid = ?", ownTenant))
|
|
|
|
|
}
|
|
|
|
|
excQuery.Count(&openExceptions)
|
|
|
|
|
|
|
|
|
|
completionRate := 0.0
|
|
|
|
|
if totalBookings > 0 {
|
|
|
|
|
@@ -176,14 +275,23 @@ func GetAdminReports(c *fiber.Ctx) error {
|
|
|
|
|
Delivered int64 `gorm:"column:delivered"`
|
|
|
|
|
}
|
|
|
|
|
var hubRows []hubRow
|
|
|
|
|
db.DB.Raw(`
|
|
|
|
|
// The tenant filter belongs in the JOIN condition, not a WHERE — in a WHERE
|
|
|
|
|
// it would drop hubs with no matching parcels instead of showing them as
|
|
|
|
|
// zero.
|
|
|
|
|
hubSQL := `
|
|
|
|
|
SELECT h.hubid AS hubid, h.hubname AS hubname, COUNT(c.consignmentid) AS delivered
|
|
|
|
|
FROM hubs h
|
|
|
|
|
LEFT JOIN consignments c ON c.currenthubid = h.hubid AND c.status = ? AND c.updatedat BETWEEN ? AND ?
|
|
|
|
|
LEFT JOIN consignments c ON c.currenthubid = h.hubid AND c.status = ? AND c.updatedat BETWEEN ? AND ?`
|
|
|
|
|
hubArgs := []interface{}{constants.ConsignmentDelivered, from, to}
|
|
|
|
|
if ownTenant != 0 {
|
|
|
|
|
hubSQL += ` AND c.tenantid = ?`
|
|
|
|
|
hubArgs = append(hubArgs, ownTenant)
|
|
|
|
|
}
|
|
|
|
|
hubSQL += `
|
|
|
|
|
WHERE h.deletedat IS NULL
|
|
|
|
|
GROUP BY h.hubid, h.hubname
|
|
|
|
|
ORDER BY delivered DESC
|
|
|
|
|
`, constants.ConsignmentDelivered, from, to).Scan(&hubRows)
|
|
|
|
|
ORDER BY delivered DESC`
|
|
|
|
|
db.DB.Raw(hubSQL, hubArgs...).Scan(&hubRows)
|
|
|
|
|
|
|
|
|
|
byHub := make([]fiber.Map, 0, len(hubRows))
|
|
|
|
|
for _, r := range hubRows {
|
|
|
|
|
@@ -197,13 +305,19 @@ func GetAdminReports(c *fiber.Ctx) error {
|
|
|
|
|
Bookings int64 `gorm:"column:bookings"`
|
|
|
|
|
}
|
|
|
|
|
var tenantRows []tenantRow
|
|
|
|
|
db.DB.Raw(`
|
|
|
|
|
tenantSQL := `
|
|
|
|
|
SELECT t.tenantid AS tenantid, t.tenantname AS tenantname, COUNT(c.consignmentid) AS bookings
|
|
|
|
|
FROM tenants t
|
|
|
|
|
LEFT JOIN consignments c ON c.tenantid = t.tenantid AND c.createdat BETWEEN ? AND ?
|
|
|
|
|
LEFT JOIN consignments c ON c.tenantid = t.tenantid AND c.createdat BETWEEN ? AND ?`
|
|
|
|
|
tenantArgs := []interface{}{from, to}
|
|
|
|
|
if ownTenant != 0 {
|
|
|
|
|
tenantSQL += ` WHERE t.tenantid = ?`
|
|
|
|
|
tenantArgs = append(tenantArgs, ownTenant)
|
|
|
|
|
}
|
|
|
|
|
tenantSQL += `
|
|
|
|
|
GROUP BY t.tenantid, t.tenantname
|
|
|
|
|
ORDER BY bookings DESC
|
|
|
|
|
`, from, to).Scan(&tenantRows)
|
|
|
|
|
ORDER BY bookings DESC`
|
|
|
|
|
db.DB.Raw(tenantSQL, tenantArgs...).Scan(&tenantRows)
|
|
|
|
|
|
|
|
|
|
byTenant := make([]fiber.Map, 0, len(tenantRows))
|
|
|
|
|
for _, r := range tenantRows {
|
|
|
|
|
@@ -219,6 +333,9 @@ func GetAdminReports(c *fiber.Ctx) error {
|
|
|
|
|
TotalKms float64 `gorm:"column:total_kms"`
|
|
|
|
|
TotalEarnings float64 `gorm:"column:total_earnings"`
|
|
|
|
|
}
|
|
|
|
|
// Rider earnings, kms and completed stops are Doormile's own workforce data
|
|
|
|
|
// — a client has no business reading them, and there's no per-client view
|
|
|
|
|
// of a rider who works across tenants. Clients get an empty list.
|
|
|
|
|
var riderRows []riderRow
|
|
|
|
|
riderQuery := `
|
|
|
|
|
SELECT mp.userid AS userid, mp.displayname AS displayname,
|
|
|
|
|
@@ -235,7 +352,9 @@ func GetAdminReports(c *fiber.Ctx) error {
|
|
|
|
|
args = append(args, hubID)
|
|
|
|
|
}
|
|
|
|
|
riderQuery += " GROUP BY mp.userid, mp.displayname ORDER BY completed_stops DESC LIMIT 50"
|
|
|
|
|
db.DB.Raw(riderQuery, args...).Scan(&riderRows)
|
|
|
|
|
if isDoormileConsoleStaff(c) {
|
|
|
|
|
db.DB.Raw(riderQuery, args...).Scan(&riderRows)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
byRider := make([]fiber.Map, 0, len(riderRows))
|
|
|
|
|
for _, r := range riderRows {
|
|
|
|
|
@@ -271,13 +390,16 @@ func GetAppUsers(c *fiber.Ctx) error {
|
|
|
|
|
page := utils.ParsePage(c)
|
|
|
|
|
|
|
|
|
|
var total int64
|
|
|
|
|
if err := db.DB.Model(&models.AppUser{}).Where("roleid != ?", 5).Count(&total).Error; err != nil {
|
|
|
|
|
if err := scopeToOwnTenant(c, db.DB.Model(&models.AppUser{}), "tenantid").
|
|
|
|
|
Where("roleid != ?", 5).Count(&total).Error; err != nil {
|
|
|
|
|
return utils.Internal(c, "failed to count users")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var users []models.AppUser
|
|
|
|
|
// Exclude Milers (Roleid = 5) from the CRM user list
|
|
|
|
|
if err := page.Apply(db.DB.Where("roleid != ?", 5)).Find(&users).Error; err != nil {
|
|
|
|
|
// Exclude Milers (Roleid = 5) from the CRM user list. Scoped as well, so a
|
|
|
|
|
// client sees only their own people, not Doormile's staff directory.
|
|
|
|
|
if err := page.Apply(scopeToOwnTenant(c, db.DB, "tenantid").Where("roleid != ?", 5)).
|
|
|
|
|
Find(&users).Error; err != nil {
|
|
|
|
|
return utils.Internal(c, "failed to fetch users")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -420,7 +542,9 @@ func DeleteAppUser(c *fiber.Ctx) error {
|
|
|
|
|
|
|
|
|
|
func GetTenants(c *fiber.Ctx) error {
|
|
|
|
|
var tenants []models.Tenant
|
|
|
|
|
if err := db.DB.Find(&tenants).Error; err != nil {
|
|
|
|
|
// A client login sees only its own tenant row; the tenant key here is the
|
|
|
|
|
// primary key, not a "tenantid" foreign column.
|
|
|
|
|
if err := scopeToOwnTenant(c, db.DB, "tenantid").Find(&tenants).Error; err != nil {
|
|
|
|
|
return utils.Internal(c, "failed to fetch tenants")
|
|
|
|
|
}
|
|
|
|
|
return utils.List(c, tenants, int64(len(tenants)))
|
|
|
|
|
@@ -441,6 +565,9 @@ func CreateTenant(c *fiber.Ctx) error {
|
|
|
|
|
if tenant.Status == "" {
|
|
|
|
|
tenant.Status = "Active"
|
|
|
|
|
}
|
|
|
|
|
if req.Requiredeliveryotp != nil {
|
|
|
|
|
tenant.Requiredeliveryotp = *req.Requiredeliveryotp
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := db.DB.Create(&tenant).Error; err != nil {
|
|
|
|
|
return utils.Internal(c, "failed to create tenant")
|
|
|
|
|
@@ -451,7 +578,7 @@ func CreateTenant(c *fiber.Ctx) error {
|
|
|
|
|
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 {
|
|
|
|
|
if err := scopeToOwnTenant(c, db.DB, "tenantid").First(&tenant, id).Error; err != nil {
|
|
|
|
|
return utils.NotFound(c, "tenant not found")
|
|
|
|
|
}
|
|
|
|
|
return utils.OK(c, tenant)
|
|
|
|
|
@@ -459,6 +586,9 @@ func GetTenantDetails(c *fiber.Ctx) error {
|
|
|
|
|
|
|
|
|
|
func UpdateTenant(c *fiber.Ctx) error {
|
|
|
|
|
id, _ := strconv.Atoi(c.Params("id"))
|
|
|
|
|
if !canAccessTenant(c, id) {
|
|
|
|
|
return utils.Forbidden(c, "not permitted for this tenant")
|
|
|
|
|
}
|
|
|
|
|
var tenant models.Tenant
|
|
|
|
|
if err := db.DB.First(&tenant, id).Error; err != nil {
|
|
|
|
|
return utils.NotFound(c, "tenant not found")
|
|
|
|
|
@@ -481,6 +611,9 @@ func UpdateTenant(c *fiber.Ctx) error {
|
|
|
|
|
if req.Status != "" {
|
|
|
|
|
tenant.Status = req.Status
|
|
|
|
|
}
|
|
|
|
|
if req.Requiredeliveryotp != nil {
|
|
|
|
|
tenant.Requiredeliveryotp = *req.Requiredeliveryotp
|
|
|
|
|
}
|
|
|
|
|
tenant.Updatedat = time.Now()
|
|
|
|
|
|
|
|
|
|
if err := db.DB.Save(&tenant).Error; err != nil {
|
|
|
|
|
@@ -491,6 +624,11 @@ func UpdateTenant(c *fiber.Ctx) error {
|
|
|
|
|
|
|
|
|
|
func DeleteTenant(c *fiber.Ctx) error {
|
|
|
|
|
id, _ := strconv.Atoi(c.Params("id"))
|
|
|
|
|
// Deleting your own tenant is not a client operation either — this is
|
|
|
|
|
// Doormile-staff only.
|
|
|
|
|
if !isDoormileConsoleStaff(c) {
|
|
|
|
|
return utils.Forbidden(c, "not permitted for this tenant")
|
|
|
|
|
}
|
|
|
|
|
var tenant models.Tenant
|
|
|
|
|
if err := db.DB.First(&tenant, id).Error; err != nil {
|
|
|
|
|
return utils.NotFound(c, "tenant not found")
|
|
|
|
|
@@ -504,6 +642,9 @@ func DeleteTenant(c *fiber.Ctx) error {
|
|
|
|
|
|
|
|
|
|
func GetTenantLocations(c *fiber.Ctx) error {
|
|
|
|
|
tenantID, _ := strconv.Atoi(c.Params("id"))
|
|
|
|
|
if !canAccessTenant(c, tenantID) {
|
|
|
|
|
return utils.Forbidden(c, "not permitted for this tenant")
|
|
|
|
|
}
|
|
|
|
|
var locations []models.TenantLocation
|
|
|
|
|
if err := db.DB.Where("tenantid = ?", tenantID).Find(&locations).Error; err != nil {
|
|
|
|
|
return utils.Internal(c, "failed to fetch locations")
|
|
|
|
|
@@ -513,21 +654,25 @@ func GetTenantLocations(c *fiber.Ctx) error {
|
|
|
|
|
|
|
|
|
|
func CreateTenantLocation(c *fiber.Ctx) error {
|
|
|
|
|
tenantID, _ := strconv.Atoi(c.Params("id"))
|
|
|
|
|
if !canAccessTenant(c, tenantID) {
|
|
|
|
|
return utils.Forbidden(c, "not permitted for this tenant")
|
|
|
|
|
}
|
|
|
|
|
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,
|
|
|
|
|
Tenantid: tenantID,
|
|
|
|
|
Locationname: req.Locationname,
|
|
|
|
|
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"
|
|
|
|
|
@@ -549,12 +694,20 @@ func UpdateTenantLocation(c *fiber.Ctx) error {
|
|
|
|
|
if err := db.DB.First(&location, id).Error; err != nil {
|
|
|
|
|
return utils.NotFound(c, "tenant location not found")
|
|
|
|
|
}
|
|
|
|
|
// The location is addressed by its own id, so the tenant guard has to run
|
|
|
|
|
// against the row we loaded rather than a path parameter.
|
|
|
|
|
if !canAccessTenant(c, location.Tenantid) {
|
|
|
|
|
return utils.Forbidden(c, "not permitted for this tenant")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req := new(dto.TenantLocationCreateRequest)
|
|
|
|
|
if err := c.BodyParser(req); err != nil {
|
|
|
|
|
return utils.BadRequest(c, "invalid request body")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if req.Locationname != "" {
|
|
|
|
|
location.Locationname = req.Locationname
|
|
|
|
|
}
|
|
|
|
|
if req.Address != "" {
|
|
|
|
|
location.Address = req.Address
|
|
|
|
|
}
|
|
|
|
|
@@ -1177,6 +1330,21 @@ func CreateMiler(c *fiber.Ctx) error {
|
|
|
|
|
appLocID = 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A client login may only create riders under its own tenant.
|
|
|
|
|
tenantID := req.Tenantid
|
|
|
|
|
if own := consoleTenantID(c); own != 0 {
|
|
|
|
|
tenantID = own
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Configid must match what LoginMiler looks up by — it queries
|
|
|
|
|
// "contactno = ? AND configid = ?" defaulting to 1001. Left unset, AppUser's
|
|
|
|
|
// column default of 1 applies and the rider can never log in, which is what
|
|
|
|
|
// happened to every miler created through this endpoint until now.
|
|
|
|
|
configID := req.Configid
|
|
|
|
|
if configID == 0 {
|
|
|
|
|
configID = 1001
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
user := models.AppUser{
|
|
|
|
|
Authname: req.Authname,
|
|
|
|
|
Email: req.Email,
|
|
|
|
|
@@ -1185,6 +1353,9 @@ func CreateMiler(c *fiber.Ctx) error {
|
|
|
|
|
Roleid: 5, // Miler
|
|
|
|
|
Status: "Active",
|
|
|
|
|
Applocationid: appLocID,
|
|
|
|
|
Tenantid: tenantID,
|
|
|
|
|
Hubid: req.Hubid,
|
|
|
|
|
Configid: configID,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := tx.Create(&user).Error; err != nil {
|
|
|
|
|
@@ -1200,6 +1371,7 @@ func CreateMiler(c *fiber.Ctx) error {
|
|
|
|
|
Availabilitystatus: constants.MilerOffline,
|
|
|
|
|
Rating: 5.00,
|
|
|
|
|
Applocationid: appLocID,
|
|
|
|
|
Hubid: req.Hubid,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := tx.Create(&profile).Error; err != nil {
|
|
|
|
|
@@ -1360,6 +1532,9 @@ func GetAdminBookings(c *fiber.Ctx) error {
|
|
|
|
|
if tenantID := c.Query("tenantid"); tenantID != "" {
|
|
|
|
|
query = query.Where("tenantid = ?", tenantID)
|
|
|
|
|
}
|
|
|
|
|
// Applied after the caller's own ?tenantid= filter so a client login can
|
|
|
|
|
// narrow within their tenant but never widen past it.
|
|
|
|
|
query = scopeToOwnTenant(c, query, "tenantid")
|
|
|
|
|
|
|
|
|
|
var total int64
|
|
|
|
|
if err := query.Count(&total).Error; err != nil {
|
|
|
|
|
@@ -1388,10 +1563,15 @@ func GetAdminBookings(c *fiber.Ctx) error {
|
|
|
|
|
// (one booking) and AdminBulkCreateBookings (many) — was previously a type
|
|
|
|
|
// local to CreateExpressBooking, promoted to package level so both can use it.
|
|
|
|
|
type AdminBookingRequest struct {
|
|
|
|
|
Tenantid int `json:"tenantid"`
|
|
|
|
|
Appcustomerid int `json:"appcustomerid"`
|
|
|
|
|
CustomerPhone string `json:"customer_phone"`
|
|
|
|
|
CustomerName string `json:"customer_name"`
|
|
|
|
|
Tenantid int `json:"tenantid"`
|
|
|
|
|
Appcustomerid int `json:"appcustomerid"`
|
|
|
|
|
CustomerPhone string `json:"customer_phone"`
|
|
|
|
|
CustomerName string `json:"customer_name"`
|
|
|
|
|
// Pickuplocationid names the client site the parcel is collected from — a
|
|
|
|
|
// DailyGrubs kitchen, for instance. Optional, but supplying it lets the
|
|
|
|
|
// address/pincode/coordinates be filled from the stored location instead of
|
|
|
|
|
// retyped, and is the only thing that makes per-site reporting possible.
|
|
|
|
|
Pickuplocationid *int `json:"pickuplocationid"`
|
|
|
|
|
Pickupaddress string `json:"pickupaddress"`
|
|
|
|
|
Pickuppincode string `json:"pickuppincode"`
|
|
|
|
|
Pickuplatitude float64 `json:"pickuplatitude"`
|
|
|
|
|
@@ -1426,9 +1606,6 @@ func (e *expressBookingValidationError) Error() string { return e.msg }
|
|
|
|
|
// *fiber.Ctx — the original function never touched c after BodyParser, so
|
|
|
|
|
// both callers can use this identically.
|
|
|
|
|
func createExpressBooking(req AdminBookingRequest) (*models.PickupBooking, error) {
|
|
|
|
|
if req.Pickupaddress == "" || req.Pickuppincode == "" {
|
|
|
|
|
return nil, &expressBookingValidationError{"pickup address and pincode are required"}
|
|
|
|
|
}
|
|
|
|
|
if len(req.Parcels) == 0 {
|
|
|
|
|
return nil, &expressBookingValidationError{"at least one parcel is required"}
|
|
|
|
|
}
|
|
|
|
|
@@ -1440,6 +1617,35 @@ func createExpressBooking(req AdminBookingRequest) (*models.PickupBooking, error
|
|
|
|
|
return nil, &expressBookingValidationError{"tenantid does not match a known tenant"}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A named pickup location fills in whatever the caller left blank, so the
|
|
|
|
|
// console can send a kitchen id instead of restating its address every time.
|
|
|
|
|
// It must belong to the booking's tenant — otherwise one client could book
|
|
|
|
|
// against another client's site.
|
|
|
|
|
if req.Pickuplocationid != nil {
|
|
|
|
|
var loc models.TenantLocation
|
|
|
|
|
if err := db.DB.Where("tenantlocationid = ?", *req.Pickuplocationid).First(&loc).Error; err != nil {
|
|
|
|
|
return nil, &expressBookingValidationError{"pickuplocationid does not match a known location"}
|
|
|
|
|
}
|
|
|
|
|
if loc.Tenantid != req.Tenantid {
|
|
|
|
|
return nil, &expressBookingValidationError{"pickuplocationid does not belong to this tenant"}
|
|
|
|
|
}
|
|
|
|
|
if req.Pickupaddress == "" {
|
|
|
|
|
req.Pickupaddress = loc.Address
|
|
|
|
|
}
|
|
|
|
|
if req.Pickuppincode == "" {
|
|
|
|
|
req.Pickuppincode = loc.Pincode
|
|
|
|
|
}
|
|
|
|
|
if req.Pickuplatitude == 0 && req.Pickuplongitude == 0 {
|
|
|
|
|
req.Pickuplatitude, req.Pickuplongitude = loc.Latitude, loc.Longitude
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Checked after the location fill-in, so a caller supplying only a kitchen
|
|
|
|
|
// id is not rejected for an address it never needed to send.
|
|
|
|
|
if req.Pickupaddress == "" || req.Pickuppincode == "" {
|
|
|
|
|
return nil, &expressBookingValidationError{"pickup address and pincode are required"}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tx := db.DB.Begin()
|
|
|
|
|
|
|
|
|
|
customerID := req.Appcustomerid
|
|
|
|
|
@@ -1472,6 +1678,7 @@ func createExpressBooking(req AdminBookingRequest) (*models.PickupBooking, error
|
|
|
|
|
Bookingno: generateBookingNo(),
|
|
|
|
|
Tenantid: &tenantID,
|
|
|
|
|
Appcustomerid: customerID,
|
|
|
|
|
Pickuplocationid: req.Pickuplocationid,
|
|
|
|
|
Pickupaddress: req.Pickupaddress,
|
|
|
|
|
Pickuppincode: req.Pickuppincode,
|
|
|
|
|
Pickuplatitude: req.Pickuplatitude,
|
|
|
|
|
@@ -1641,6 +1848,13 @@ func CreateExpressBooking(c *fiber.Ctx) error {
|
|
|
|
|
return utils.BadRequest(c, "invalid request body")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A client login may only book under its own tenant. Left unchecked, the
|
|
|
|
|
// tenantid is caller-supplied, so a client could attribute bookings — and
|
|
|
|
|
// their cost — to another client.
|
|
|
|
|
if own := consoleTenantID(c); own != 0 {
|
|
|
|
|
req.Tenantid = own
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
booking, err := createExpressBooking(*req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if _, ok := err.(*expressBookingValidationError); ok {
|
|
|
|
|
@@ -1679,7 +1893,14 @@ func AdminBulkCreateBookings(c *fiber.Ctx) error {
|
|
|
|
|
}
|
|
|
|
|
results := make([]result, 0, len(req.Bookings))
|
|
|
|
|
|
|
|
|
|
ownTenant := consoleTenantID(c)
|
|
|
|
|
|
|
|
|
|
for i, item := range req.Bookings {
|
|
|
|
|
// Same tenant pin as the single-booking path — a bulk import must not
|
|
|
|
|
// be a way around it.
|
|
|
|
|
if ownTenant != 0 {
|
|
|
|
|
item.Tenantid = ownTenant
|
|
|
|
|
}
|
|
|
|
|
booking, err := createExpressBooking(item)
|
|
|
|
|
if err != nil {
|
|
|
|
|
results = append(results, result{Index: i, Success: false, Error: err.Error()})
|
|
|
|
|
@@ -1694,7 +1915,8 @@ func AdminBulkCreateBookings(c *fiber.Ctx) error {
|
|
|
|
|
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 {
|
|
|
|
|
q := scopeToOwnTenant(c, db.DB.Preload("Parcels").Preload("ServiceOptions").Preload("Payments"), "tenantid")
|
|
|
|
|
if err := q.First(&booking, id).Error; err != nil {
|
|
|
|
|
return utils.NotFound(c, "booking not found")
|
|
|
|
|
}
|
|
|
|
|
return utils.OK(c, booking)
|
|
|
|
|
@@ -1702,6 +1924,9 @@ func GetAdminBookingDetails(c *fiber.Ctx) error {
|
|
|
|
|
|
|
|
|
|
func AdminAssignMiler(c *fiber.Ctx) error {
|
|
|
|
|
id, _ := strconv.Atoi(c.Params("id"))
|
|
|
|
|
if err := assertBookingAccess(c, id); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type MilerAssign struct {
|
|
|
|
|
Mileruserid int `json:"mileruserid"`
|
|
|
|
|
@@ -1723,6 +1948,9 @@ func AdminAssignMiler(c *fiber.Ctx) error {
|
|
|
|
|
|
|
|
|
|
func AdminAssignVehicle(c *fiber.Ctx) error {
|
|
|
|
|
id, _ := strconv.Atoi(c.Params("id"))
|
|
|
|
|
if err := assertBookingAccess(c, id); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type VehicleAssign struct {
|
|
|
|
|
Vehicleid int `json:"vehicleid"`
|
|
|
|
|
@@ -1749,6 +1977,9 @@ func AdminAssignVehicle(c *fiber.Ctx) error {
|
|
|
|
|
|
|
|
|
|
func AdminUpdateBookingStatus(c *fiber.Ctx) error {
|
|
|
|
|
id, _ := strconv.Atoi(c.Params("id"))
|
|
|
|
|
if err := assertBookingAccess(c, id); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type StatusUpdate struct {
|
|
|
|
|
Status string `json:"status"`
|
|
|
|
|
@@ -1783,6 +2014,9 @@ func AdminCancelBooking(c *fiber.Ctx) error {
|
|
|
|
|
if err != nil {
|
|
|
|
|
return utils.BadRequest(c, "invalid booking ID")
|
|
|
|
|
}
|
|
|
|
|
if err := assertBookingAccess(c, id); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var booking models.PickupBooking
|
|
|
|
|
if err := db.DB.First(&booking, id).Error; err != nil {
|
|
|
|
|
@@ -1850,6 +2084,8 @@ func AdminBulkCancelBookings(c *fiber.Ctx) error {
|
|
|
|
|
}
|
|
|
|
|
results := make([]result, 0, len(req.Bookingids))
|
|
|
|
|
|
|
|
|
|
ownTenant := consoleTenantID(c)
|
|
|
|
|
|
|
|
|
|
for _, id := range req.Bookingids {
|
|
|
|
|
var booking models.PickupBooking
|
|
|
|
|
if err := db.DB.First(&booking, id).Error; err != nil {
|
|
|
|
|
@@ -1857,6 +2093,13 @@ func AdminBulkCancelBookings(c *fiber.Ctx) error {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reported as not-found rather than forbidden, so a client can't probe
|
|
|
|
|
// which booking ids belong to other tenants.
|
|
|
|
|
if ownTenant != 0 && (booking.Tenantid == nil || *booking.Tenantid != ownTenant) {
|
|
|
|
|
results = append(results, result{Bookingid: id, Success: false, Error: "booking not found"})
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if booking.Status == constants.BookingConvertedConsignment || booking.Status == constants.BookingCancelled {
|
|
|
|
|
results = append(results, result{Bookingid: id, Success: false, Error: "cannot cancel a delivered or already cancelled booking"})
|
|
|
|
|
continue
|
|
|
|
|
@@ -1905,12 +2148,13 @@ func GetAdminConsignments(c *fiber.Ctx) error {
|
|
|
|
|
page := utils.ParsePage(c)
|
|
|
|
|
|
|
|
|
|
var total int64
|
|
|
|
|
if err := db.DB.Model(&models.Consignment{}).Count(&total).Error; err != nil {
|
|
|
|
|
if err := scopeToOwnTenant(c, db.DB.Model(&models.Consignment{}), "tenantid").
|
|
|
|
|
Count(&total).Error; err != nil {
|
|
|
|
|
return utils.Internal(c, "failed to count consignments")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var list []models.Consignment
|
|
|
|
|
if err := page.Apply(db.DB).Find(&list).Error; err != nil {
|
|
|
|
|
if err := page.Apply(scopeToOwnTenant(c, db.DB, "tenantid")).Find(&list).Error; err != nil {
|
|
|
|
|
return utils.Internal(c, "failed to fetch consignments")
|
|
|
|
|
}
|
|
|
|
|
return utils.Paginated(c, list, total, page)
|
|
|
|
|
@@ -1919,7 +2163,7 @@ func GetAdminConsignments(c *fiber.Ctx) error {
|
|
|
|
|
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 {
|
|
|
|
|
if err := scopeToOwnTenant(c, db.DB, "tenantid").First(&csg, id).Error; err != nil {
|
|
|
|
|
return utils.NotFound(c, "consignment not found")
|
|
|
|
|
}
|
|
|
|
|
return utils.OK(c, csg)
|
|
|
|
|
@@ -1928,7 +2172,8 @@ func GetAdminConsignmentDetails(c *fiber.Ctx) error {
|
|
|
|
|
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 {
|
|
|
|
|
if err := scopeToOwnTenant(c, db.DB, "tenantid").
|
|
|
|
|
Where("trackingno = ?", trackingNo).First(&consignment).Error; err != nil {
|
|
|
|
|
return utils.NotFound(c, "consignment not found")
|
|
|
|
|
}
|
|
|
|
|
var history []models.ConsignmentHistory
|
|
|
|
|
@@ -2218,7 +2463,10 @@ func ArriveTripsheet(c *fiber.Ctx) error {
|
|
|
|
|
|
|
|
|
|
func GetPricing(c *fiber.Ctx) error {
|
|
|
|
|
var pricing []models.Pricing
|
|
|
|
|
if err := db.DB.Where("deletedat IS NULL").Find(&pricing).Error; err != nil {
|
|
|
|
|
// Rates are commercially sensitive: one client must never see what another
|
|
|
|
|
// is charged.
|
|
|
|
|
if err := scopeToOwnTenant(c, db.DB, "tenantid").
|
|
|
|
|
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)))
|
|
|
|
|
|