updates on the admincontroler page
This commit is contained in:
@@ -2062,6 +2062,41 @@ func AssignMilerVehicle(c *fiber.Ctx) error {
|
|||||||
// BOOKINGS MANAGEMENT
|
// BOOKINGS MANAGEMENT
|
||||||
// --------------------
|
// --------------------
|
||||||
|
|
||||||
|
// bookingDestinationCounts is one row of the grouped bookingdestinations
|
||||||
|
// aggregate: how many destinations a booking carries and how many packages they
|
||||||
|
// add up to across all of them.
|
||||||
|
type bookingDestinationCounts struct {
|
||||||
|
Bookingid int `gorm:"column:bookingid"`
|
||||||
|
Destinationcount int `gorm:"column:destinationcount"`
|
||||||
|
Totalpackagecount int `gorm:"column:totalpackagecount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyDestinationCounts writes the grouped counts onto the bookings they
|
||||||
|
// belong to. A booking with no bookingdestinations rows — every
|
||||||
|
// console-created booking, and every booking predating the customer app —
|
||||||
|
// keeps the 0/0 zero value, which is the correct answer and the one the
|
||||||
|
// console distinguishes from a single-destination booking.
|
||||||
|
//
|
||||||
|
// Split out from the handler because it is the whole of the mapping logic and
|
||||||
|
// the only part that can be tested without a database.
|
||||||
|
func applyDestinationCounts(bookings []models.PickupBooking, counts []bookingDestinationCounts) {
|
||||||
|
if len(bookings) == 0 || len(counts) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
byBooking := make(map[int]bookingDestinationCounts, len(counts))
|
||||||
|
for _, row := range counts {
|
||||||
|
byBooking[row.Bookingid] = row
|
||||||
|
}
|
||||||
|
for i := range bookings {
|
||||||
|
row, ok := byBooking[bookings[i].Bookingid]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
bookings[i].Destinationcount = row.Destinationcount
|
||||||
|
bookings[i].Totalpackagecount = row.Totalpackagecount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func GetAdminBookings(c *fiber.Ctx) error {
|
func GetAdminBookings(c *fiber.Ctx) error {
|
||||||
pageno := max(1, c.QueryInt("pageno", 1))
|
pageno := max(1, c.QueryInt("pageno", 1))
|
||||||
pagesize := min(100, max(1, c.QueryInt("pagesize", 20)))
|
pagesize := min(100, max(1, c.QueryInt("pagesize", 20)))
|
||||||
@@ -2113,6 +2148,27 @@ func GetAdminBookings(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// How many destinations each booking carries, and the packages summed across
|
||||||
|
// them. A customer-app pickup is ONE booking with N destinations, and the
|
||||||
|
// console needs to render "3 destinations · 4 packages" on the collapsed row.
|
||||||
|
// The full array is deliberately NOT preloaded here: the console drains up to
|
||||||
|
// 12 pages of 100 bookings and only opens one row at a time, so the array is
|
||||||
|
// payload the list never reads. Batched exactly like the consignment status
|
||||||
|
// above — one grouped query for the whole page, never one per row.
|
||||||
|
bookingIDs := make([]int, 0, len(bookings))
|
||||||
|
for _, b := range bookings {
|
||||||
|
bookingIDs = append(bookingIDs, b.Bookingid)
|
||||||
|
}
|
||||||
|
if len(bookingIDs) > 0 {
|
||||||
|
var counts []bookingDestinationCounts
|
||||||
|
db.DB.Model(&models.BookingDestination{}).
|
||||||
|
Select("bookingid, COUNT(*) AS destinationcount, COALESCE(SUM(packagecount), 0) AS totalpackagecount").
|
||||||
|
Where("bookingid IN ?", bookingIDs).
|
||||||
|
Group("bookingid").
|
||||||
|
Scan(&counts)
|
||||||
|
applyDestinationCounts(bookings, counts)
|
||||||
|
}
|
||||||
|
|
||||||
pages := int(math.Ceil(float64(total) / float64(pagesize)))
|
pages := int(math.Ceil(float64(total) / float64(pagesize)))
|
||||||
|
|
||||||
return c.JSON(fiber.Map{
|
return c.JSON(fiber.Map{
|
||||||
@@ -2595,7 +2651,23 @@ func AdminBulkCreateBookings(c *fiber.Ctx) error {
|
|||||||
func GetAdminBookingDetails(c *fiber.Ctx) error {
|
func GetAdminBookingDetails(c *fiber.Ctx) error {
|
||||||
id, _ := strconv.Atoi(c.Params("id"))
|
id, _ := strconv.Atoi(c.Params("id"))
|
||||||
var booking models.PickupBooking
|
var booking models.PickupBooking
|
||||||
q := scopeToOwnTenant(c, db.DB.Preload("Parcels").Preload("ServiceOptions").Preload("Payments"), "tenantid")
|
// Destinations is the customer-app half of the booking: one pickup carries N
|
||||||
|
// of them, each with its own consignment, tracking number and stage once the
|
||||||
|
// miler completes pickup (cxPickupFanout.go). The relation has been declared
|
||||||
|
// on the model since the customer app shipped and nothing preloaded it, which
|
||||||
|
// is the entire reason the console could only ever show one drop.
|
||||||
|
//
|
||||||
|
// Ordered by seq ascending and never by anything else: seq is the
|
||||||
|
// customer-facing position and the {index} in
|
||||||
|
// PATCH /customer/bookings/{ref}/destinations/{index}, so the order the
|
||||||
|
// console renders has to be the order the customer addresses.
|
||||||
|
q := scopeToOwnTenant(c, db.DB.
|
||||||
|
Preload("Parcels").
|
||||||
|
Preload("ServiceOptions").
|
||||||
|
Preload("Payments").
|
||||||
|
Preload("Destinations", func(d *gorm.DB) *gorm.DB {
|
||||||
|
return d.Order("seq ASC")
|
||||||
|
}), "tenantid")
|
||||||
if err := q.First(&booking, id).Error; err != nil {
|
if err := q.First(&booking, id).Error; err != nil {
|
||||||
return utils.NotFound(c, "booking not found")
|
return utils.NotFound(c, "booking not found")
|
||||||
}
|
}
|
||||||
|
|||||||
102
controllers/adminDestinations_test.go
Normal file
102
controllers/adminDestinations_test.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"doormile/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// One customer pickup is ONE booking carrying N destinations. The admin list
|
||||||
|
// says how many without shipping the array, and these cover the mapping that
|
||||||
|
// does it — the only half of the change that can be tested without Postgres.
|
||||||
|
// The queries themselves need the integration pass (docs/customer-app-api.md).
|
||||||
|
|
||||||
|
func TestDestinationCountsLandOnTheRightBooking(t *testing.T) {
|
||||||
|
bookings := []models.PickupBooking{
|
||||||
|
{Bookingid: 11},
|
||||||
|
{Bookingid: 22},
|
||||||
|
{Bookingid: 33},
|
||||||
|
}
|
||||||
|
// Deliberately out of order and not covering every booking: the grouped
|
||||||
|
// query returns rows for whichever bookings have destinations, in whatever
|
||||||
|
// order the database chose.
|
||||||
|
counts := []bookingDestinationCounts{
|
||||||
|
{Bookingid: 33, Destinationcount: 1, Totalpackagecount: 1},
|
||||||
|
{Bookingid: 11, Destinationcount: 3, Totalpackagecount: 4},
|
||||||
|
}
|
||||||
|
|
||||||
|
applyDestinationCounts(bookings, counts)
|
||||||
|
|
||||||
|
if bookings[0].Destinationcount != 3 || bookings[0].Totalpackagecount != 4 {
|
||||||
|
t.Errorf("booking 11: got %d destinations / %d packages, want 3/4",
|
||||||
|
bookings[0].Destinationcount, bookings[0].Totalpackagecount)
|
||||||
|
}
|
||||||
|
if bookings[2].Destinationcount != 1 || bookings[2].Totalpackagecount != 1 {
|
||||||
|
t.Errorf("booking 33: got %d destinations / %d packages, want 1/1",
|
||||||
|
bookings[2].Destinationcount, bookings[2].Totalpackagecount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A console-created booking has no bookingdestinations rows at all, so the
|
||||||
|
// grouped query returns nothing for it. It must report 0/0 rather than
|
||||||
|
// inheriting a neighbour's counts — the console reads 0 as "no destinations
|
||||||
|
// recorded" and 1 as "a single drop", and they render differently.
|
||||||
|
func TestBookingWithNoDestinationsStaysZero(t *testing.T) {
|
||||||
|
bookings := []models.PickupBooking{
|
||||||
|
{Bookingid: 11},
|
||||||
|
{Bookingid: 99}, // console-created: no destination rows
|
||||||
|
}
|
||||||
|
counts := []bookingDestinationCounts{
|
||||||
|
{Bookingid: 11, Destinationcount: 3, Totalpackagecount: 4},
|
||||||
|
}
|
||||||
|
|
||||||
|
applyDestinationCounts(bookings, counts)
|
||||||
|
|
||||||
|
if bookings[1].Destinationcount != 0 || bookings[1].Totalpackagecount != 0 {
|
||||||
|
t.Errorf("console booking: got %d/%d, want 0/0",
|
||||||
|
bookings[1].Destinationcount, bookings[1].Totalpackagecount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The aggregate is keyed by booking id, so a count can never be written onto a
|
||||||
|
// booking that was not on this page. Guards the map lookup against being
|
||||||
|
// replaced by anything positional.
|
||||||
|
func TestCountsForBookingsOutsideThePageAreIgnored(t *testing.T) {
|
||||||
|
bookings := []models.PickupBooking{{Bookingid: 11}}
|
||||||
|
counts := []bookingDestinationCounts{
|
||||||
|
{Bookingid: 77, Destinationcount: 9, Totalpackagecount: 9},
|
||||||
|
}
|
||||||
|
|
||||||
|
applyDestinationCounts(bookings, counts)
|
||||||
|
|
||||||
|
if bookings[0].Destinationcount != 0 || bookings[0].Totalpackagecount != 0 {
|
||||||
|
t.Errorf("booking 11 picked up booking 77's counts: got %d/%d, want 0/0",
|
||||||
|
bookings[0].Destinationcount, bookings[0].Totalpackagecount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty inputs are the ordinary case on an empty page, not an error.
|
||||||
|
func TestApplyDestinationCountsHandlesEmptyInputs(t *testing.T) {
|
||||||
|
applyDestinationCounts(nil, nil)
|
||||||
|
applyDestinationCounts([]models.PickupBooking{}, []bookingDestinationCounts{{Bookingid: 1}})
|
||||||
|
|
||||||
|
bookings := []models.PickupBooking{{Bookingid: 11, Destinationcount: 0}}
|
||||||
|
applyDestinationCounts(bookings, nil)
|
||||||
|
if bookings[0].Destinationcount != 0 {
|
||||||
|
t.Errorf("no counts should leave the booking at 0, got %d", bookings[0].Destinationcount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A single-destination customer booking must report 1, not 0. The console
|
||||||
|
// branches on destinationcount > 1 to decide whether to show the summary line,
|
||||||
|
// and 1 is what keeps a B2C row reading as it does today.
|
||||||
|
func TestSingleDestinationBookingReportsOne(t *testing.T) {
|
||||||
|
bookings := []models.PickupBooking{{Bookingid: 11}}
|
||||||
|
counts := []bookingDestinationCounts{{Bookingid: 11, Destinationcount: 1, Totalpackagecount: 2}}
|
||||||
|
|
||||||
|
applyDestinationCounts(bookings, counts)
|
||||||
|
|
||||||
|
if bookings[0].Destinationcount != 1 || bookings[0].Totalpackagecount != 2 {
|
||||||
|
t.Errorf("got %d/%d, want 1/2", bookings[0].Destinationcount, bookings[0].Totalpackagecount)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -82,6 +82,13 @@ type cxCreateBookingRequest struct {
|
|||||||
Min int `json:"min"`
|
Min int `json:"min"`
|
||||||
Max int `json:"max"`
|
Max int `json:"max"`
|
||||||
} `json:"estimate"`
|
} `json:"estimate"`
|
||||||
|
// Remarks is the free-text note the customer adds on Review ("Handle with
|
||||||
|
// care"). It is top-level in the documented payload
|
||||||
|
// (docs/customer-app-api-crisp.md) and lands in PickupBooking.Notes, which
|
||||||
|
// the admin Orders table displays and searches. Without the field here
|
||||||
|
// BodyParser drops it silently and every customer-app booking reaches the
|
||||||
|
// console with an empty note.
|
||||||
|
Remarks string `json:"remarks"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateCxBooking creates the pickup.
|
// CreateCxBooking creates the pickup.
|
||||||
@@ -270,6 +277,8 @@ func CreateCxBooking(c *fiber.Ctx) error {
|
|||||||
Estimatemaxrupees: estimateMax,
|
Estimatemaxrupees: estimateMax,
|
||||||
Routekm: quote.RouteKM,
|
Routekm: quote.RouteKM,
|
||||||
|
|
||||||
|
Notes: req.Remarks,
|
||||||
|
|
||||||
Createdat: now,
|
Createdat: now,
|
||||||
Updatedat: now,
|
Updatedat: now,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,18 @@ type PickupBooking struct {
|
|||||||
// GET /customer/track/:trackingno at all: a booking is addressed by id, a
|
// GET /customer/track/:trackingno at all: a booking is addressed by id, a
|
||||||
// shipment by tracking number, and nothing joined the two.
|
// shipment by tracking number, and nothing joined the two.
|
||||||
Trackingno string `json:"trackingno,omitempty" gorm:"-"`
|
Trackingno string `json:"trackingno,omitempty" gorm:"-"`
|
||||||
|
// Destinationcount and Totalpackagecount are not columns either. A
|
||||||
|
// customer-app pickup is ONE booking carrying N destinations, and the admin
|
||||||
|
// list has to be able to say "3 destinations · 4 packages" without shipping
|
||||||
|
// the whole destination array on every row — the console drains up to 12
|
||||||
|
// pages of 100 bookings and does not use the array in the list. Filled in by
|
||||||
|
// the admin bookings list from one grouped query per page (see
|
||||||
|
// applyDestinationCounts). Deliberately NOT omitempty: a console-created
|
||||||
|
// booking has no bookingdestinations rows at all and must report 0, which
|
||||||
|
// the console reads as "no destinations recorded" — omitting the field would
|
||||||
|
// make it indistinguishable from a single-destination booking.
|
||||||
|
Destinationcount int `json:"destinationcount" gorm:"-"`
|
||||||
|
Totalpackagecount int `json:"totalpackagecount" gorm:"-"`
|
||||||
Createdat time.Time `json:"createdat" gorm:"column:createdat;default:CURRENT_TIMESTAMP"`
|
Createdat time.Time `json:"createdat" gorm:"column:createdat;default:CURRENT_TIMESTAMP"`
|
||||||
Updatedat time.Time `json:"updatedat" gorm:"column:updatedat;default:CURRENT_TIMESTAMP"`
|
Updatedat time.Time `json:"updatedat" gorm:"column:updatedat;default:CURRENT_TIMESTAMP"`
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user