confirmed bundle Type, reactivation endpoints, and big spreadsheet audit - #34
Open
AbeerDas wants to merge 8 commits into
Open
confirmed bundle Type, reactivation endpoints, and big spreadsheet audit#34AbeerDas wants to merge 8 commits into
AbeerDas wants to merge 8 commits into
Conversation
Three related pieces of work off the back of reading the office's real
RouteLabelsFileBMN.xlsx and Kristen's answer on the Type column.
Label type is confirmed, not guessed. Kristen (Slack, 2026-08-23):
Carrier = a normal volunteer route, Commercial = a bulk drop at a
business, Residential = a bulk drop at an apartment or condo. The
spreadsheet backs it — LABELS column I holds exactly these category
keywords. LabelRoute now carries a real `type`, the page renders it
instead of a placeholder, and the filter pills actually filter. Every
row is "carrier" today because route_deliveries only ever sources from
volunteer_routes, so Commercial and Residential correctly match nothing
until bulk drops get their own per-issue delivery record.
Reactivation now exists. POST /api/{volunteers,captains}/{id}/reactivate
clears retired_at, closing the gap where an accidental retirement needed
a database edit to undo. It deliberately does not re-attach routes or
reclaim a territory: retirement released them and someone else may hold
them now, so a reactivated person comes back unassigned. Covered by
integration tests for both roles, including the 409 when they were never
retired.
docs/reference/route_labels_spreadsheet.md records what their workbook
actually contains. The file itself is NOT committed and must not be —
322 names, 310 home addresses, 157 phones, 384 emails and seven years of
payment history, against a public repo. The doc is structure and counts
only, plus how to decrypt a copy (password BMN) if a future session
needs one.
What it turned up, none of it acted on here:
- A third of their 468 "people" are not first+last names — 80
organizations, 41 shared households, 35 single words. Our required
first_name/last_name cannot hold them.
- Phone is filled on 19% of rows and email on 43%, but createVolunteer
requires both. Most of their roster would fail validation on import.
- Captain pay includes a car allowance we do not model. Route91 spells
it out: 36 drops x $1.50 + $15 gas = $69. We compute the $54 only.
- SKIP HOUSES is a real per-territory do-not-deliver list with nowhere
to live, and it will make automated house counts (#23) overcount.
- Substitutes have been used once in seven years.
It also settles several open items: the RT number is per territory (three
sheets agree), Bundle N of M is correct, territory drops do have a date
(added/removed, not per-issue), and the label stock question — they print
on real peel-and-stick stock two sheets at a time, which makes the cut
guides added in #30 likely wrong. Flagged in the flow doc rather than
reverted, pending the SKU.
Second pass over the workbook, plus the two sections asked for. New in the analysis: - The Word merge reads only `LABELS!$A$1:$E$221`. Five columns reach the label, so the category keyword has never printed on one — it is a working column. And only rows 2-221 of 468 are in range, with the same mix of territories and categories above and below the line. Either the bottom half genuinely needs no labels or the range is stale and some are missed each issue; added as a question rather than an assumption. - `Sheet1` is a captain's pickup worksheet with a literal Labeled/Unlabeled column per bundle. That is our `bundle_labels` model in spreadsheet form and independently confirms the per-bundle decision. - `Payments2026` totals $915.80 per issue across 24 captains, so roughly $20k a year — a sanity check for the overview's cost figures. - `Route24` shows per-paper pay in the open (1,200 x $0.18 = $216) and carries "DISCONTINUED 8.22.2023 AS PER SUSAN": they retire a route with a reason and a date, we soft-delete with neither. - `QUITS` has drifted structurally — columns hold different kinds of value in different rows, only 117 of 431 have a parseable date, and one reason is "Deceased", which is not the same event as quitting. Two new sections: §5 lists the 17 things the office demonstrably does today that our software has no way to represent, each pointing at the evidence. Some were deliberate scope cuts; the list exists so those stay decisions rather than oversights. §6 replaces the old six-line priority list with the actual action items: ten questions to put to the office and five decisions for the team, in non-technical language, plus what is already answered and needs nothing.
AbeerDas
marked this pull request as ready for review
August 24, 2026 03:00
Add docs/reference/label_template_docx.md, covering MasterLabelsMELINDA.docx — the Word mail-merge template our PDF exporter reimplements. Every constant in lib/pdf/label-geometry.ts was re-derived from the file and verified; the 21-per-sheet grid is now exact rather than inferred (the document holds exactly 20 NEXT fields). The docx itself stays out of the repo: its properties carry staff names and an internal network path. Re-read the workbook against the tidied copy the office sent. Two structural findings: LABELS is two passes through the territories, and the merge range covering only the first is deliberate — Sheet1 shows every unlabeled bundle is exactly 25 or 50 papers and every labeled one is a remainder quantity. That answers the "half the rows never print" question. Also corrects the earlier reading of substitutes: 'away' is written into the per-issue payment cells and appears six times in 2026, not once in seven years. Record the decisions taken against the gap list, including the two that go against what the file shows (no car allowance, RT on the captain) so they stay decisions rather than oversights. Resolve the territory-on-label open item — the template's first merge field is the RT number all along.
…emplate The office has a phone for 19% of their roster and an email for 42%, so requiring both blocked four in five of their real volunteers from being entered at all. Confirmed 2026-08-24: optional for everyone, not just on import. Drop NOT NULL from email and phone on volunteers and captains. The request schemas normalise undefined, null, "" and whitespace to null so "not on file" is one state rather than three, while still rejecting a malformed address when one is actually given. The side panel drops its three required-field guards, seeds form state with ?? "", and reads back "Not on file". Two things this surfaced. The member search filters interpolated the email into a template string, so a null would have rendered as the literal "null" and matched every contactless person on that query. And the edit form's dirty check compared "" against null, marking the field changed on every save. Also set the label's copy count to 24pt, the size the Word template uses; it had been sharing the RT chip's 28pt. The two now take separate baselines from a shared helper, since Word centres each cell independently rather than aligning them. Cut guides are deliberately left in place.
…base The hosted database's migration history has 20260728000000, applied from the house-count branch, but the file never landed on main — so every `db push` failed on a remote version with no local counterpart, for everyone. Restore the file verbatim from 8e2bbdf so the repo reflects what the database actually has. Nothing is applied by this: the remote already records the version, so push skips it. Chosen over `migration repair --status reverted`, which would have cleared the bookkeeping while leaving the table in place and set the house-count branch up to fail later on a duplicate CREATE TABLE. This also unblocked 20260805000000_payout_comment_settled, a column-comment migration that had been sitting unapplied on the remote.
…+ last A third of the office's roster is not one person: 122 organizations, 40 rows naming several people who share a delivery, 9 with no surname at all. Our required first_name + last_name mangled every one of them, and a household sharing a route is one delivery rather than two volunteers. display_name becomes authoritative — it is what prints on a label, what the lists show, and what search matches. first_name and last_name stay, because surname sort and structured search are worth keeping for the people who have them; they just stop being mandatory. The backfill is exact, since both columns were NOT NULL and validation required a non-empty trimmed string. On create, a caller giving first + last gets the display name composed for them, so the common case stays a two-field write; a caller naming a church sends display_name alone. The create form offers Person or Organization/household and renders two fields or one to match, which is the whole feature as far as the office is concerned. Ordering moves from last_name to display_name, since last_name is now nullable and would have sorted every organization into a clump at the end.
Two things the office asked for on 2026-08-24. The RT number is a designation carried by the captain, so it becomes captains.rt_number and the label chip stops printing the RTXX stub. Text rather than an integer: the office prints the leading zero of '01', and one captain's designation is the span '31-71'. Unique where set, so a chip is never ambiguous, and nullable, so a captain can exist before being given one — those still print RTXX, which keeps the gap visible on paper instead of inventing a number. Reprinting: listLabels takes an optional issue id and returns the recent issues alongside the sheet, so the picker costs no second round trip. The screen still defaults to the open issue. Picking a closed one puts it in reprint mode, where Export with nothing selected takes every bundle rather than only the unlabelled ones — a shipped run is normally fully labelled, so the usual default would have found nothing and refused. The number of issues offered is capped at twelve. Reprints are for "the printer jammed on the last run", not for trawling years of history.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Self-review of this branch turned up one miss and several rough edges. The display_name sweep skipped lib/services/members.ts, which composes names from the already-mapped camelCase fields rather than the snake_case row, so it fell between the two passes. The members table — the main people screen — was rendering the literal "null null" for every church, building and household, which is precisely the record type the display_name work exists to support. The volunteer and captain search filters had the same shape: they interpolated the now-nullable halves, so an organization could not be found by name and a search for "null" returned all of them. Reprinting no longer rewrites history. Export marked every bundle labelled by default, and reprint mode selects all of them, so reprinting a shipped issue would silently mark bundles that had deliberately gone out unlabelled. The guard is server-side rather than a markLabelled: false from the caller, so no client can get it wrong. Reprinting is also now reachable when nothing is open. The screen resolved the open issue before rendering, so with none it failed outright and the picker never appeared — impossible in exactly the situation that most calls for a reprint, the stretch after the last run of the year is closed. It falls back to the most recent issue and only refuses when there are no issues at all. Two smaller ones. Renaming a person through first/last now carries display_name along, so the label stops printing the old surname; the recompose is guarded to never fire on a record that is not a person, since a stray first_name write must not clobber "St. Aidan's Church". And a whitespace-only email now reads as "not on file" like a whitespace phone already did, instead of failing as an invalid address.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three related pieces off the back of reading the office's real
RouteLabelsFileBMN.xlsxand Kristen's Slack answer on the Type column.Ticket
None — follow-on from #30/#32 plus a design answer in Slack.
What changed
1. Bundle Type is confirmed, not guessed. Kristen (Slack, 2026-08-23): Carrier = a normal volunteer route, Commercial = a bulk drop at a business, Residential = a bulk drop at an apartment/condo. The spreadsheet independently backs it —
LABELScolumn I holds exactly these category keywords.LabelRoutecarries a realtypefield—placeholdercarriertoday, becauseroute_deliveriesonly sources fromvolunteer_routes— so Commercial/Residential correctly match nothing until bulk drops get a per-issue delivery recorddesign_decisions.md2. Reactivation endpoints.
POST /api/{volunteers,captains}/{id}/reactivateclearsretired_at, closing the gap where an accidental retirement needed a database edit to undo.Deliberately does not re-attach routes or reclaim a territory — retirement released them and someone else may hold them now, so a reactivated person comes back unassigned and gets reassigned by hand. Integration tests cover both roles including the 409 when never retired.
3.
docs/reference/route_labels_spreadsheet.md— a full audit of the workbook they actually run distribution from.Why
The Type column and the RT number were both blocking
[OPEN]items. The Slack answer settled one; reading the spreadsheet settled or advanced several more.It holds 322 names, 310 home addresses, 157 phone numbers, 384 email addresses and seven years of captain payment history — and this repo is public. The doc is structure, counts and patterns only, no records. It documents how to decrypt a copy (password
BMN) so a future session can open one without it living in git.What the audit found — flagged, not acted on
first_name/last_namecan't hold a third of their rostercreateVolunteerRoute91: 36 drops × $1.50 + $15 gas = $69SKIP HOUSES— per-territory do-not-deliver listPERhas a 4th value,N/Apay_typeenum has threeLucky Volunteerpopulated on 38 rowsOpen items this settles
Bundle N of Mis correct — 150+ real values match, on carrier rows only. Bulk-drop rows carry handling instructions instead ("Tie Loosely", "Open and leave in lobby") — that's the deferredVolunteerInstruction.New/Stop, an added/removed date, not a per-issue delivery date.Also removed five stale
open_items.mdentries that were already resolved in code (Google Maps go-live, cron scheduling, Places Autocomplete, Add Member, transfer atomicity).Test plan
members.integration.test.ts— 16/16 pass, including two new reactivation tests (retire → reactivate round trip, and the 409 path) verified against the real databaselistLabels()returnstype: "carrier"on real rowsReviewer should check: the Labels page now shows
Carrierand the pills filter. I could not verify in a browser — the app is behind the login gate.Screenshots
None — UI change unverified visually, see above.
Checklist