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:
@@ -100,11 +100,11 @@ func todayMidnight() time.Time {
|
|||||||
return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
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
|
// parseHubDateRange parses optional from/to (YYYY-MM-DD) query params shared
|
||||||
// for GetHubDashboardStats, defaulting to "today so far" when both are
|
// by the hub console's date-range-scoped endpoints (dashboard, inbound,
|
||||||
// omitted. Mirrors GetHubReport's range semantics: to is inclusive of the
|
// bookings), defaulting to "today so far" when both are omitted. Mirrors
|
||||||
// whole day.
|
// GetHubReport's range semantics: to is inclusive of the whole day.
|
||||||
func parseHubDashboardRange(c *fiber.Ctx) (time.Time, time.Time, error) {
|
func parseHubDateRange(c *fiber.Ctx) (time.Time, time.Time, error) {
|
||||||
fromStr := c.Query("from")
|
fromStr := c.Query("from")
|
||||||
toStr := c.Query("to")
|
toStr := c.Query("to")
|
||||||
|
|
||||||
@@ -215,7 +215,7 @@ func GetHubDashboardStats(c *fiber.Ctx) error {
|
|||||||
hubID := c.Locals("hubid").(int)
|
hubID := c.Locals("hubid").(int)
|
||||||
prefix := hubPincodePrefix(hubID)
|
prefix := hubPincodePrefix(hubID)
|
||||||
|
|
||||||
from, to, err := parseHubDashboardRange(c)
|
from, to, err := parseHubDateRange(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return utils.BadRequest(c, err.Error())
|
return utils.BadRequest(c, err.Error())
|
||||||
}
|
}
|
||||||
@@ -305,6 +305,115 @@ func GetHubUnassignedBookings(c *fiber.Ctx) error {
|
|||||||
return utils.List(c, response, int64(len(response)))
|
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 {
|
func GetHubInboundToday(c *fiber.Ctx) error {
|
||||||
hubID := c.Locals("hubid").(int)
|
hubID := c.Locals("hubid").(int)
|
||||||
midnight := todayMidnight()
|
midnight := todayMidnight()
|
||||||
@@ -317,33 +426,33 @@ func GetHubInboundToday(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
response := make([]fiber.Map, 0, len(consignments))
|
response := make([]fiber.Map, 0, len(consignments))
|
||||||
for _, cs := range consignments {
|
for _, cs := range consignments {
|
||||||
originName := fmt.Sprintf("Direct pickup (%s)", cs.Pickuppincode)
|
response = append(response, renderInboundConsignment(cs))
|
||||||
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, fiber.Map{
|
return utils.List(c, response, int64(len(response)))
|
||||||
"consignmentid": cs.Consignmentid,
|
}
|
||||||
"trackingno": cs.Trackingno,
|
|
||||||
// sendername is empty: consignments carry no FK back to any
|
// GetHubInboundRange returns parcels received/inbounded at this hub whose
|
||||||
// customer/booking record (Senderid/Receiverid are never
|
// inbound timestamp falls in [from, to] (both YYYY-MM-DD, inclusive),
|
||||||
// populated anywhere in this codebase).
|
// defaulting to today when omitted. Historical counterpart to
|
||||||
"sendername": "",
|
// GetHubInboundToday, which stays as the live "since midnight" view.
|
||||||
"originname": originName,
|
func GetHubInboundRange(c *fiber.Ctx) error {
|
||||||
// destinationname: consignments store no free-text delivery
|
hubID := c.Locals("hubid").(int)
|
||||||
// address, only deliverypincode — used as the best available label.
|
|
||||||
"destinationname": cs.Deliverypincode,
|
from, to, err := parseHubDateRange(c)
|
||||||
"weight": fmt.Sprintf("%.1f kg", cs.Chargeableweight),
|
if err != nil {
|
||||||
"condition": cs.Condition,
|
return utils.BadRequest(c, err.Error())
|
||||||
"shelf": cs.Shelf,
|
}
|
||||||
// temperature: no column exists anywhere yet for this.
|
|
||||||
"temperature": "N/A",
|
var consignments []models.Consignment
|
||||||
"status": cs.Status,
|
if err := db.DB.Where("currenthubid = ? AND status = ? AND updatedat BETWEEN ? AND ?", hubID, constants.ConsignmentInwardedAtHub, from, to).
|
||||||
"updatedat": cs.Updatedat,
|
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)))
|
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
|
// 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 {
|
func GetHubBatches(c *fiber.Ctx) error {
|
||||||
hubID := c.Locals("hubid").(int)
|
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
|
var tripsheets []models.Tripsheet
|
||||||
if err := db.DB.Where("sourcehubid = ? AND deletedat IS NULL", hubID).
|
if err := query.Order("createdat DESC").Find(&tripsheets).Error; err != nil {
|
||||||
Order("createdat DESC").Find(&tripsheets).Error; err != nil {
|
|
||||||
return utils.Internal(c, "failed to fetch batches")
|
return utils.Internal(c, "failed to fetch batches")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -254,7 +254,9 @@ func RegisterRoutes(app *fiber.App, cfg *config.Config) {
|
|||||||
hubAuth := hub.Use(middlewares.HubStaffAuth(cfg))
|
hubAuth := hub.Use(middlewares.HubStaffAuth(cfg))
|
||||||
hubAuth.Get("/dashboard", controllers.GetHubDashboardStats)
|
hubAuth.Get("/dashboard", controllers.GetHubDashboardStats)
|
||||||
hubAuth.Get("/bookings/unassigned", controllers.GetHubUnassignedBookings)
|
hubAuth.Get("/bookings/unassigned", controllers.GetHubUnassignedBookings)
|
||||||
|
hubAuth.Get("/bookings", controllers.GetHubBookingsRange)
|
||||||
hubAuth.Get("/inbound/today", controllers.GetHubInboundToday)
|
hubAuth.Get("/inbound/today", controllers.GetHubInboundToday)
|
||||||
|
hubAuth.Get("/inbound", controllers.GetHubInboundRange)
|
||||||
hubAuth.Post("/bookings/:id/inbound", controllers.CreateInboundScan)
|
hubAuth.Post("/bookings/:id/inbound", controllers.CreateInboundScan)
|
||||||
hubAuth.Post("/bookings/:id/assign-miler", controllers.HubAssignMiler)
|
hubAuth.Post("/bookings/:id/assign-miler", controllers.HubAssignMiler)
|
||||||
hubAuth.Post("/bookings/:id/auto-assign", controllers.HubAutoAssign)
|
hubAuth.Post("/bookings/:id/auto-assign", controllers.HubAutoAssign)
|
||||||
|
|||||||
Reference in New Issue
Block a user