A client login could list its own bookings but had no way to see what its riders were actually doing. jupiter's console gave them getridersummary and the rider/delivery logs; Doormile records all of it and exposed none of it. New console endpoints, all tenant-scoped: GET /admin/milers/summary roster with live state + range totals GET /admin/milers/:id/logs GPS trail from the Redis telemetry index GET /admin/milers/:id/activity one rider's assignments, duty and breaks GET /admin/consignments/:id/logs event history + telemetry + proof GET /admin/bookings/:id/track booking -> assignments -> parcel -> proof Also closes a rider IDOR: GetMilers scoped the roster to the caller's own fleet, but reading, editing, blocking, notifying or assigning a vehicle to a single rider by id did not, so a client login could walk the whole network's riders by incrementing the id. All five now go through assertMilerAccess. And the client dashboard no longer reports milers/customers/exceptions as zero — those have no tenant column, so they are counted through appusers, bookings and consignments respectively. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
586 lines
18 KiB
Go
586 lines
18 KiB
Go
package controllers
|
|
|
|
// Rider-facing console reporting: the roster summary, per-rider GPS trails,
|
|
// per-consignment delivery logs and the end-to-end booking track view.
|
|
//
|
|
// These exist because jupiter's console gave a client a live picture of their
|
|
// riders — getridersummary, riderlogs, delivery logs — and the express console
|
|
// had no equivalent. Doormile records all of it already; none of it was
|
|
// readable from /admin.
|
|
//
|
|
// Every handler here is tenant-scoped: a client login sees its own fleet and
|
|
// nothing else.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"time"
|
|
|
|
"doormile/constants"
|
|
"doormile/db"
|
|
"doormile/models"
|
|
"doormile/utils"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/redis/go-redis/v9"
|
|
)
|
|
|
|
// assertMilerAccess resolves a miler profile by its profile id and proves the
|
|
// caller is allowed to see that rider. A rider belongs to a client through the
|
|
// tenantid on their appusers row — the same link GetMilers filters on. Doormile
|
|
// staff (tenant 0) skip the check.
|
|
func assertMilerAccess(c *fiber.Ctx, milerProfileID int) (*models.MilerProfile, error) {
|
|
var profile models.MilerProfile
|
|
if err := db.DB.Where("milerprofileid = ?", milerProfileID).First(&profile).Error; err != nil {
|
|
return nil, utils.NotFound(c, "miler not found")
|
|
}
|
|
|
|
own := consoleTenantID(c)
|
|
if own == 0 {
|
|
return &profile, nil
|
|
}
|
|
|
|
var user models.AppUser
|
|
if err := db.DB.Select("userid", "tenantid").Where("userid = ?", profile.Userid).First(&user).Error; err != nil {
|
|
return nil, utils.NotFound(c, "miler not found")
|
|
}
|
|
// Deliberately "not found" rather than "forbidden": a client should not be
|
|
// able to probe which rider ids exist outside their own fleet.
|
|
if user.Tenantid != own {
|
|
return nil, utils.NotFound(c, "miler not found")
|
|
}
|
|
return &profile, nil
|
|
}
|
|
|
|
// visibleMilerUserIDs lists the appusers.userid of every rider the caller may
|
|
// see. Returns nil for Doormile staff, meaning "no restriction".
|
|
func visibleMilerUserIDs(c *fiber.Ctx) []int {
|
|
own := consoleTenantID(c)
|
|
if own == 0 {
|
|
return nil
|
|
}
|
|
var ids []int
|
|
db.DB.Model(&models.AppUser{}).Where("tenantid = ? AND roleid = ?", own, 5).Pluck("userid", &ids)
|
|
return ids
|
|
}
|
|
|
|
// milerSummaryRow is one line of the roster table — the shape the console's
|
|
// rider list renders directly.
|
|
type milerSummaryRow struct {
|
|
Milerprofileid int `json:"milerprofileid"`
|
|
Userid int `json:"userid"`
|
|
Displayname string `json:"displayname"`
|
|
Phone string `json:"phone"`
|
|
Availabilitystatus string `json:"availabilitystatus"`
|
|
Defaultvehicletype string `json:"defaultvehicletype"`
|
|
Hubid *int `json:"hubid"`
|
|
Hubname string `json:"hubname"`
|
|
Rating float64 `json:"rating"`
|
|
Onduty bool `json:"onduty"`
|
|
Dutystartedat *time.Time `json:"dutystartedat"`
|
|
Currentlatitude float64 `json:"currentlatitude"`
|
|
Currentlongitude float64 `json:"currentlongitude"`
|
|
Lastlocationupdatedat *time.Time `json:"lastlocationupdatedat"`
|
|
Lastpingat *time.Time `json:"lastpingat"`
|
|
|
|
Assigned int64 `json:"assigned"`
|
|
Accepted int64 `json:"accepted"`
|
|
Rejected int64 `json:"rejected"`
|
|
Completed int64 `json:"completed"`
|
|
Cancelled int64 `json:"cancelled"`
|
|
Delivered int64 `json:"delivered"`
|
|
|
|
Riderkms float64 `json:"riderkms"`
|
|
Ridercharges float64 `json:"ridercharges"`
|
|
}
|
|
|
|
// GetMilerSummary is the console's rider roster: one row per rider with their
|
|
// live state and their numbers for the date range. This is the express-console
|
|
// equivalent of jupiter's getridersummary.
|
|
//
|
|
// GET /admin/milers/summary?from=&to=&applocationid=&hubid=
|
|
func GetMilerSummary(c *fiber.Ctx) error {
|
|
from, to, err := parseHubDateRange(c)
|
|
if err != nil {
|
|
return utils.BadRequest(c, err.Error())
|
|
}
|
|
|
|
query := db.DB.Model(&models.MilerProfile{})
|
|
if appLoc := c.Query("applocationid"); appLoc != "" {
|
|
query = query.Where("applocationid = ?", appLoc)
|
|
}
|
|
if hubID := c.Query("hubid"); hubID != "" {
|
|
query = query.Where("hubid = ?", hubID)
|
|
}
|
|
if visible := visibleMilerUserIDs(c); visible != nil {
|
|
if len(visible) == 0 {
|
|
return utils.List(c, []milerSummaryRow{}, 0)
|
|
}
|
|
query = query.Where("userid IN ?", visible)
|
|
}
|
|
|
|
var profiles []models.MilerProfile
|
|
if err := query.Order("displayname").Find(&profiles).Error; err != nil {
|
|
return utils.Internal(c, "failed to fetch milers")
|
|
}
|
|
if len(profiles) == 0 {
|
|
return utils.List(c, []milerSummaryRow{}, 0)
|
|
}
|
|
|
|
userIDs := make([]int, 0, len(profiles))
|
|
for _, p := range profiles {
|
|
userIDs = append(userIDs, p.Userid)
|
|
}
|
|
|
|
// One grouped query per fact rather than per rider, so the roster costs a
|
|
// fixed handful of queries whatever the fleet size.
|
|
type assignAgg struct {
|
|
Mileruserid int `gorm:"column:mileruserid"`
|
|
Status string `gorm:"column:assignmentstatus"`
|
|
Cnt int64 `gorm:"column:cnt"`
|
|
Riderkms float64 `gorm:"column:riderkms"`
|
|
Ridercharges float64 `gorm:"column:ridercharges"`
|
|
}
|
|
var aggs []assignAgg
|
|
db.DB.Model(&models.BookingAssignment{}).
|
|
Select("mileruserid, assignmentstatus, COUNT(*) AS cnt, COALESCE(SUM(riderkms),0) AS riderkms, COALESCE(SUM(ridercharges),0) AS ridercharges").
|
|
Where("mileruserid IN ? AND assignedat BETWEEN ? AND ?", userIDs, from, to).
|
|
Group("mileruserid, assignmentstatus").
|
|
Scan(&aggs)
|
|
|
|
type deliveredAgg struct {
|
|
Userid int `gorm:"column:userid"`
|
|
Cnt int64 `gorm:"column:cnt"`
|
|
}
|
|
var delivered []deliveredAgg
|
|
db.DB.Model(&models.ConsignmentHistory{}).
|
|
Select("userid, COUNT(*) AS cnt").
|
|
Where("userid IN ? AND eventstatus = ? AND createdat BETWEEN ? AND ?",
|
|
userIDs, constants.ConsignmentDelivered, from, to).
|
|
Group("userid").
|
|
Scan(&delivered)
|
|
|
|
var dutyLogs []models.MilerDutyLog
|
|
db.DB.Where("userid IN ? AND onduty = ?", userIDs, true).Find(&dutyLogs)
|
|
|
|
hubNames := map[int]string{}
|
|
var hubs []models.Hub
|
|
db.DB.Select("hubid", "hubname").Find(&hubs)
|
|
for _, h := range hubs {
|
|
hubNames[h.Hubid] = h.Hubname
|
|
}
|
|
|
|
rows := make([]milerSummaryRow, 0, len(profiles))
|
|
for _, p := range profiles {
|
|
row := milerSummaryRow{
|
|
Milerprofileid: p.Milerprofileid,
|
|
Userid: p.Userid,
|
|
Displayname: p.Displayname,
|
|
Phone: p.Phone,
|
|
Availabilitystatus: p.Availabilitystatus,
|
|
Defaultvehicletype: p.Defaultvehicletype,
|
|
Hubid: p.Hubid,
|
|
Rating: p.Rating,
|
|
Currentlatitude: p.Currentlatitude,
|
|
Currentlongitude: p.Currentlongitude,
|
|
Lastlocationupdatedat: p.Lastlocationupdatedat,
|
|
Lastpingat: lastTelemetryPing(p.Userid),
|
|
}
|
|
if p.Hubid != nil {
|
|
row.Hubname = hubNames[*p.Hubid]
|
|
}
|
|
for _, d := range dutyLogs {
|
|
if d.Userid == p.Userid {
|
|
row.Onduty = true
|
|
start := d.Loginat
|
|
row.Dutystartedat = &start
|
|
break
|
|
}
|
|
}
|
|
for _, a := range aggs {
|
|
if a.Mileruserid != p.Userid {
|
|
continue
|
|
}
|
|
row.Riderkms += a.Riderkms
|
|
row.Ridercharges += a.Ridercharges
|
|
switch a.Status {
|
|
case constants.AssignmentAssigned:
|
|
row.Assigned += a.Cnt
|
|
case constants.AssignmentAccepted:
|
|
row.Accepted += a.Cnt
|
|
case constants.AssignmentRejected:
|
|
row.Rejected += a.Cnt
|
|
case constants.AssignmentCompleted:
|
|
row.Completed += a.Cnt
|
|
case constants.AssignmentCancelled:
|
|
row.Cancelled += a.Cnt
|
|
}
|
|
}
|
|
for _, d := range delivered {
|
|
if d.Userid == p.Userid {
|
|
row.Delivered = d.Cnt
|
|
break
|
|
}
|
|
}
|
|
rows = append(rows, row)
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"success": true,
|
|
"data": rows,
|
|
"total": len(rows),
|
|
"from": from.Format("2006-01-02"),
|
|
"to": to.Format("2006-01-02"),
|
|
})
|
|
}
|
|
|
|
// lastTelemetryPing reads the timestamp of a rider's most recent periodic log
|
|
// straight off the Redis index. Nil when the rider has never pinged or Redis is
|
|
// unavailable — telemetry is best-effort and must never fail the roster.
|
|
func lastTelemetryPing(userID int) *time.Time {
|
|
if db.Rdb == nil {
|
|
return nil
|
|
}
|
|
res, err := db.Rdb.ZRevRangeWithScores(db.Ctx, fmt.Sprintf("miler_periodic_logs:%d", userID), 0, 0).Result()
|
|
if err != nil || len(res) == 0 {
|
|
return nil
|
|
}
|
|
t := time.Unix(int64(res[0].Score), 0).UTC()
|
|
return &t
|
|
}
|
|
|
|
// GetMilerLogs returns a rider's GPS trail for a date range — jupiter's
|
|
// riderlogs, but read from the Redis telemetry index rather than a 1.17M-row
|
|
// unindexed table.
|
|
//
|
|
// GET /admin/milers/:id/logs?from=&to=&limit=
|
|
func GetMilerLogs(c *fiber.Ctx) error {
|
|
id, err := strconv.Atoi(c.Params("id"))
|
|
if err != nil {
|
|
return utils.BadRequest(c, "invalid miler ID")
|
|
}
|
|
profile, aerr := assertMilerAccess(c, id)
|
|
if aerr != nil {
|
|
return aerr
|
|
}
|
|
if db.Rdb == nil {
|
|
return utils.Internal(c, "cache service unavailable")
|
|
}
|
|
|
|
from, to, err := parseHubDateRange(c)
|
|
if err != nil {
|
|
return utils.BadRequest(c, err.Error())
|
|
}
|
|
|
|
limit := 500
|
|
if l := c.QueryInt("limit"); l > 0 {
|
|
limit = l
|
|
}
|
|
if limit > 5000 {
|
|
limit = 5000
|
|
}
|
|
|
|
// The zset is scored by the log's own unix timestamp, so the range query is
|
|
// the date filter — no scanning every key for the rider.
|
|
keys, err := db.Rdb.ZRangeByScore(db.Ctx, fmt.Sprintf("miler_periodic_logs:%d", profile.Userid), &redis.ZRangeBy{
|
|
Min: strconv.FormatInt(from.Unix(), 10),
|
|
Max: strconv.FormatInt(to.Unix(), 10),
|
|
Count: int64(limit),
|
|
}).Result()
|
|
if err != nil {
|
|
return utils.Internal(c, "failed to fetch rider logs")
|
|
}
|
|
if len(keys) == 0 {
|
|
return c.JSON(fiber.Map{
|
|
"success": true, "data": []models.MilerLog{}, "total": 0,
|
|
"distancekm": 0.0, "miler": milerLogHeader(profile),
|
|
})
|
|
}
|
|
|
|
vals, err := db.Rdb.MGet(db.Ctx, keys...).Result()
|
|
if err != nil {
|
|
return utils.Internal(c, "failed to read rider logs")
|
|
}
|
|
|
|
logs := make([]models.MilerLog, 0, len(vals))
|
|
for _, v := range vals {
|
|
s, ok := v.(string)
|
|
if !ok {
|
|
continue
|
|
}
|
|
var l models.MilerLog
|
|
if json.Unmarshal([]byte(s), &l) == nil {
|
|
logs = append(logs, l)
|
|
}
|
|
}
|
|
|
|
// Trail distance, so the console can show kms actually ridden over the
|
|
// window rather than only the per-booking figure.
|
|
var distance float64
|
|
for i := 1; i < len(logs); i++ {
|
|
lat1, ok1 := parseCoord(logs[i-1].Latitude)
|
|
lon1, ok2 := parseCoord(logs[i-1].Longitude)
|
|
lat2, ok3 := parseCoord(logs[i].Latitude)
|
|
lon2, ok4 := parseCoord(logs[i].Longitude)
|
|
if ok1 && ok2 && ok3 && ok4 {
|
|
distance += haversineKM(lat1, lon1, lat2, lon2)
|
|
}
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"success": true,
|
|
"data": logs,
|
|
"total": len(logs),
|
|
"distancekm": distance,
|
|
"miler": milerLogHeader(profile),
|
|
"from": from.Format("2006-01-02"),
|
|
"to": to.Format("2006-01-02"),
|
|
})
|
|
}
|
|
|
|
func milerLogHeader(p *models.MilerProfile) fiber.Map {
|
|
return fiber.Map{
|
|
"milerprofileid": p.Milerprofileid,
|
|
"userid": p.Userid,
|
|
"displayname": p.Displayname,
|
|
"phone": p.Phone,
|
|
}
|
|
}
|
|
|
|
// parseCoord reads a telemetry coordinate. The Redis log models store lat/long
|
|
// as strings because that is what the rider app sends.
|
|
func parseCoord(s string) (float64, bool) {
|
|
if s == "" {
|
|
return 0, false
|
|
}
|
|
f, err := strconv.ParseFloat(s, 64)
|
|
if err != nil || f == 0 {
|
|
return 0, false
|
|
}
|
|
return f, true
|
|
}
|
|
|
|
// GetMilerActivity is one rider's detail page: their roster row, plus the duty
|
|
// and break sessions and the individual assignments behind the counts.
|
|
//
|
|
// GET /admin/milers/:id/activity?from=&to=
|
|
func GetMilerActivity(c *fiber.Ctx) error {
|
|
id, err := strconv.Atoi(c.Params("id"))
|
|
if err != nil {
|
|
return utils.BadRequest(c, "invalid miler ID")
|
|
}
|
|
profile, aerr := assertMilerAccess(c, id)
|
|
if aerr != nil {
|
|
return aerr
|
|
}
|
|
from, to, err := parseHubDateRange(c)
|
|
if err != nil {
|
|
return utils.BadRequest(c, err.Error())
|
|
}
|
|
|
|
var assignments []models.BookingAssignment
|
|
db.DB.Where("mileruserid = ? AND assignedat BETWEEN ? AND ?", profile.Userid, from, to).
|
|
Order("assignedat DESC").Find(&assignments)
|
|
|
|
// The booking behind each assignment, so the console can show the pickup and
|
|
// drop without a request per row.
|
|
bookingIDs := make([]int, 0, len(assignments))
|
|
for _, a := range assignments {
|
|
bookingIDs = append(bookingIDs, a.Bookingid)
|
|
}
|
|
var bookings []models.PickupBooking
|
|
if len(bookingIDs) > 0 {
|
|
db.DB.Where("bookingid IN ?", bookingIDs).Find(&bookings)
|
|
}
|
|
|
|
var dutyLogs []models.MilerDutyLog
|
|
db.DB.Where("userid = ? AND loginat BETWEEN ? AND ?", profile.Userid, from, to).
|
|
Order("loginat DESC").Find(&dutyLogs)
|
|
|
|
var breakLogs []models.MilerBreakLog
|
|
db.DB.Where("userid = ? AND startat BETWEEN ? AND ?", profile.Userid, from, to).
|
|
Order("startat DESC").Find(&breakLogs)
|
|
|
|
var delivered int64
|
|
db.DB.Model(&models.ConsignmentHistory{}).
|
|
Where("userid = ? AND eventstatus = ? AND createdat BETWEEN ? AND ?",
|
|
profile.Userid, constants.ConsignmentDelivered, from, to).Count(&delivered)
|
|
|
|
var totalKms, totalCharges float64
|
|
var dutyMinutes float64
|
|
for _, a := range assignments {
|
|
totalKms += a.Riderkms
|
|
totalCharges += a.Ridercharges
|
|
}
|
|
for _, d := range dutyLogs {
|
|
end := utils.DBNow()
|
|
if d.Logoutat != nil {
|
|
end = *d.Logoutat
|
|
}
|
|
dutyMinutes += end.Sub(d.Loginat).Minutes()
|
|
}
|
|
|
|
return utils.OK(c, fiber.Map{
|
|
"miler": profile,
|
|
"from": from.Format("2006-01-02"),
|
|
"to": to.Format("2006-01-02"),
|
|
"assignments": assignments,
|
|
"bookings": bookings,
|
|
"dutylogs": dutyLogs,
|
|
"breaklogs": breakLogs,
|
|
"lastpingat": lastTelemetryPing(profile.Userid),
|
|
"totals": fiber.Map{
|
|
"assignments": len(assignments),
|
|
"delivered": delivered,
|
|
"riderkms": totalKms,
|
|
"ridercharges": totalCharges,
|
|
"dutyminutes": dutyMinutes,
|
|
},
|
|
})
|
|
}
|
|
|
|
// GetAdminConsignmentLogs returns everything recorded against one parcel: the
|
|
// durable event history from Postgres and the rider's telemetry trail from
|
|
// Redis. jupiter kept these in one flat table; here they are two stores, so the
|
|
// console gets both in one call rather than reconciling them itself.
|
|
//
|
|
// GET /admin/consignments/:id/logs
|
|
func GetAdminConsignmentLogs(c *fiber.Ctx) error {
|
|
id, err := strconv.Atoi(c.Params("id"))
|
|
if err != nil {
|
|
return utils.BadRequest(c, "invalid consignment ID")
|
|
}
|
|
|
|
var consignment models.Consignment
|
|
q := scopeToOwnTenant(c, db.DB.Model(&models.Consignment{}), "tenantid")
|
|
if err := q.Where("consignmentid = ?", id).First(&consignment).Error; err != nil {
|
|
return utils.NotFound(c, "consignment not found")
|
|
}
|
|
|
|
var history []models.ConsignmentHistory
|
|
db.DB.Where("consignmentid = ?", id).Order("createdat").Find(&history)
|
|
|
|
var proof models.DeliveryProof
|
|
hasProof := db.DB.Where("consignmentid = ?", id).First(&proof).Error == nil
|
|
|
|
telemetry := readConsignmentTelemetry(id)
|
|
|
|
resp := fiber.Map{
|
|
"consignment": consignment,
|
|
"history": history,
|
|
"telemetry": telemetry,
|
|
}
|
|
if hasProof {
|
|
resp["deliveryproof"] = proof
|
|
}
|
|
return utils.OK(c, resp)
|
|
}
|
|
|
|
// readConsignmentTelemetry pulls the rider's per-parcel log list out of Redis.
|
|
// Always returns a usable slice — telemetry missing is not an error worth
|
|
// failing a tracking screen over.
|
|
func readConsignmentTelemetry(consignmentID int) []models.ConsignmentLog {
|
|
logs := []models.ConsignmentLog{}
|
|
if db.Rdb == nil {
|
|
return logs
|
|
}
|
|
raw, err := db.Rdb.LRange(db.Ctx, "Consignmentlogs:"+strconv.Itoa(consignmentID), 0, -1).Result()
|
|
if err != nil {
|
|
return logs
|
|
}
|
|
for _, item := range raw {
|
|
var l models.ConsignmentLog
|
|
if json.Unmarshal([]byte(item), &l) == nil {
|
|
logs = append(logs, l)
|
|
}
|
|
}
|
|
return logs
|
|
}
|
|
|
|
// GetAdminBookingTrack is the one call the console's tracking screen needs: the
|
|
// booking, every assignment attempt against it with the rider behind each, the
|
|
// consignment it became, that parcel's event history and proof of delivery, and
|
|
// the rider's live position.
|
|
//
|
|
// GET /admin/bookings/:id/track
|
|
func GetAdminBookingTrack(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
|
|
q := scopeToOwnTenant(c, db.DB.Preload("Parcels").Preload("ServiceOptions").Preload("Payments"), "tenantid")
|
|
if err := q.Where("bookingid = ?", id).First(&booking).Error; err != nil {
|
|
return utils.NotFound(c, "booking not found")
|
|
}
|
|
|
|
var assignments []models.BookingAssignment
|
|
db.DB.Where("bookingid = ?", id).Order("assignedat").Find(&assignments)
|
|
|
|
// Every rider who has touched this booking, not just the current one — a
|
|
// rejected first attempt is exactly what ops needs to see when asking why a
|
|
// pickup was slow.
|
|
riderIDs := make([]int, 0, len(assignments))
|
|
for _, a := range assignments {
|
|
riderIDs = append(riderIDs, a.Mileruserid)
|
|
}
|
|
riders := []models.MilerProfile{}
|
|
if len(riderIDs) > 0 {
|
|
db.DB.Where("userid IN ?", riderIDs).Find(&riders)
|
|
}
|
|
|
|
resp := fiber.Map{
|
|
"booking": booking,
|
|
"assignments": assignments,
|
|
"riders": riders,
|
|
}
|
|
|
|
// The live position of whoever currently holds it.
|
|
if booking.Assignedmileruserid != nil {
|
|
resp["livelocation"] = readMilerLiveLocation(*booking.Assignedmileruserid)
|
|
resp["lastpingat"] = lastTelemetryPing(*booking.Assignedmileruserid)
|
|
}
|
|
|
|
// The consignment, once the booking has been picked up and converted.
|
|
// BookingPickupComplete writes the link back onto the booking.
|
|
var consignment models.Consignment
|
|
if booking.Consignmentid != nil &&
|
|
db.DB.Where("consignmentid = ?", *booking.Consignmentid).First(&consignment).Error == nil {
|
|
var history []models.ConsignmentHistory
|
|
db.DB.Where("consignmentid = ?", consignment.Consignmentid).Order("createdat").Find(&history)
|
|
|
|
resp["consignment"] = consignment
|
|
resp["history"] = history
|
|
resp["telemetry"] = readConsignmentTelemetry(consignment.Consignmentid)
|
|
|
|
var proof models.DeliveryProof
|
|
if db.DB.Where("consignmentid = ?", consignment.Consignmentid).First(&proof).Error == nil {
|
|
resp["deliveryproof"] = proof
|
|
}
|
|
}
|
|
|
|
return utils.OK(c, resp)
|
|
}
|
|
|
|
// readMilerLiveLocation reads the rider's current position out of the Redis key
|
|
// UpdateMilerLocation writes on every ping. That key holds a bare "lat,lon"
|
|
// string with a 30-minute TTL, so a nil here means the rider has not pinged in
|
|
// the last half hour — normal for someone off duty, and worth showing as
|
|
// "no live position" rather than a stale one.
|
|
func readMilerLiveLocation(milerUserID int) interface{} {
|
|
if db.Rdb == nil {
|
|
return nil
|
|
}
|
|
val, err := db.Rdb.Get(db.Ctx, fmt.Sprintf("miler:gps:%d", milerUserID)).Result()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var lat, lon float64
|
|
if _, err := fmt.Sscanf(val, "%f,%f", &lat, &lon); err != nil {
|
|
return nil
|
|
}
|
|
return fiber.Map{"latitude": lat, "longitude": lon}
|
|
}
|