feat: date-range endpoints for hub inbound, bookings, and batches

Adds GET /hub/inbound, GET /hub/bookings (both new), and extends the
existing GET /hub/batches with optional from/to (YYYY-MM-DD, inclusive)
query params, backing the Hub Console's date-range picker on the Pickup
Requests, Receive Parcels, and Dispatch & Transfer pages. Reuses the same
range-parsing helper (renamed from parseHubDashboardRange to
parseHubDateRange) added for GET /hub/dashboard, defaulting to today when
omitted. The existing live endpoints (/inbound/today, /bookings/unassigned)
are untouched.

GET /hub/bookings also surfaces each booking's assignment status, mapped
to a small vocabulary (pending/assigned/picked_up/delivered/cancelled) via
the new hubBookingDisplayStatus, plus milername when assigned.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 15:14:36 +05:30
parent 624891b7bd
commit 1c24e93a7a
2 changed files with 156 additions and 34 deletions

View File

@@ -100,11 +100,11 @@ func todayMidnight() time.Time {
return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
}
// parseHubDashboardRange parses optional from/to (YYYY-MM-DD) query params
// for GetHubDashboardStats, defaulting to "today so far" when both are
// omitted. Mirrors GetHubReport's range semantics: to is inclusive of the
// whole day.
func parseHubDashboardRange(c *fiber.Ctx) (time.Time, time.Time, error) {
// parseHubDateRange parses optional from/to (YYYY-MM-DD) query params shared
// by the hub console's date-range-scoped endpoints (dashboard, inbound,
// bookings), defaulting to "today so far" when both are omitted. Mirrors
// GetHubReport's range semantics: to is inclusive of the whole day.
func parseHubDateRange(c *fiber.Ctx) (time.Time, time.Time, error) {
fromStr := c.Query("from")
toStr := c.Query("to")
@@ -215,7 +215,7 @@ func GetHubDashboardStats(c *fiber.Ctx) error {
hubID := c.Locals("hubid").(int)
prefix := hubPincodePrefix(hubID)
from, to, err := parseHubDashboardRange(c)
from, to, err := parseHubDateRange(c)
if err != nil {
return utils.BadRequest(c, err.Error())
}
@@ -305,6 +305,115 @@ func GetHubUnassignedBookings(c *fiber.Ctx) error {
return utils.List(c, response, int64(len(response)))
}
// hubBookingDisplayStatus normalizes a booking's raw status into the small
// vocabulary the hub console's history views key off: "pending" (needs a
// miler), "cancelled", or a handled state ("assigned", "picked_up",
// "delivered").
func hubBookingDisplayStatus(status string, hasMiler bool) string {
if status == constants.BookingCancelled {
return "cancelled"
}
if !hasMiler {
return "pending"
}
switch status {
case constants.BookingPickedUp:
return "picked_up"
case constants.BookingConvertedConsignment:
return "delivered"
default:
return "assigned"
}
}
// GetHubBookingsRange returns all pickup requests created at this hub in
// [from, to] (both YYYY-MM-DD, inclusive), each with its current assignment
// status and miler. Historical superset of GetHubUnassignedBookings, which
// stays as the live "needs a miler now" view.
func GetHubBookingsRange(c *fiber.Ctx) error {
hubID := c.Locals("hubid").(int)
prefix := hubPincodePrefix(hubID)
from, to, err := parseHubDateRange(c)
if err != nil {
return utils.BadRequest(c, err.Error())
}
query := db.DB.Preload("Parcels").Where("createdat BETWEEN ? AND ?", from, to)
if prefix != "" {
query = query.Where("pickuppincode LIKE ?", prefix+"%")
}
var bookings []models.PickupBooking
if err := query.Order("createdat DESC").Find(&bookings).Error; err != nil {
return utils.Internal(c, "failed to fetch bookings")
}
response := make([]fiber.Map, 0, len(bookings))
for _, b := range bookings {
var customer models.AppCustomer
db.DB.Where("appcustomerid = ?", b.Appcustomerid).First(&customer)
hasMiler := b.Assignedmileruserid != nil
var milerName interface{}
if hasMiler {
var mp models.MilerProfile
if db.DB.Where("userid = ?", *b.Assignedmileruserid).First(&mp).Error == nil {
milerName = mp.Displayname
}
}
response = append(response, fiber.Map{
"bookingid": b.Bookingid,
"bookingno": b.Bookingno,
"customer_name": strings.TrimSpace(customer.Firstname + " " + customer.Lastname),
"pickup_address": b.Pickupaddress,
"pickup_pincode": b.Pickuppincode,
"delivery_address": b.Deliveryaddress,
"parcels": b.Parcels,
"status": hubBookingDisplayStatus(b.Status, hasMiler),
"milername": milerName,
"created_at": b.Createdat,
})
}
return utils.List(c, response, int64(len(response)))
}
// renderInboundConsignment builds the hub console's inbound-parcel row,
// shared by GetHubInboundToday and GetHubInboundRange.
func renderInboundConsignment(cs models.Consignment) fiber.Map {
originName := fmt.Sprintf("Direct pickup (%s)", cs.Pickuppincode)
if cs.Originhubid != nil {
var oh models.Hub
if db.DB.Where("hubid = ?", *cs.Originhubid).First(&oh).Error == nil {
originName = oh.Hubname
}
}
return fiber.Map{
"consignmentid": cs.Consignmentid,
"trackingno": cs.Trackingno,
// sendername is empty: consignments carry no FK back to any
// customer/booking record (Senderid/Receiverid are never
// populated anywhere in this codebase).
"sendername": "",
"originname": originName,
// destinationname: consignments store no free-text delivery
// address, only deliverypincode — used as the best available label.
"destinationname": cs.Deliverypincode,
"deliverypincode": cs.Deliverypincode,
"weight": fmt.Sprintf("%.1f kg", cs.Chargeableweight),
"chargeableweight": cs.Chargeableweight,
"condition": cs.Condition,
"shelf": cs.Shelf,
// temperature: no column exists anywhere yet for this.
"temperature": "N/A",
"status": cs.Status,
"updatedat": cs.Updatedat,
}
}
func GetHubInboundToday(c *fiber.Ctx) error {
hubID := c.Locals("hubid").(int)
midnight := todayMidnight()
@@ -317,33 +426,33 @@ func GetHubInboundToday(c *fiber.Ctx) error {
response := make([]fiber.Map, 0, len(consignments))
for _, cs := range consignments {
originName := fmt.Sprintf("Direct pickup (%s)", cs.Pickuppincode)
if cs.Originhubid != nil {
var oh models.Hub
if db.DB.Where("hubid = ?", *cs.Originhubid).First(&oh).Error == nil {
originName = oh.Hubname
}
}
response = append(response, renderInboundConsignment(cs))
}
response = append(response, fiber.Map{
"consignmentid": cs.Consignmentid,
"trackingno": cs.Trackingno,
// sendername is empty: consignments carry no FK back to any
// customer/booking record (Senderid/Receiverid are never
// populated anywhere in this codebase).
"sendername": "",
"originname": originName,
// destinationname: consignments store no free-text delivery
// address, only deliverypincode — used as the best available label.
"destinationname": cs.Deliverypincode,
"weight": fmt.Sprintf("%.1f kg", cs.Chargeableweight),
"condition": cs.Condition,
"shelf": cs.Shelf,
// temperature: no column exists anywhere yet for this.
"temperature": "N/A",
"status": cs.Status,
"updatedat": cs.Updatedat,
})
return utils.List(c, response, int64(len(response)))
}
// GetHubInboundRange returns parcels received/inbounded at this hub whose
// inbound timestamp falls in [from, to] (both YYYY-MM-DD, inclusive),
// defaulting to today when omitted. Historical counterpart to
// GetHubInboundToday, which stays as the live "since midnight" view.
func GetHubInboundRange(c *fiber.Ctx) error {
hubID := c.Locals("hubid").(int)
from, to, err := parseHubDateRange(c)
if err != nil {
return utils.BadRequest(c, err.Error())
}
var consignments []models.Consignment
if err := db.DB.Where("currenthubid = ? AND status = ? AND updatedat BETWEEN ? AND ?", hubID, constants.ConsignmentInwardedAtHub, from, to).
Order("updatedat DESC").Find(&consignments).Error; err != nil {
return utils.Internal(c, "failed to fetch inbound consignments")
}
response := make([]fiber.Map, 0, len(consignments))
for _, cs := range consignments {
response = append(response, renderInboundConsignment(cs))
}
return utils.List(c, response, int64(len(response)))
@@ -442,12 +551,23 @@ func CreateInboundScan(c *fiber.Ctx) error {
// HUB BATCHES (DISPATCH) — backed by the existing Tripsheet model
// --------------------
// GetHubBatches lists this hub's outgoing batches. With no from/to it
// returns every batch (unchanged default); passing from/to (YYYY-MM-DD,
// inclusive) scopes it to batches created in that range.
func GetHubBatches(c *fiber.Ctx) error {
hubID := c.Locals("hubid").(int)
query := db.DB.Where("sourcehubid = ? AND deletedat IS NULL", hubID)
if c.Query("from") != "" || c.Query("to") != "" {
from, to, err := parseHubDateRange(c)
if err != nil {
return utils.BadRequest(c, err.Error())
}
query = query.Where("createdat BETWEEN ? AND ?", from, to)
}
var tripsheets []models.Tripsheet
if err := db.DB.Where("sourcehubid = ? AND deletedat IS NULL", hubID).
Order("createdat DESC").Find(&tripsheets).Error; err != nil {
if err := query.Order("createdat DESC").Find(&tripsheets).Error; err != nil {
return utils.Internal(c, "failed to fetch batches")
}