Commit graph

1210 commits

Author SHA1 Message Date
Jun-te Kim
f6545c2fa0 property override 2026-06-08 09:38:48 +00:00
Jun-te Kim
b9bbd916b5 Render finalising status on the bulk-upload page and auto-advance to complete
The async finaliser (ADR-0005) introduced the `finalising` status, but the
server page's STATUS_CONFIG had no entry for it, so it fell through to the
`ready_for_processing` fallback ("Awaiting column mapping") and never mounted the
live poller — the page looked stuck even though the Lambda had inserted the
properties and written `complete`.

- Add the `finalising` card ("Uploading to ARA") to STATUS_CONFIG.
- Render OnboardingProgress during `finalising` so it polls live.
- Refresh the server page once when the poll first sees a terminal status
  (guarded by a new `serverStatus` prop to avoid a loop; uses react-query v4
  onSuccess, no useEffect) so it advances to the "Processing complete" card.
- Add a `finalising` → "Uploading to ARA" badge on the uploads list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 09:38:48 +00:00
Jun-te Kim
87aca96dfa bulk upload finaliser 2026-06-08 09:38:48 +00:00
Jun-te Kim
dff86b1ec2 save current progress 2026-06-08 09:38:48 +00:00
Jun-te Kim
22375e1746 Add Step 1 "Verify classification" to bulk-upload review (ADR-0004)
For uploads with classifier columns, surface the classifier's
description→enum assumptions on the awaiting_review screen so the user can
correct any (written source='user') and acknowledge before Finalise.
Previously only the multi-entry order step existed, so non-multi-entry
uploads got no classification review at all and the assumptions were
applied silently.

- detectMultiEntry: capture a sample whenever classifier columns are
  mapped (largest-count row if multi-entry, else first classified row);
  the sample now carries all classifier columns. "sample != null" means
  "there is something to verify"; largestCount >= 2 stays the multi-entry
  signal.
- setVerifyAck + verify-classification PATCH route + useConfirmVerification.
- VerifyClassificationPanel (Step 1); MultiEntryOrderingPanel slimmed to
  order-only with read-only classification annotation; canFinalize gated
  on both steps where each applies.
- Unit tests for detectMultiEntry + ordering helpers.

The verify_ack column + 0219 migration landed separately via #303.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 09:38:48 +00:00
Jun-te Kim
c9b7d71843 Show + edit sample classifications on awaiting_review (#298, #299)
#298: read the classifier's resolved enums for the multi-entry sample's
entries (joined from landlord_*_overrides by normalized description) and show
each beside the raw text, or "not classified" when absent.

#299: make each classification editable — a dropdown of the category's valid
enum values whose selection upserts the override by (portfolio, description)
with source='user', so the classifier never re-clobbers it. UI notes the
portfolio-wide blast radius. Verification ack is folded into the existing
order-confirm (no separate flag/migration); editing is optional review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 09:38:48 +00:00
Jun-te Kim
65a5f57321 Confirm building-part ordering on awaiting_review (#297)
Adds the multiEntryOrdering jsonb column + interactive order picker: the
largest-count multi-entry sample is shown with a building-part dropdown per
file position (one Main building + Extensions), validated as a permutation.
A PATCH route persists { count: permutation } + confirmed, and Finalise is
disabled until the ordering is confirmed when the upload is multi-entry.

Migration for the new column is intentionally not included here — generated
after origin/main is merged (its multi_entry_summary migration lands first).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 09:38:48 +00:00
Jun-te Kim
9cb0cb19f3 lanlord exetension application 2026-06-08 09:38:21 +00:00
Jun-te Kim
052ab446e0 merged from main 2026-06-08 09:36:26 +00:00
Daniel Roth
d2c9c0696a add guard clause when backfilling plan_id to recommendation 2026-06-05 10:13:49 +00:00
Daniel Roth
694bac47e1 backfill materials columns for recommendation table 2026-06-05 10:13:31 +00:00
Daniel Roth
62b10ef808 migration for materials columns on recommendation table 2026-06-05 10:03:34 +00:00
Daniel Roth
a2a5d7212a backfill plan_id using plan_recommendations 2026-06-05 08:16:27 +00:00
Daniel Roth
c40e64bdec add plan_id column to recommendation table 2026-06-05 08:12:57 +00:00
Jun-te Kim
fa9bf538da Add property_overrides table + override_component enum (0221)
The per-Property fact layer deferred by ADR-0004: one row per
(property, building_part, override_component) holding the resolved
landlord-override enum as a denormalised text snapshot, plus the raw
spreadsheet description it resolved from.

Schema only — no writer yet. The bulk_upload_finaliser application will
populate it (recalculate-on-rerun via upsert on the unique key). Design
and rationale (snapshot-not-FK, drop source, recalculate semantics) in
docs/design/bulk-upload-finaliser.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 09:52:37 +00:00
Daniel Roth
2011847aa7 new migration files 2026-06-03 12:55:49 +00:00
Daniel Roth
fb952a6716 update migration sql so that new jsonb fields are mapped correctly 2026-06-03 10:49:54 +00:00
Daniel Roth
9c918c29df migration files 2026-06-03 10:43:32 +00:00
Daniel Roth
5553fee32e Add EPC round-trip fidelity fixes and property_baseline_performance table to Drizzle schema 2026-06-03 10:42:13 +00:00
Daniel Roth
3430fa90f0 add "other" file type 2026-06-03 08:24:48 +00:00
Jun-te Kim
ca4946f594 Add verify_ack column to bulk_address_uploads (0219)
The "Verify classification" acknowledgement flag for ADR-0004 Step 1.
Gates Finalise whenever an upload has classifier columns, independent of
multi-entry, so it lives in its own column rather than on
multiEntryOrdering.

Plain additive column (boolean NOT NULL DEFAULT false), no data backfill,
so it applies cleanly with either `drizzle-kit migrate` or `push`. The
feature that reads/writes it lands separately on
feature/frontend_landlord_overrides.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 17:54:01 +00:00
Jun-te Kim
b84bcd848d Add multi_entry_ordering column to bulk_address_uploads
Additive nullable jsonb column for the user-confirmed building-part ordering
(ADR-0004, issue #297), generated off main. No data migration. The jsonb shape
type is co-located with the column so the schema is self-contained.

Split out as its own migration PR so the DB change can be approved and deployed
independently of the feature's app code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 13:53:05 +00:00
Jun-te Kim
f67c7cfdd6 Add multi_entry_summary column to bulk_address_uploads
Additive nullable jsonb column for multi-entry building-part detection
(ADR-0004), generated off main. No data migration. The jsonb shape type is
co-located with the column so the schema is self-contained.

This is the only migration the multi-entry feature needs that isn't already
on main (the column_mapping inversion, sub_task.service, and the
landlord_overrides tables are all already merged). Split out so the DB change
can be approved and deployed independently of the feature's app code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 12:43:50 +00:00
Jun-te Kim
546316eda9
Merge pull request #293 from Hestia-Homes/feature/non-booked-surveys
Some checks failed
Test Suite / unit-tests (push) Has been cancelled
Feature/non booked surveys
2026-05-29 13:37:30 +01:00
Khalim Conn-Kowlessar
f7b1402790 Swap coordinator/survey-date columns for outcome notes in Removed from Bookings
Removed properties have no coordinator or confirmed survey date by definition,
so those columns were always blank. Outcome notes are the field that actually
explains why the property dropped out of bookings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 12:17:38 +00:00
Khalim Conn-Kowlessar
56fdfa06e4 Only let surveyed outcomes override a Do Not Book status
Previously, any HubSpot outcome would override bookingStatus="Do Not Book"
and keep the property in the normal pipeline. That was too permissive —
outcomes like "Tenant Refusal" or "Not Viable" combined with Do Not Book
should classify the property as Removed from Bookings, not lurk in Queries
or the survey-issues bucket.

Now only completed survey outcomes (Surveyed, Surveyed - Pending Upload,
EPC Completed) override Do Not Book. Any other outcome + Do Not Book
falls through to Removed from Bookings, surfaces in the Halted or Removed
panel, and gets the matching stage badge in the Properties tab. The
redundant "Removed from Bookings" chip in the drill-down table is gone
since the stage classification now carries that signal cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 12:05:53 +00:00
Jun-te Kim
c04ff901c1 Add landlord-override DB migrations (0215 data + 0216 schema)
Splits the DB migration artifacts off the frontend branch so they can
land independently:

- 0215_invert_column_mapping: one-shot data migration inverting
  bulk_address_uploads.column_mapping from header->field to field->header
  (drops 'skip' entries). One-shot — see file header and ADR-0003.
- 0216_add_subtask_service: adds sub_task.service (nullable text) to tag
  which pipeline a subtask belongs to (address2uprn vs
  landlord_description_overrides).

Includes the subtask.ts schema source for 0216 so drizzle's snapshot
stays consistent. No app code depends on these yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 09:50:03 +00:00
Khalim Conn-Kowlessar
5ff99a636b Add Surveyed Date and EPC Certificate Number as optional property columns
Both surface in the column-toggle dropdown and the CSV export, hidden by
default like the other optional columns. surveyedDate sits next to
designDate (chronological survey -> design order); epcPrn sits after the
lodgement date since the PRN is the output of lodgement.

epcPrn was already in the DB schema but absent from the HubspotDeal type
and mapper, so the plumbing is added alongside.

Extracts the CSV-formatting logic out of PropertyTable into a pure
propertyCsv module with tests, locking in the export contract (header
order, en-GB date formatting, null cells empty) so future column drift
is caught.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 20:29:10 +00:00
Khalim Conn-Kowlessar
9f552ad649 Reword "Excluded from Pipeline" card to "Halted or Removed"
Softer, less technical language for landlord-facing copy. The subtitle
now spells out the two states (halted before a survey, or removed from
the project entirely) so the relationship between the section header
and the two cards is explicit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 18:03:45 +00:00
Khalim Conn-Kowlessar
2edfb11469 Surface "Removed from Bookings" and "Removed from Program" as distinct stages
Properties that are intentionally not progressing (bookingStatus = "Do Not
Book" with no outcome, or batch = "Removed from Program") were landing in
the "Queries" bucket, inflating it with non-actionable rows. Two new
terminal DisplayStage values now classify these explicitly, with
precedence Removed from Program > Removed from Bookings > Queries. Both
are excluded from pipeline funnel and stage-progress denominators
(sibling to Queries) and surface as their own cards under "Excluded from
Pipeline" on the analytics tab. Drill-down rows in Survey Issues get
slate chips when a deal carries either flag, preserving outcome history
for properties surveyed before being de-scoped.

Also removes the unused SurveyedResultsPieChart chain (component,
computeOutcomeSlices, OutcomeSlice, outcomePieSlices field).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 17:10:43 +00:00
Khalim Conn-Kowlessar
9e3df59ea0 Broaden the Awaab's Law panel to cover other condition issues
Coordinators record non-damp/mould observations (e.g. wasp nests) in the
same comments field, but the section was framed entirely around damp and
mould. Reframe the panel copy and table titles around "condition issues",
keep "Damp, Mould" up front so the Awaab's Law urgency still leads, and
mark the damp/mould rows specifically with a red badge column so they
don't blend into the broader list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 15:33:08 +00:00
Khalim Conn-Kowlessar
62e9c548f1 Surface coordinator damp & mould commentary in the risk drill-down
The risk drill-down's coordinator-stage table showed "Yes" for every row,
which carried no useful signal. It also missed properties where the
coordinator wrote a comment without setting the growth flag.

Include rows where dampmould_growth is "yes" (case-insensitive) OR the
comment is populated, and render the comment in the cell — truncated
with a popover for the full text, or a "no note from coordinator"
placeholder when the row is here only because the flag was ticked.

Also drop the typo in the schema property name
(damnp -> damp); SQL column unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 14:58:23 +00:00
KhalimCK
ec10a53aeb
Merge pull request #287 from Hestia-Homes/bug/portfolio-invitations
Bug/portfolio invitations
2026-05-28 14:01:55 +01:00
Jun-te Kim
d37ce12f10 booking status 2026-05-28 12:01:22 +00:00
Khalim Conn-Kowlessar
7ced6c0818 Move inviteRequestSchema out of route.ts to satisfy Next 15 build
Next.js 15 enforces a strict allowlist of named exports from route.ts
files; "inviteRequestSchema" was rejected by the route-validator with
"is not a valid Route export field" during npm run build. Splitting the
schema into a sibling module (which is exempt from the allowlist) and
importing it from route.ts and the test. Renames the test file to
match its target module.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 11:29:54 +00:00
Khalim Conn-Kowlessar
f26156ddb5 added x margin 2026-05-28 11:17:21 +00:00
Khalim Conn-Kowlessar
dcd25c9b54 Merge branch 'main' of https://github.com/Hestia-Homes/assessment-model into bug/portfolio-invitations 2026-05-28 11:14:22 +00:00
Khalim Conn-Kowlessar
618a92a06d Drop the unused invitee name field from the invite flow
The Full name input on the user-access card was never persisted (no
column on portfolio_invitations), never used in the invitation email
(only the inviter's name appears there), and never displayed in the
pending-invitations table — purely dead weight on the form. Removing
the input, the request-body field, and the response-payload reference.

Extracts the POST body schema to a named inviteRequestSchema export so
the contract change is locked in by a unit test rather than left implicit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 11:13:59 +00:00
Khalim Conn-Kowlessar
3b63c5ea1a Confirm invitation accept with a dialog + refresh portfolios list
Replaces the auto-dismiss toast on accept with a shadcn Dialog that
names the portfolio and offers a "Go to {portfolioName}" CTA. Decline
keeps the existing toast. router.refresh() updates /home in place, and
revalidatePath("/home") in the API handler guarantees the server-rendered
portfolio list is fresh on any later navigation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 10:55:34 +00:00
Khalim Conn-Kowlessar
676ad5b107 Fix lint errors blocking the Vercel build
- Escape apostrophe in the revoke-invitation ConfirmDialog description
  (react/no-unescaped-entities)
- Add role="tab" to the two tab-button arrays in PropertyDetailDrawer
  and DealPage so aria-selected is valid for that element
  (jsx-a11y/role-supports-aria-props)

The aria-selected warnings were pre-existing in those files but the
build now blocks on warnings as well as errors. The fix is the correct
ARIA pattern — these buttons are real tabs, role="tab" is what
aria-selected expects.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 09:49:32 +00:00
Khalim Conn-Kowlessar
d70b15e705 Unify invitation flow: explicit accept/decline via profile-menu notifications
Replaces the auto-accept-on-signin behaviour with an explicit accept or
decline step. Every invitee (existing user or brand new) is now treated
the same way: an invitation lands in portfolio_invitations, the email
is a deep-link "Sign in to Ara", and the invitee accepts (or declines)
explicitly from a notifications panel hanging off their profile avatar.

Why: the previous flow silently added existing users to portfolios with
no way to refuse, and gave them no in-app confirmation that the invite
landed. Existing users complained they didn't know which account they
were signed in as either — both gaps are closed by the same panel.

Backend:
  - POST /collaborators no longer creates portfolioUsers directly for
    existing users; it writes a portfolio_invitations row in every case
    except the trivial "already a member of THIS portfolio" path
    (silent role update, no email)
  - New /api/user/invitations endpoint: GET lists pending invitations
    addressed to the current user across all portfolios, joined with
    portfolio + inviter context; POST accepts or declines a single
    invitation, scoped to session.email so users can't act on others'
  - Accept reuses the existing planInvitationApplication helper for
    the "already a member" idempotency check
  - Decline is a silent delete (matches GitHub/Linear/Notion convention;
    no email to inviter, no tombstone)
  - signIn callback no longer auto-applies pending invitations — that
    block is removed entirely along with its now-unused imports
  - Email template subject + body unified, no longer suggests the user
    is "added"; both modes say "invited" and direct them to the profile
    menu

Frontend:
  - ProfileDropDown rewritten as a notifications panel: shows the
    signed-in email at the top (closing the "which account?" gap),
    lists pending invitations with Accept/Decline buttons, displays a
    red count badge on the avatar (max "9+"). Uses TanStack Query with
    optimistic update on accept/decline and toast on outcome. Existing
    Help + Sign Out menu items preserved.
  - No useEffect — pending-count derived from query data, mutations
    handle the rest

Vercel preview test plan:
  - Invite a user already in another portfolio → red badge appears on
    their next page load; Accept lands them in the portfolio
  - Invite a new email → sign-up flow finishes; new account lands on
    home with a badge waiting for Accept (no longer auto-accepted)
  - Existing member of THIS portfolio re-invited → silent role update,
    no email

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 09:43:13 +00:00
Jun-te Kim
aecff4bdbf migration table 2026-05-27 19:19:39 +00:00
Khalim Conn-Kowlessar
f3887a215c Polish user-access UX: shadcn confirm dialog + toast feedback
Two small UX upgrades on the portfolio user-access page:

- Replace native confirm() prompts (for "Remove user" and "Revoke
  invitation") with a shadcn-based ConfirmDialog. Shows the email of
  the user/invitation being acted on, disables buttons while the
  mutation is in flight, and matches the visual language of the rest
  of the app. New ConfirmDialog component is generic and reusable.

- Toast on every mutation outcome (invite/remove/revoke, success and
  failure), using the existing useToast hook. Invite success now
  surfaces "Invitation sent — We've emailed <email>…" so admins get
  immediate feedback that the email was dispatched, instead of just
  the form silently clearing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 17:29:36 +00:00
Khalim Conn-Kowlessar
66cc71d228 Fix collaborators-not-iterable crash; rename misspelled colloborators route
Crash root cause: UsersPermissionsCard and CapabilitiesCard both used the
TanStack Query key ["portfolioUsers", id], but a previous change made
UsersPermissionsCard's fetcher return { users, currentUser } while
CapabilitiesCard's still returned an array. TanStack Query dedupes by
key — whichever component mounted first won the cache; the other read an
incompatible shape and crashed on `for (const c of collaborators)`.

Fix: extracted a single shared fetcher (collaboratorsClient.ts) so both
components import the same fetchCollaborators function and consume the
same response shape. CapabilitiesCard projects data?.users ?? []; the
shared cache stays consistent regardless of mount order.

Also rename the route folder from "colloborators" to "collaborators"
(spelling fix). Used git mv so history follows. Fetch URLs, console
error labels, and the route doc comment updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 17:12:00 +00:00
Khalim Conn-Kowlessar
171c586db1 Added database migration files 2026-05-27 16:54:58 +00:00
Khalim Conn-Kowlessar
7cde991871 Merge branch 'main' of https://github.com/Hestia-Homes/assessment-model into bug/portfolio-invitations 2026-05-27 16:53:36 +00:00
Jun-te Kim
27dcc218ce visual improvement on groups 2026-05-27 16:49:57 +00:00
Khalim Conn-Kowlessar
616302a5c7 Gate user-access page behind admin privilege; allow admin role assignment
Adds a portfolio-privilege concept (creator > admin > domna employee >
write > read > none) and gates all user-access mutations + the pending-
invitations view behind it. Plus opens the role dropdown to include
"admin" so creators/admins/Domna can promote and demote.

Privilege model:
  - Portfolio creator: full admin powers (cannot be removed/demoted)
  - Portfolio admin: full admin powers via explicit membership role
  - Domna employee (email ends @domna.homes, case-insensitive):
    implicit admin across all portfolios, even if not a member —
    intended for customer-support / internal-tooling needs
  - Anyone else (read/write/none): no admin powers

Backend:
  - New pure-function helpers in src/app/lib/portfolioAdmin.ts —
    isDomnaEmail() and canAdminister(privilege), with 6 tests covering
    case-insensitivity and look-alike domain rejection
  - New server helper resolvePortfolioPrivilege() that reads
    portfolioUsers + checks the email, returning the highest privilege
  - New denyIfNotAdmin(portfolioId, session) one-liner that returns a
    401/403 NextResponse or null; used at the top of every mutating
    route handler to keep the guard out of the way
  - POST/PUT/DELETE on /colloborators and GET/DELETE on /invitations
    are now all gated. Non-admin callers get 403.
  - GET /colloborators now requires auth and returns
    `{ users, currentUser: { privilege } }` so the UI knows which
    actions to expose without an extra round-trip

Frontend:
  - ROLE_OPTIONS extended to ["read", "write", "admin"]. RoleDropdown
    takes allowAdminPromotion?: boolean to keep the basic dropdown
    unchanged where promotion isn't allowed.
  - UsersPermissionsCard derives isAdmin = canAdminister(privilege)
    from the API response. Invite section, role-change dropdown,
    Remove button, and the entire Pending Invitations section are now
    rendered only when isAdmin. Non-admins see a read-only members
    table.
  - The invitations useQuery is disabled when !isAdmin, avoiding
    guaranteed-403 network calls.

Defensive note: the UI gating is for UX; the backend guard is the
security boundary. A non-admin who hand-crafts a POST still gets 403.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 16:40:34 +00:00
Jun-te Kim
7086a63c1e project db addition and live tracker update 2026-05-27 16:27:48 +00:00
Khalim Conn-Kowlessar
07acf4d93d Add pending-invitations admin UI and wire member removal
Adds two pieces of user-access management on the portfolio settings page
that were previously stubbed or missing:

- The existing "Remove" button was wired to console.log only. It now
  calls a DELETE on /colloborators with optimistic cache update and
  rollback on failure. The route refuses to remove the portfolio
  creator and 404s if the membership isn't in the URL's portfolio.

- A new "Pending invitations" section in UsersPermissionsCard lists
  invitees who haven't signed in yet, backed by a new
  /api/portfolio/[id]/invitations endpoint (GET + DELETE). Admins can
  revoke a pending invitation; revoking deletes the row so the invitee
  no longer auto-joins on sign-in. Inviting a new email shows up here
  immediately because the invite mutation invalidates both query keys.

Both new mutations use optimistic updates with rollback, and disable
only the in-flight row (mutation.variables === currentId) so the rest
of the table stays interactive. No useEffect, TanStack Query throughout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 16:27:41 +00:00