Commit graph

256 commits

Author SHA1 Message Date
KhalimCK
c861d4a0b0
Merge pull request #353 from Hestia-Homes/feature/portfolio-scenarios
feat(scenarios): portfolio Scenarios tab — gallery, detail, and creation journey
2026-07-06 16:21:21 +01:00
Khalim Conn-Kowlessar
8a0ad0c0dc feat(postcode-search): routes, add-properties page, PropertyTable entry point
Completes the add-properties-by-postcode journey (ADR-0007) on top of the
TDD'd domain core:

- GET /api/portfolio/[id]/properties/search?postcode= — postcodes.io for
  validation + geography (admin_district, parliamentary_constituency) every
  time; OS Places through the 30-day postcode_search read-through cache
  (refresh=1 bypasses it). Returns candidates with alreadyInPortfolio flags
  from one grouped uprn query. Cache reads accept legacy space-less keys;
  writes use the canonical spaced form.
- POST /api/portfolio/[id]/properties — re-derives facts server-side from
  classification codes (client propertyType never trusted), then one
  transaction: property rows via uq_property_portfolio_uprn ON CONFLICT DO
  NOTHING, vocabulary upserts with os_places provenance (DO NOTHING preserves
  user rows; snapshots mirror the resolved value), property_overrides
  snapshots (building_part 0, original description = OS code), and
  property_details_spatial per-property coordinates. Returns {added, skipped}.
- OS Places fetches now request output_srs=EPSG:4326 so records carry
  LAT/LNG (no mapper read the BNG X/Y coordinates).
- /portfolio/[slug]/properties/add built to the approved mockup: search card
  with cache-age line, per-postcode result cards, cross-postcode selection
  tray, done state. TanStack Query v4, no useEffect/useMemo.
- "Add properties" dropdown beside Export in PropertyTable (search by
  postcode + bulk excel import "Soon"); toolbar AddNew dropdown retired.
- New display/cache helpers (describeCandidate, postcodeCacheKeys,
  cacheAgeDays) TDD'd in the domain core.

Verified live against portfolio 814: search (cache + refresh), submit
(added 2, all four write layers checked in DB), dedup re-submit (skipped 2),
alreadyInPortfolio flags, page + dropdown render.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 14:33:22 +00:00
Khalim Conn-Kowlessar
639cee6c4c feat(scenarios): portfolio Scenarios tab — gallery, detail, creation journey
Users could only create scenarios buried inside the CSV-upload modal, at
trigger time. This adds a first-class Scenarios tab: a gallery of the
portfolio's scenarios, a read-only detail page, and a 3-step creation
journey (goal → measures → review) that saves app-authored scenario rows
per ADR-0003.

Domain rules (TDD'd in src/lib/scenarios/model.test.ts, 19 tests):
- goal semantics: "Increasing EPC" requires a target band; all other
  goals are maximise-within-budget with goal_value forced null
- exclusions-only measure model: normalised (dedupe+sort), unknown keys
  rejected, all-excluded rejected, empty set valid; serialised to the
  backend's brace text format (parses the legacy space-padded variant)
- insert mapping: multi_plan=true, is_default=false, budget per property
- exact-config duplicate detection ignoring name (review-step warning)
- status derived from plan existence — no stored status column

Queries follow the ADR's discipline: gallery status = one grouped
COUNT(DISTINCT property_id) per portfolio; detail = one scoped count
(index-only, powers the blocked-delete message); delete = single atomic
DELETE guarded on NOT EXISTS(plans). Rename is the only other mutation —
configuration is immutable.

Verified live on portfolio 814: create (400 on missing band, 201 on CO₂
draft), derived Awaiting/Modelled in the gallery, rename, delete of the
draft (200) and blocked delete of the modelled scenario (409, "338
properties…" matching the portfolio's true count).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 08:34:47 +00:00
Khalim Conn-Kowlessar
59297c3495 Merge remote-tracking branch 'origin/main' into feature/update-ui-for-new-modelling 2026-07-01 15:34:29 +00:00
Daniel Roth
54cf807f2b correct comment following previous commit 2026-06-30 11:10:19 +00:00
Daniel Roth
7c9faba215 sort dupe files based on upload timestamp rather than id 2026-06-30 11:06:58 +00:00
Daniel Roth
ac6983ae68 only get most recent version of each uploaded file per s3 key 2026-06-30 11:02:49 +00:00
Khalim Conn-Kowlessar
2f0f937fb6 fix(reporting): read scenario measures via denormalised recommendation.plan_id
The scenario measures modal came up empty because both measures routes
gated on EXISTS against plan_recommendations — a retired join table with
no rows for new-approach plans (25.7M legacy rows, none for e.g.
portfolio 812's plans), so the query returned zero measures.

Read the denormalised model instead: drive from the latest plan per
property (default or scenario) and JOIN recommendation by the indexed
property_id, scoped to the plan via recommendation.plan_id. Portfolio 812
default now returns 5 measures (solar_pv 54 homes/£266k, …) where it
returned 0. Also removes the stale commented query block that referenced
the retired plan_recommendations / recommendation_materials tables.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 11:42:19 +00:00
Khalim Conn-Kowlessar
7de48448c0 fix(reporting): temp guards for sub-baseline scenario post_sap
The modelling writes a target-level post_sap (e.g. ~C) even for homes
already above the scenario target, so plans can carry cost while modelling
post_sap BELOW the effective baseline. That skewed three reporting
surfaces. Three TEMP (demo) guards, all keyed on the effective baseline
(ADR-0002); revert once the Model team fixes the sub-baseline plans:

1. EPC band chart: post-scenario SAP clamped to GREATEST(baseline, post)
   so already-compliant properties aren't shown "improving" down a band
   (portfolio 796: EPC B 4,244 -> 1,660 wrongly, now 4,479).
2. n_units_upgraded + cost: exclude plans whose post_sap is below the
   effective baseline (not real upgrades) -- 796: 10,283 -> 9,765, -£1.28M.
3. total_sap_uplift / £-per-SAP: baseline is the effective SAP, not the
   null-for-new-approach current_sap_points, and counts genuine gains
   only -- uplift 0 -> 89,724, so £/SAP £0 -> £536.

Also fixes the no-plan branch to use the effective baseline instead of
the null current_sap_points (Unknown-band leakage).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 07:44:22 +00:00
Khalim Conn-Kowlessar
fec714a406 fix(property-meta): resolve in-process, drop self-fetch to process.env.URL
getPropertyMeta self-fetched process.env.URL + /api/property-meta. That URL can
resolve to a different deployment (e.g. production), so the building-passport
pages were reading property meta from OLD code — the backfilled headline SAP/EPC
never arrived and the current rating rendered blank, regardless of caching.

Extract resolvePropertyMeta() and call it directly from both the route and
getPropertyMeta, so the page runs identical in-process logic. JSON round-trip
through serializeBigInt preserves the previous shape (bigints/dates as strings).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:40:17 +00:00
Khalim Conn-Kowlessar
5d87481b6b fix(plans): source headline SAP/EPC from baseline for new properties
The plans page computed the plan uplift as postSapPoints - currentSapPoints,
but current_sap_points is unwritten on the property row for new-approach
properties (null -> 0), so the uplift showed the full post score (e.g. +71.39)
and no current rating.

property-meta now backfills currentSapPoints / currentEpcRating from
property_baseline_performance (lodged_sap_score / lodged_epc_band) for
new-approach properties, fixing every propertyMeta consumer (plans page, hero
cards, building passport). e.g. 709634: current 60 (D), uplift now +11.39.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:20:49 +00:00
Khalim Conn-Kowlessar
6026944fe5 feat(epc): read new EPC graph for new-approach properties via resolver
The new modelling pipeline no longer writes property_details_epc. Route all
EPC reads through src/lib/services/epcSources.ts, which branches on the cutoff
(property.updated_at >= 2026-06-01): legacy rows keep reading
property_details_epc; new-approach rows read epc_property (+ children),
property_baseline_performance, and plan/recommendation.

- per-property: resolveConditionReport (building passport),
  resolveDetailsEpcMeta (/api/property-meta)
- aggregates: shared carbon/bills/energy/estimated/expired/floor-area/
  lodgement/mainfuel SQL fragments + newApproachJoins, wired into the
  portfolio list + filters, reporting metrics, and both scenario metrics routes

estimated = predicted-only EPC. Gracefully omits gap fields for new
properties (mainfuel, energyTariff, floorHeight, installed-measures bill
adjustment, SAP-05 likely-downgrade).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 00:40:37 +00:00
Jun-te Kim
30d3e727d1 route of the address2uprn problem of float value 2026-06-08 09:38:48 +00:00
Jun-te Kim
f6545c2fa0 property override 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
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
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
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
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
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
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
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
Khalim Conn-Kowlessar
7e9193313b added missing email and email test files 2026-05-27 16:19:14 +00:00
Khalim Conn-Kowlessar
c921db7d9c initial implementation for portfolio invitations. A user can send an invitation to a user and they will receive an invitation email 2026-05-27 16:18:21 +00:00
Khalim Conn-Kowlessar
d042606955 Add 6-digit code sign-in as primary, magic link as fast-path fallback
Same email now contains a 6-digit code and a magic link, both backed by a
single verificationToken row. After submitting their email, the user
lands on /auth/verify-code with a single-input form (inputmode=numeric,
autocomplete=one-time-code, auto-submit on 6 digits or paste) and can
either type the code or use the link from the email. Either path
consumes the same row — single-use, replace-on-resend.

This is the structural fix for the silent-quarantine pattern observed
with Atkins and Sustainable Building UK: corporate gateways are happier
with short transactional content than long opaque token URLs, and a
code can't be broken by SafeLinks-style URL rewriting. The link path is
preserved so users whose email gets through unmangled keep one-click UX.

Security:
  - Codes are 6-digit, crypto.randomInt-generated, stored as sha256
    hashed against NEXTAUTH_SECRET on the same row as the link token
  - 5-attempt lockout per code (attempts column); 6th attempt with the
    correct code still fails
  - Per-email send rate limit: 5/hour fixed window (authRateLimits
    table); 6th send returns an error
  - Code + link share a 10-minute window (maxAge dropped from 1h)
  - Resending replaces any prior token rows for the identifier so only
    the latest send is ever live

Implementation:
  - verificationCode.ts holds generateCode + hashCode + the pure
    evaluateCodeAttempt decision tree; 9 unit tests cover every branch
    of the verification outcome (no-such-row, expired, locked-out, ok,
    wrong-with-newAttempts, locked-out-still-rejects-correct-code)
  - sendVerificationRequest now hashes the URL token the same way
    /verify/[token]/page.tsx does, applies the rate limit + records the
    code + replaces older rows in two transactions
  - CredentialsProvider (id: "email-code") calls evaluateCodeAttempt
    inside a transaction, handles all 5 outcomes, creates the user on
    first successful code (parity with the magic-link callback path)
  - oauthId backfill in the signIn callback is now guarded on
    account.type === "oauth" so the credentials flow doesn't pollute
    oauthProvider with "email-code"
  - Migration is additive: code_hash nullable, attempts default 0; new
    authRateLimits table is independent. In-flight tokens at deploy time
    keep working via the link path.

Vercel preview deployment is the test surface; a Mailpit + Cypress E2E
loop is intentionally deferred per the lean-setup plan in docs/wip/
auth-email-code-fallback-plan.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:16:47 +00:00
Khalim Conn-Kowlessar
9c569f5584 Add SES observability foundation for email auth (PR 1 of code-fallback)
Wires the X-SES-CONFIGURATION-SET header on outbound auth emails so SES
bounce/delivery events flow through dev-ses-config to the dev-ses-events
SNS topic. Replaces the fire-and-forget "EMAIL MAGIC LINK SENT" log
(which fired before the SMTP transaction and swallowed downstream errors)
with structured EMAIL_MAGIC_LINK_SUCCESS/_FAILURE logs carrying the
Nodemailer messageId, so app-side sends are now correlatable with SES
events.

Motivated by the Atkins / Sustainable Building UK silent-quarantine
incidents where we couldn't tell whether SES had even tried to send.

Plan doc at docs/wip/auth-email-code-fallback-plan.md tracks the
broader email-code-fallback design that PR 2 will implement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 13:15:25 +00:00
KhalimCK
20f6aff62e
Merge pull request #266 from Hestia-Homes/feature/pm-ui-ux
Some checks failed
Test Suite / unit-tests (push) Has been cancelled
Feature/pm UI ux
2026-05-18 10:00:40 +01:00
Jun-te Kim
604e0014fc deploy to main 2026-05-15 11:34:14 +00:00
Jun-te Kim
41891a4540 job ordering 2026-05-15 10:13:09 +00:00
Jun-te Kim
cd47aef985 order by job started and not job updated time 2026-05-15 10:10:00 +00:00
Khalim Conn-Kowlessar
d467a61c22 Adding bulk approve and instruct 2026-05-09 08:39:20 +00:00
Khalim Conn-Kowlessar
5eced11db1 reverse order of activity log 2026-05-07 21:09:28 +00:00
Khalim Conn-Kowlessar
5f0617b691 adding multi organisation connect 2026-05-07 12:32:20 +00:00
Khalim Conn-Kowlessar
a046ed4a5c working on pibi ui 2026-05-06 23:04:45 +00:00
Khalim Conn-Kowlessar
19139b6253 Updated survey request UI for Devon County Council 2026-05-06 20:37:30 +00:00
Khalim Conn-Kowlessar
2b05abb185 Adding tests for modified approvals route 2026-05-06 19:10:47 +00:00
Khalim Conn-Kowlessar
fccf4130c8 added instruct measures approval 2026-05-06 18:00:06 +00:00
Jun-te Kim
7ece33b7b6 removed finalise code as its needed only in the final new process 2026-05-06 16:05:24 +00:00
Jun-te Kim
4f43d32309 Merge remote-tracking branch 'origin' into feature/onbarding_of_addresses 2026-05-06 15:50:35 +00:00
Jun-te Kim
8170601efb refactored with improve-code-archievture and grill-with-docs 2026-05-06 15:49:34 +00:00
Khalim Conn-Kowlessar
bf85787936 re-designing UI for PM' 2026-05-06 14:23:54 +00:00
Khalim Conn-Kowlessar
4734eeed07 updating live tracking new features UI 2026-05-05 20:43:19 +00:00
Khalim Conn-Kowlessar
1e5eb09c3e add pibi-measures route
Adds POST /api/portfolio/[portfolioId]/pibi-measures (approver-gated)
that accepts { dealId, measureNames[] } and delegates to
selectPibiMeasures. Also adds a GET handler that returns the current
pibi_ordered selection, approved measures, and instructed measures for
a deal — used by the drawer's PIBI selector to pre-populate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 19:14:20 +00:00
Khalim Conn-Kowlessar
7de92cf5ea add instruct-measure service, route and soft-warning helper
Approver-only flow that records an instructed measure in
user_defined_deal_measures, auto-creates a deal_measure_approvals row +
event in a single tx, then pushes the instructed list to HubSpot under
instructed_measures. When the deal has no proposed measures and no prior
approvals, also pushes the new measure as proposed_measures so the deal
has a coherent starting point. Soft-warning helper surfaces the
out-of-order case for the drawer (and later slices).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 18:56:48 +00:00
Khalim Conn-Kowlessar
e020b3fd83 add halted state fields and approver capability gate
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 18:24:43 +00:00