PRD — Package Dropdown on the Inventories Overview (package-scoped calendar)
Status: Drafted from grilling; ready for review
Branch: continues perf/hhserver-vendor-availability-major
Tracking: EPIC: Package dropdown on inventories overview #8333 - tracked on PR #8274
Audience: HungryHub Engineering + CS-Ops
Relationship to prior work: Extends the shipped bookability diagnosis — adds
a package selector to the overview Check form so the availability calendar can be
scoped to one package (or all).
1. Problem
The inventories overview Check form has Party size + Channel, and both
color the 45-day calendar — but only at the restaurant level (“bookable if
ANY package is bookable that day”). To see a specific package’s calendar, ops
must leave the overview and navigate to the separate per-package page
(?restaurant_package_id=X). Feedback: “maybe better add package selection
also besides party-size selection?” — so ops can scope the same calendar to one
package without leaving the page.
2. Goal
Add a Package dropdown to the overview Check form (alongside Party + Channel). Selecting “All” keeps the current restaurant-level calendar; selecting a package renders that package’s calendar in place on the overview — one page where party + channel + package drive one calendar.
3. Grounded reality (what already exists)
- Overview Check form (
_overview.html.erb): GET form withparty+channel, feeding the AJAX-loaded calendar fragment (inventory_calendar→_calendar_body). - Restaurant-level calendar =
date_verdicts(channel:, party:)→fresh_payload(checker, date)(no slug) — fast fresh-calc, “any package bookable”. - Per-package calendar already exists —
_restaurant_package_overview.html.erb(reached via?restaurant_package_id=X), which colors each day viapackage_verdict(rp, date)— the full gating cascade (config / storefront / agenda / supplier-mismatch / outside-window), giving rich per-day reasons. package_verdict-per-day ×45 is already a proven, accepted pattern (the existing per-package page runs exactly that).all_packages(channel)already filters packages byfor_dine_in?/for_delivery?, so a package list is naturally channel-scoped.
4. Solution
4.1 Two verdict paths behind one dropdown
- “All packages” (default) → the current restaurant-level
date_verdicts(fresh-calc). Unchanged. - A specific package → per-day
package_verdict(rp, date)(the gating cascade — same logic the existing per-package calendar uses). This gives the why per day, which is the point of the diagnosis tool. We do NOT makedate_verdictsslug-aware; the dropdown switches which path is correct for the question being asked.
4.2 Unify onto the overview page (B)
The dropdown lives in the overview Check form; submitting re-renders the overview
with the package-scoped calendar in place (via the AJAX fragment). The
existing _restaurant_package_overview route keeps working as a deep-link (not
removed — avoids breaking bookmarks), but the unified overview is the primary UX.
4.3 Package-aware AJAX calendar fragment
- The Check-form JS builds the fragment
data-urlincluding the selectedrestaurant_package_id(identifier chosen because it’s consistent with the existing per-package route and withpackage_verdict(rp, ...); the rp is resolved from the already-loadedall_restaurant_packages— no extra query). inventory_calendarreads optionalparams[:restaurant_package_id], resolves the rp, passes it to_calendar_body._calendar_bodybranches:if rp→ loop(today..today+45)callingpackage_verdict(rp, date)(reuse the exact tile markup + logic the existing per-package calendar uses);else→ currentdate_verdicts.- Day-tile links carry the package: when a package is selected, clicking a
day links to the package-detail page (
restaurant_package_id+date), not the restaurant-level date page.
4.4 The dropdown itself
- Options: “All packages” (default) + every package from
all_packages(channel)(channel-scoped; all packages incl. misconfigured ones — diagnosing them is the point). - Sort valid-first: packages with
active == trueORactive_for_third_party_only == truelisted before the rest. - Label format:
"{package.name_en.presence || package.name} {rp.id} - {rp.slug}".
4.5 Channel interaction
- Form keeps the existing Channel dropdown, default
dine_in. - The package list is scoped to the selected channel (
all_packages(channel)) — so a package can never contradict the channel. Changing channel re-scopes the package list; if the previously-selected package isn’t in the new channel’s list, the selection resets to “All”.
5. Acceptance criteria
- The overview Check form shows Party + Channel + Package; Package defaults to “All packages”.
- “All” renders the current restaurant-level calendar (unchanged).
- Selecting a package renders that package’s calendar in place on the
overview, colored via
package_verdictper day (rich reasons + the calm/grey states it already produces). - Package options are sorted valid-first and labeled
"{name_en||name} {id} - {slug}". - The package list is scoped to the selected channel; changing channel re-scopes it and resets to “All” if the selection no longer applies.
- The AJAX calendar fragment receives and honors
restaurant_package_id; the initial page load stays shell-only (no perf regression — the compute is still deferred). - Day-tile links carry the selected package (→ package-detail on click).
- The existing
?restaurant_package_id=Xper-package route still works. - No regression to “All” behavior, party/channel behavior, or the outside-window state.
6. Non-goals (v1)
- No change to
date_verdictsorpackage_verdictlogic — only routing to them. - No removal/deprecation of the existing
_restaurant_package_overviewroute. - No new per-package availability computation — reuse existing.
- No special-casing of the outside-window (#8331) state for the package-scoped
calendar —
package_verdicthandles its own boundaries. - No customer-facing changes.
7. Risks
- Perf: the package-scoped calendar runs
package_verdict×45, but it’s in the AJAX fragment (deferred, off the initial page load) and is already the proven per-package pattern — no new perf exposure beyond what exists. - Channel/package desync: must reset package→“All” when channel changes and the selection no longer applies (acceptance #5).
- Label nil-safety:
name_en/name/slugmay be nil for odd packages — guard withpresence+ fallbacks.
8. Source-of-truth file map
- Form + calendar skeleton:
app/views/admin/restaurants/inventories/_overview.html.erb - AJAX fragment:
admin/restaurants_controller#inventory_calendar,_calendar_body.html.erb,setup_bookability_context - Verdict paths:
BookabilityPresenter#date_verdicts(All),#package_verdict(per package),#all_packages(channel) - Existing per-package calendar (reuse markup/logic):
_restaurant_package_overview.html.erb