From 1c24e93a7a4785ab5bd4ce18c06e4a00de077430 Mon Sep 17 00:00:00 2001 From: Suriya Date: Thu, 9 Jul 2026 15:14:36 +0530 Subject: [PATCH] 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 --- controllers/hubController.go | 188 ++++++++++++++++++++++++++++------- routes/routes.go | 2 + 2 files changed, 156 insertions(+), 34 deletions(-) diff --git a/controllers/hubController.go b/controllers/hubController.go index 18c9c22..edffe7c 100644 --- a/controllers/hubController.go +++ b/controllers/hubController.go @@ -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") } diff --git a/routes/routes.go b/routes/routes.go index 8d15f1f..e2ce532 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -254,7 +254,9 @@ func RegisterRoutes(app *fiber.App, cfg *config.Config) { hubAuth := hub.Use(middlewares.HubStaffAuth(cfg)) hubAuth.Get("/dashboard", controllers.GetHubDashboardStats) hubAuth.Get("/bookings/unassigned", controllers.GetHubUnassignedBookings) + hubAuth.Get("/bookings", controllers.GetHubBookingsRange) hubAuth.Get("/inbound/today", controllers.GetHubInboundToday) + hubAuth.Get("/inbound", controllers.GetHubInboundRange) hubAuth.Post("/bookings/:id/inbound", controllers.CreateInboundScan) hubAuth.Post("/bookings/:id/assign-miler", controllers.HubAssignMiler) hubAuth.Post("/bookings/:id/auto-assign", controllers.HubAutoAssign)