package tools import ( "context" "fmt" "sort" "strings" "nearle/models" ) // "What is out for delivery?" // // The question the order table cannot answer. Fiesta mirrors only three delivery // statuses onto the order — pending, delivered, cancelled — so an order row // reads "pending" from the moment a rider is assigned until the moment the job // completes. A rider standing at the customer's door and a job nobody has been // told about are the same row there. // // The delivery row carries the real stage, so this counts from that instead. // It is the same correction `orderProgress.ts` makes in the console, applied at // the other end. // // ── Counts, not a list ───────────────────────────────────────────────────── // // "What is out for delivery?" is a question about how much, not which. Forty // rows summarised by a model becomes a sentence nobody can check; a stage with a // count against it is a number a dispatcher can act on. The list is one click // away and the answer says where. // The delivery ladder, in the order a job walks it. // // Fixed order rather than whatever the map iterates to: a person reading // "picked 3, accepted 8, arrived 1" has to reassemble the journey in their head // every time. var deliveryStages = []struct { key string label string live bool }{ {"pending", "Not yet accepted", true}, {"accepted", "Accepted", true}, {"arrived", "At the shop", true}, {"picked", "Picked up", true}, {"active", "On the way", true}, {"skipped", "Nobody answered", false}, {"rejected", "Declined by the rider", false}, {"delivered", "Delivered", false}, {"cancelled", "Cancelled", false}, } // StageCount is one rung of the ladder. type StageCount struct { Stage string `json:"stage"` Count int `json:"count"` // True while somebody is still carrying the job. `skipped` and `rejected` // are stopped rather than finished, and both need a person — so neither is // live, and neither is done. Live bool `json:"in_progress"` } // DeliveryProgress builds the tool. func DeliveryProgress(deliveries DeliveryReader) Tool { return Tool{ Name: "delivery_progress", Description: "How many deliveries are at each stage right now — waiting to be accepted, picked up, on the way, delivered. " + "Use for questions about what is out for delivery, how deliveries are going, or how many are still in progress. " + "The order list cannot answer this: an order row says 'pending' for the whole journey.", Scope: ScopeRead, Schema: Schema{}, Handler: func(_ context.Context, req Request) (Result, error) { rows := deliveries.GetDeliveries(models.DeliveryQuery{ Tenantid: req.Caller.Tenantid, Locationid: req.Caller.Locationid, Pagesize: 500, Pageno: 1, }) counts := map[string]int{} var unknown []string for _, row := range rows { stage := strings.ToLower(strings.TrimSpace(row.Orderstatus)) if stage == "" { stage = "pending" } counts[stage]++ } out := make([]StageCount, 0, len(deliveryStages)) live, finished := 0, 0 for _, stage := range deliveryStages { count := counts[stage.key] delete(counts, stage.key) // Stages with nothing in them are left out. A ladder of zeroes // buries the two rungs that have anything on them. if count == 0 { continue } out = append(out, StageCount{Stage: stage.label, Count: count, Live: stage.live}) if stage.live { live += count } else { finished += count } } // A status the ladder has never heard of. Reported rather than // dropped: a stage nobody counted is how a whole category of work // goes missing from a board that looks complete. for stage, count := range counts { unknown = append(unknown, fmt.Sprintf("%s (%d)", stage, count)) } sort.Strings(unknown) result := Result{ Rows: out, Count: live, Source: "/admin/dispatch", Scope: scopeWords(req.Caller), Note: fmt.Sprintf("%d deliveries still in progress, %d finished or stopped. The count is jobs in progress.", live, finished), } if len(unknown) > 0 { result.Note += " Statuses this console does not recognise: " + strings.Join(unknown, ", ") + "." } return result, nil }, } }