Commit graph

1792 commits

Author SHA1 Message Date
Khalim Conn-Kowlessar
ae6a6ebf68 fix(live): flag major condition issues by the issue, not a retired stage / S3 URL
24 Charlesworth Street (deal 507693469922) has a major condition issue recorded
(major_condition_issue_description = "Damp in bathroom") but didn't appear in the
live tracker's condition-issue surfaces. Two causes, both now keyed on the issue
itself via a shared hasMajorConditionIssue(deal) predicate:

- majorConditionDeals filtered on dealstage === "3061261536" — a HubSpot stage
  that was retired, so the set was always empty. (This set is also not yet
  rendered in any card — noted for follow-up.)
- The rendered "Damp, Mould & Other Condition Issues → Flagged at Survey" count
  (computeDampMouldRisk.surveyFlagDeals) keyed on major_condition_issue_evidence_s3_url,
  which is null for this deal (its photos are in the raw HubSpot field). This is
  the count the property was actually missing from.

There is no yes/no column; the description is the "Yes" signal (only filled when
an issue exists). Removed the now-dead MAJOR_CONDITION_STAGE_ID. TDD:
red→green→refactor; 66 live-tracker tests, full suite (502) + typecheck + lint green.

Also gitignore .sandcastle/ (untracked agent worktree scratch).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 16:42:55 +01:00
Daniel Roth
fe77a2e74e
Merge pull request #404 from Hestia-Homes/feature/ara-projects-schema
Feature/ara projects schema
2026-07-20 13:31:02 +01:00
Daniel Roth
fa9461167b Rework Projects schema after PR feedback
Post-PR structural changes to the Projects module schema (migration
regenerated; not yet applied):

- Delete the contractor table. Contractors are organisations:
  project_workstream_contractor.contractor_id -> organisation_id
  (uuid -> organisation).
- Rename project.client_id -> organisation_id (uuid -> organisation).
- uploaded_files: drop project_workstream_id and
  project_workstream_contractor_id; keep
  project_workstream_evidence_requirement_id; add nullable work_order_id
  (evidence is per work order, not per project workstream).
- Rewire relations accordingly and regenerate migration 0273.
- Reconcile schema.md with all of the above.

Note: deleting contractor drops its phone_number / email_address, which
have no equivalent on organisation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 11:27:16 +00:00
Daniel Roth
10992f41ae Note the file_type reuse trade-off in schema.md Evidence Model
Record that, because evidence requirements reference the file_type enum
rather than a document_type table, adding a new required document kind is
a schema change (enum value) rather than a data insert.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 16:29:05 +00:00
Daniel Roth
aa0ac24868 Drop document_type table; reuse file_type enum for evidence requirements
Evidence requirements now reference the existing uploaded_files file_type
enum instead of a separate document_type reference table, so the required
and submitted document kinds share one taxonomy.

- Remove the document_type table and its relations.
- project_workstream_evidence_requirement.document_type_id ->
  file_type (file_type enum, NOT NULL).
- Extract fileType/fileSource enums into uploaded_files_enums.ts so the
  Projects module can reference fileType without an import cycle
  (uploaded_files.ts imports the Projects tables for its FK columns).
  uploaded_files.ts re-exports the enums for backwards compatibility.
- Regenerate migration 0273 (now 10 Projects tables, not 11).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 16:26:00 +00:00
Daniel Roth
6f9097d293 Fix drizzle-kit schema glob and generate Projects migration
drizzle-kit recurses the schema directory and tries to compile every
file as JS/TS. The new projects/schema.md broke generation with a
SyntaxError. Narrow the glob to *.ts so docs can live alongside schema.

Generate migration 0273 for the Projects module: 11 new tables, the
three nullable uploaded_files FK columns, and the new "projects"
file_source enum value.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 15:36:08 +00:00
Jun-te Kim
d29da8a848
Merge pull request #402 from Hestia-Homes/fix/remove-orphaned-queries-review-panel
Some checks failed
Test Suite / unit-tests (push) Has been cancelled
fix(analytics): remove orphaned QueriesReviewPanel import
2026-07-17 16:06:19 +01:00
Jun-te Kim
9c8184d48b fix(analytics): remove orphaned QueriesReviewPanel import
AnalyticsView.tsx imported and rendered QueriesReviewPanel, but that
component file was never added in any commit -- the import has been
dangling since it was introduced, breaking the production build with
"Module not found: Can't resolve './QueriesReviewPanel'".

Removes the import and its render block. The Queries/Review-with-Landlord
row can be reintroduced once the component actually exists.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 15:04:43 +00:00
Jun-te Kim
ddff8366cb
Merge pull request #401 from Hestia-Homes/feature/add-properties-search-filter
feat(add-properties): filter long address result lists
2026-07-17 15:58:35 +01:00
Jun-te Kim
90800d3786 feat(add-properties): filter long address result lists
Postcode searches can return ~100 addresses. Add a client-side filter
above the results that narrows the list live by address substring, so a
user can jump to a house number or street name without scrolling.

- Filter box only appears when a postcode returns more than 8 addresses
- Header count reflects the filtered subset (e.g. "12 of 97 addresses")
- Empty state when nothing matches; clear button and new searches reset it

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 14:53:08 +00:00
Daniel Roth
b21be2e5cd Reconcile Projects schema.md with implementation decisions
Update the proposal doc to match what was actually built:
- client_id is uuid -> organisation (Client maps to organisation).
- property_id FKs are bigint (Property has a bigint PK).
- status/priority columns documented as text (values TBC).
- Drop the free-text uploaded_by column; note the existing bigint
  FK -> user is reused.
- Document the new minimal project_type table.
- Record that a "projects" file_source value was added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 14:17:41 +00:00
Daniel Roth
e452fc9ab2 Add Projects module database schema
Introduce the Projects domain model as Drizzle table definitions
(schema-only; no migration generated/run yet).

New tables in src/app/db/schema/projects/projects.ts: project,
project_type, project_property, contractor, workstream,
project_workstream, project_workstream_stage,
project_workstream_contractor, document_type,
project_workstream_evidence_requirement, work_order.

Also:
- uploaded_files: add "projects" file_source value and three nullable
  bigint FK columns linking evidence to the Projects module.
- relations.ts: wire the Projects relations.
- db.ts: register the new schema module.

Deviations from schema.md, decided during implementation:
- client_id is uuid -> organisation (Client maps to organisation).
- property_id FKs are bigint (Property has a bigint PK, not integer).
- TBC status/priority columns modelled as text for now.
- No new uploaded_by column; reuse the existing bigint FK -> user.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 14:14:30 +00:00
Jun-te Kim
dd5811285f
Merge pull request #399 from Hestia-Homes/feature/add-properties-search
feat(add-properties): filter long address result lists
2026-07-17 15:00:03 +01:00
Daniel Roth
558633c128
Merge pull request #400 from Hestia-Homes/feature/abri-resource-hubspot-migration
Abri API: add third party surveyor id to hubspot deal data table
2026-07-17 14:29:10 +01:00
Daniel Roth
d0fd4ce498 migration files 2026-07-17 12:42:01 +00:00
Daniel Roth
4a73df3a25 add third party surveyor id to hubspot deal data table 2026-07-17 12:41:38 +00:00
Jun-te Kim
5810afa35e feat(add-properties): filter long address result lists
Postcode searches can return ~100 addresses. Add a client-side filter
above the results that narrows the list live by address substring, so a
user can jump to a house number or street name without scrolling.

- Filter box only appears when a postcode returns more than 8 addresses
- Header count reflects the filtered subset (e.g. "12 of 97 addresses")
- Empty state when nothing matches; clear button and new searches reset it

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 14:56:42 +00:00
Jun-te Kim
769030ab49
Merge pull request #391 from Hestia-Homes/feature/design-extra-pm-columns
feat(live): add planning fields as toggleable property columns
2026-07-14 15:44:07 +01:00
Jun-te Kim
0ad66c25d4
Merge pull request #393 from Hestia-Homes/feature/epc-cards-count-certificates
fix(reporting): EPC cards count certificates, not predictions (ADR-0014)
2026-07-14 15:43:30 +01:00
Jun-te Kim
5dc9c49132 fix(reporting): quote the camelCase aliases on the EPC coverage query
The card rendered a blank value and "NaN%": Postgres returns column names
verbatim, so `AS without_epc` came back as `without_epc` while the code read
`withoutEpc` — undefined, and `undefined / total * 100` is NaN.

`db.execute<T>()` asserts the row shape rather than checking it, so tsc, lint and
the full suite all passed on the broken query. The previous field names (`estimated`,
`actual`) were single words and happened to survive the round trip; renaming to
camelCase did not. Quoted aliases are the existing convention (see utils.ts).

Verified against portfolio 839: keys come back as [withoutEpc, withEpc], and the
card renders 29 / "0.5% have no EPC on record".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 12:41:00 +00:00
KhalimCK
140383f18f
Merge pull request #392 from Hestia-Homes/feature/tag-select-all-matching
feat(tags): explicit select-all-matching in bulk tag mode
2026-07-14 13:31:30 +01:00
Khalim Conn-Kowlessar
5ed5a7f57c fix(tags): make "Select all N matching" a visible secondary button
Impeccable critique (P1): the escalation was a bare blue text-link on the bar's
pale-blue tint, no persistent underline — invisible next to the navy primary,
and it's the exact action users hunt for. Promote it to a secondary button
(white fill + brand-navy border + check icon, fills on hover) so it stands out
without competing with "Choose a tag"; demote "Clear selection" to a legible
neutral underlined link (was blue-on-blue).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 12:21:56 +00:00
Jun-te Kim
0933f05173 fix(reporting): EPC cards count certificates, not predictions (ADR-0014)
"Homes Without an EPC" and "Expired EPCs" were both derived from estimatedSql,
which asks "was this picture gap-filled?". That conflates two different questions:
provenance (was it modelled?) and coverage (does a certificate exist?).

They used to agree, because a gap-fill implied no certificate. Model ADR-0054 ends
that: the gov EPC API only serves certs registered since 2012, so a pre-2012
dwelling is gap-filled even though a real, stale certificate exists — and gets the
new source='expired'. On portfolio 824, 375 of the 403 "without an EPC" homes have
exactly that: a real certificate, just an out-of-date one. A different, and far more
actionable, problem than never having been certified.

Two defects fell out of the same conflation:

  - Every query matched source='predicted' literally, and `source` is plain text
    with no enum or CHECK. An 'expired' row matches neither the lodged nor the
    predicted alias, so the home would have read as having a VALID CURRENT cert —
    silently gone from both cards, no Estimated badge, nothing to catch it.
  - isExpiredSql read epl.registration_date alone, but 714 of 824's 4,873 lodged
    certs carry only inspection_date. All 714 counted as current; 170 are in fact
    >10 years old. lodgementDateSql already coalesced the two; expiry didn't.

The cards now partition on coverage: withoutEpcSql = no lodged AND no historical
record; isExpiredSql = a cert of either kind, out of date. estimatedSql keeps its
own meaning and spans the whole predicted slot, so an expired home is estimated AND
has an EPC — both true. The `AND estimated = false` guard is gone from the Expired
card: an expired home IS estimated, so it excluded the very homes being counted.

Legacy portfolios are untouched (632: 8/1662 before and after). 824 moves to
913 → 1,083 expired immediately (the inspection_date fix), then to 25 / 1,461 once
Model writes expired rows. Safe to deploy BEFORE Model — the predicates just find
none. The reverse was not safe, which is why this goes first.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 12:19:02 +00:00
Khalim Conn-Kowlessar
a7781eca81 fix(tags): header checkbox selects all loaded rows, not just the page
With a 7-row page size, the header "select all" (toggleAllPageRowsSelected)
only selected the visible 7 — so it never reached "all loaded", the condition
that reveals the "Select all N matching" escalation. It now selects every loaded
row (toggleAllRowsSelected):
- when everything matching is already loaded, that IS the whole set (tag via ids);
- when more match than are loaded (e.g. 250 loaded of 10,000), the escalation
  appears and routes through mode:"filter" — the server resolves and tags the
  entire matching set (getFilteredPropertyIds, chunked), so memory is never the
  ceiling and no huge id list is sent from the client.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 12:12:05 +00:00
Khalim Conn-Kowlessar
99025ce4b3 feat(tags): explicit select-all-matching in bulk tag mode
The header checkbox only selects loaded rows, so bulk-tagging a portfolio with
more properties than are loaded (250-row window) silently tagged just the page.

Bulk mode is now explicit (Gmail pattern): nothing selected = no target (Choose
a tag is disabled), so a tag can't be applied to everything by accident. Tick
rows to target them; once every loaded row is ticked and more match, a "Select
all N matching" escalation switches the target to the whole matching set,
resolved server-side via the existing assignments filter mode (no backend
change). Any manual checkbox change drops the escalation back to the explicit
selection; changing filters / leaving the mode clears it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 11:34:37 +00:00
KhalimCK
fc5fc80d88
Merge pull request #389 from Hestia-Homes/feature/portfolio-tagging
feat: portfolio tagging system (ADR-0013)
2026-07-14 11:52:58 +01:00
Khalim Conn-Kowlessar
0b4f88165c feat(live): add four more planning fields as toggleable columns
Surface Design Constraints, Planning Comments, Planning Status, and
Planning Suggested Approach in the Live Reporting properties table,
following the same read-path wiring as the previous planning columns
(query mapping, HubspotDeal type, column defs, hidden-by-default toggle,
CSV export, test fixtures). DB columns already exist via migration 0267.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 09:33:46 +00:00
Khalim Conn-Kowlessar
8d8d4b7fa1 feat(live): add planning fields as toggleable property columns
Surface the four planning columns from hubspot_deal_data — Planning
Authority, Designated Area, Article 4 PD Rights, Listed Building — as
optional columns in the Live Reporting properties table.

The DB columns already exist (migration 0267); this wires them through
the read path only: query mapping, HubspotDeal type, column defs, the
column-visibility toggle (hidden by default), and CSV export. Rendered
as plain text since they are text columns in HubSpot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:10:25 +00:00
Khalim Conn-Kowlessar
971767a62a fix(portfolio): don't show Run modelling twice in the no-plans state
The not-modelled banner and the toolbar both rendered a navy "Run modelling"
CTA whenever a portfolio had properties but no plans — two identical buttons.
The banner owns the action in that state, so the toolbar's Run modelling is now
hidden while the banner is up (it returns once everything is modelled or the
banner is dismissed). One primary CTA at a time, as intended.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 22:30:58 +00:00
Khalim Conn-Kowlessar
af171f50d7 feat(tags): only show row-select checkboxes while bulk-tagging
The selection checkbox column was persistently visible. Now it appears only
when a bulk assign/remove mode is active (launched from the Tags menu) via a
select-less column set, and the selection clears on entering and leaving the
mode. The default table has no checkbox column; per-property tagging still uses
the inline Tags-cell popover, and the bulk bar's target still follows selection
→ filter → whole portfolio while the mode is open.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 22:10:02 +00:00
Khalim Conn-Kowlessar
46421279fb feat(portfolio): address impeccable critique — no-plans state, contrast, focus
Design pass on the portfolio table (critique: 27/40; P0 was the unmodelled state).

- onboard: a mixed-safe "not modelled yet" nudge above the table — shown whenever
  any loaded property lacks a plan (so a part-modelled portfolio still gets it),
  wired to Run modelling. Turns a wall of empty cells into a next step without
  hiding real data for the rows that ARE modelled.
- harden: one empty-cell vocabulary (EmptyCell) replacing the four divergent
  renderings (blank div, slate-300 "—", italic "No cost", "Unknown"); AA-legible
  and aria-labelled. Empty EPC bubble is now a labelled placeholder ring, not an
  invisible div. Export failures surface an inline error instead of dying silently.
- a11y: the quick-filter clear is no longer a <span role=button> nested inside a
  <button> — pill is a container with sibling open/clear buttons.
- distill: exactly one navy CTA at a time — Add properties leads an empty
  portfolio, Run modelling leads a populated one; the other demotes to neutral.
- colorize: black tokens in PropertyFilters + the Edit-Columns checkboxes now use
  the brand navy/brandmidblue scale (was raw black, drifting off the toolbar).
- cleanup: removed the unreachable delete-property modal (no DELETE endpoint,
  onDeleteProperty never called). Real property deletion is its own slice.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 21:50:27 +00:00
Khalim Conn-Kowlessar
5872e7c335 feat(tags): consolidate bulk-tag actions into a toolbar Tags menu
Replaces the isolated "Assign tags from file" button with a single Tags ▾
dropdown in the toolbar (grouped with Export / Add properties), offering the
three bulk paths: upload from file, assign a tag, remove a tag.

Assign/Remove enter a lightweight contextual mode (TagActionBar) whose target
follows the live table — the row selection if any, else the active filter set,
else the whole portfolio — so ticking rows or changing filters re-scopes it in
place, and it supports removal, not just assignment. Row checkboxes stay visible
at all times; a "N selected" chip now sits by the results count. The upload
modal gains downloadable CSV templates (Property Ref / UPRN).

Removes TagBulkBar (superseded by TagsMenu + TagActionBar).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 21:17:18 +00:00
Khalim Conn-Kowlessar
47fd733b45 fix(tags): address PR #389 review — server guards, error handling, reuse
- Reject `tagIds` in the modelling-runs trigger route (400) until the distributor
  resolves tag_ids — the RunModellingClient guard was UI-only, so a direct POST
  (or a future caller) could dispatch a tagged run that silently models the
  unscoped superset (preview≠run, ADR-0008). Defence-in-depth now.
- Narrow the tags POST catch to Postgres 23505 (new isUniqueViolation helper) so
  transient/other DB errors surface as 500, not a misleading duplicate-name 409.
- Wrap the PATCH rename in the same unique-violation handler — the pre-check is
  racy, so a concurrent rename collision now returns 409, not an uncaught 500.
- TagsCard reuses usePortfolioTags / portfolioTagsKey / Tag instead of a copied
  query + key, so every tag surface stays in sync if the key changes.
- Scope DataTable getRowId to selection-enabled tables only, leaving other
  consumers (plan table) on TanStack's default row identity.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 19:50:10 +00:00
Khalim Conn-Kowlessar
7cd33ab18d Merge remote-tracking branch 'origin/main' into feature/portfolio-tagging
# Conflicts:
#	src/app/portfolio/[slug]/components/PropertyFilters.tsx
#	src/app/portfolio/[slug]/utils.ts
#	src/app/utils/propertyFilters.ts
2026-07-13 19:49:47 +00:00
Khalim Conn-Kowlessar
e80803c50a fix(tags): fall back to row index in DataTable getRowId for idless rows
The shared DataTable is also used by the plan table, whose PlanWithTotals rows
have no `id` (they carry planId/propertyId). A bare String(row.id) keyed every
plan row as "undefined" — colliding React keys. Key by id when present, else by
index (restoring the pre-change default for idless consumers).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 19:16:51 +00:00
Khalim Conn-Kowlessar
1d12fa1baf refactor(tags): one calm, AA-safe chip vocabulary across every surface
Impeccable pass. Replaces the solid full-saturation chips (a heavy-accent
inconsistency vs the settings card's soft chips) with a single quiet vocabulary
per PRODUCT.md: a soft tint of the Tag's freeform hex, a colour dot, and dark
hue-preserving ink whose contrast on the tint is guaranteed ≥4.5:1 for any
colour (new tagChipTokens, TDD — yellow and every hue verified). Settings,
table, filter, bulk bar, upload, and run UI now share one TagChip. Also drops a
stray dev scratch file and gitignores the pattern.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 19:09:27 +00:00
Khalim Conn-Kowlessar
9749e8525b feat(tags): tags multi-select in the Run modelling filter
Adds a Tags FilterColumn (per-portfolio options from GET /tags, colour dots,
counts) feeding filters.tagIds; folds it into filterCount/clearFilters and
removes the "Tags are coming" copy. The in-app preview already counts by tag
membership, so the selection previews exactly. The run button is guarded while
tags are selected — the distributor can't resolve tag_ids yet (backend ask,
ADR-0013), so previewing is honest but running stays disabled until it lands.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 19:00:12 +00:00
Khalim Conn-Kowlessar
f98e36ad87 feat(tags): bulk tag assignment — selection, filter-driven, and file upload
Adds row multi-select to the property table (new checkbox column + stable
id-keyed selection surviving pagination) with a bulk bar that tags the
selection ({mode:"properties"}) or every property matching the active filter
({mode:"filter"}, whole set resolved server-side). Plus a Bulk tag assignment
upload modal: client-side SheetJS → parseTagIdentifierRows → pick a target tag
→ {mode:"identifiers"} → match summary (matched / already-tagged / unmatched).
Selection is filter-scoped and clears on filter change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 17:14:30 +00:00
Khalim Conn-Kowlessar
5abf4a3a10 feat(tags): dynamic Tags filter in the property-table filter panel
Adds a "Tags" field to PropertyFilters with per-portfolio options fetched
via GET /tags plus a synthetic "Untagged" bucket. Emits enum_one_of with
value = JSON of tag ids (+ "__untagged__"); the server SQL already resolves
this (any-of, ADR-0013). Condition chips label by tag name and render
"(deleted tag)" for dangling ids.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 16:58:19 +00:00
Khalim Conn-Kowlessar
b9d7bb4bba feat(tags): property-table Tags column + inline assign popover
Chips with "+N" overflow in a default-visible, toggle-able column; the
cell doubles as an assign popover that toggles membership via
POST …/assignments {mode:"properties"}, optimistic across every cached
property-table page and reconciled on settle. Freeform-hex chips get an
auto-contrast foreground (new pure colour util, TDD). Shared usePortfolioTags
hook keeps settings/table/filter in sync.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 16:52:48 +00:00
KhalimCK
f801025ce3
Merge pull request #387 from Hestia-Homes/feature/tags-schema
feat(db): portfolio_tag + property_tag tables (tagging system)
2026-07-13 17:26:13 +01:00
Khalim Conn-Kowlessar
c5d58dbc96 feat(tags): table query + tag filter, filter-driven bulk assign, preview join, settings UI
Backend, end to end (ADR-0013):
- getProperties now returns each property's tags (name-ordered JSON); the
  PropertyWithRelations type gains a `tags: PropertyTag[]` field.
- A "tags" property-table filter (EXISTS/NOT-EXISTS on property_tag, any-of +
  an "Untagged" bucket) — PK-indexed by property_id, so no join added.
- getFilteredPropertyIds resolves the whole matching set for filter-driven Bulk
  tag assignment; the assignments route's `filter` mode uses it.
- Run filter: previewModellingRun joins property_tag for tag_ids; the two
  modelling routes accept tagIds. Distributor ask documented in
  docs/backend-asks/tag-ids-run-filter.md.

UI:
- Settings → Tags: create (name + colour), edit, delete-with-count-confirm,
  list with property counts (TanStack v4). Sidebar link added.

Typecheck clean; 431 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 16:23:49 +00:00
KhalimCK
aa38c916d2
Merge pull request #384 from Hestia-Homes/feature/portfolio-list-override-descriptors
feat(portfolio): resolve list descriptors via landlord-override precedence
2026-07-13 17:09:40 +01:00
Khalim Conn-Kowlessar
88c0c5b13d feat(tags): CRUD + assignment routes, bulk-file parser, run-filter tag_ids
- Run filter gains an optional tag_ids key (ADR-0013): normaliseRunFilters
  coerces/dedupes ids, buildRunRecord + parseRunConfig round-trip it, and
  selectionSummary counts it. Pure, TDD (9 new cases).
- parseTagIdentifierRows (pure, TDD): a CSV/Excel 2D array → the identifier
  column to match on (landlord_property_id or uprn, header-detected), trimmed,
  deduped, capped at 10k.
- Routes (write-gated via shared getPortfolioRole/canWriteTags):
  GET/POST /tags (list w/ counts, create), PATCH/DELETE /tags/[id] (edit,
  delete+cascade), POST /tags/[id]/assignments (properties add/remove +
  identifiers bulk-match with a summary). Names validated by validateTagDraft.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 16:04:28 +00:00
Khalim Conn-Kowlessar
ed1dae4e99 feat(tags): pure Tag draft validation/normalisation (TDD)
validateTagDraft: trims + requires a name, caps it at 30 chars, rejects a
case-insensitive duplicate against the portfolio's existing tag names (the
caller excludes the tag's own name on edit), and validates + lowercases a
freeform hex colour (#rgb or #rrggbb). The DB's case-insensitive unique index
remains the ultimate guard; this gives a friendly pre-check for the tag routes
and settings UI. 8 behaviours, built red-green. ADR-0013.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 15:50:09 +00:00
Khalim Conn-Kowlessar
af81827b14 docs: ADR-0013 tagging system + CONTEXT (Bulk tag assignment, Run filter tags)
Captures the grill-with-docs outcome: flat app-owned Tags, four assignment
paths (incl. in-app bulk upload, no FastAPI), tags as a Run-filter key extending
ADR-0008, hard-delete + cascade, reporting-by-tag deferred to the reporting
redesign. Design only — no implementation yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 15:45:14 +00:00
Khalim Conn-Kowlessar
4a841c20d4 docs(context): add Tag glossary entry (grill WIP)
Flat, portfolio-owned, many-to-many label for grouping Properties for bulk
actions. Captured during the tagging grill-with-docs session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 15:45:14 +00:00
Khalim Conn-Kowlessar
0a26f9ed93 feat(db): portfolio_tag + property_tag tables (tagging system)
The schema substrate for the portfolio tagging system (ADR-0013): a flat,
portfolio-owned Tag (name + freeform hex colour, case-insensitively unique per
portfolio) and the many-to-many property_tag membership. Both FKs cascade so a
tag delete drops its memberships and a property/portfolio delete drops its tags.
A tag_id-leading index backs the reverse lookup (chips, filter, run resolution).

Migration only — not yet applied to prod. Feature code stacks on this branch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 15:44:26 +00:00
KhalimCK
84a485923c
Merge pull request #386 from Hestia-Homes/feature/epc-main-heating-detail-index
feat(db): index epc_main_heating_detail(epc_property_id)
2026-07-13 16:22:16 +01:00
Khalim Conn-Kowlessar
0873fbc5c1 docs(context): Main Fuel reads EPC detail; Wall/Roof/Heating filter by bucket
Updates the portfolio Property-table flagged-ambiguity note for the two changes
in the previous commit: Main Fuel now reads epc_main_heating_detail beneath the
override (no longer override-only for new-approach), and Wall/Roof/Heating are
now filterable via coarse prefix/substring buckets while still displaying the
full free-text description.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:38:42 +00:00