feat(uat-p5): long-tail polish - tag chips, notes counts, hub context, tenancies toggle

- StageStepper renders now carry tag chips next to the progress bar
  (client interest cards, pipeline summary, preview sheet).
- Notes tab badge on the interest detail aggregates note counts across
  the interest, the linked client, the linked yacht, and any companies
  the client is an active member of - reps see the full surface area
  at a glance.
- Admin Settings: Tenancies Module toggle wired into the Feature Flags
  card. Disabling hides nav/tabs without deleting any rows; re-enabling
  brings them back. Service layer was already complete; this surfaces
  the control on the operations page.
- HubRoot recent-files rows now show folder breadcrumb + entity badge
  (Interest/Client/Yacht/Company) so reps can tell at a glance where a
  file lives. Backed by listFiles enrichment (5 batched lookups per
  page; no per-row queries).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 21:57:20 +02:00
parent 2592e28578
commit 6caf41651f
7 changed files with 270 additions and 20 deletions

View File

@@ -3,12 +3,12 @@ import { and, desc, eq, exists, gte, inArray, isNull, ne, sql } from 'drizzle-or
import { db } from '@/lib/db';
import { interests, interestBerths, interestTags, interestNotes } from '@/lib/db/schema/interests';
import { reminders, interestContactLog } from '@/lib/db/schema/operations';
import { clients, clientAddresses, clientContacts } from '@/lib/db/schema/clients';
import { clients, clientAddresses, clientContacts, clientNotes } from '@/lib/db/schema/clients';
import { berths } from '@/lib/db/schema/berths';
import { documents, documentEvents } from '@/lib/db/schema/documents';
import { berthTenancies } from '@/lib/db/schema/tenancies';
import { yachts } from '@/lib/db/schema/yachts';
import { companyMemberships } from '@/lib/db/schema/companies';
import { yachts, yachtNotes } from '@/lib/db/schema/yachts';
import { companyMemberships, companyNotes } from '@/lib/db/schema/companies';
import { tags } from '@/lib/db/schema/system';
import { userProfiles, userPortRoles, roles } from '@/lib/db/schema/users';
import { createAuditLog, type AuditMeta } from '@/lib/audit';
@@ -583,6 +583,48 @@ export async function getInterestById(id: string, portId: string) {
.from(interestNotes)
.where(eq(interestNotes.interestId, id));
// Aggregated note count = direct interest notes + notes attached to
// the linked client, yacht (if any), and any companies the linked
// client is an active member of. Surfaces as a separate field so the
// existing notesCount badge stays accurate to "this interest only",
// while the Notes tab can render the broader total when reps want
// the full picture at a glance.
const clientNotesP = db
.select({ count: sql<number>`count(*)::int` })
.from(clientNotes)
.where(eq(clientNotes.clientId, interest.clientId));
const yachtNotesP = interest.yachtId
? db
.select({ count: sql<number>`count(*)::int` })
.from(yachtNotes)
.where(eq(yachtNotes.yachtId, interest.yachtId))
: Promise.resolve([{ count: 0 }]);
const companyMembershipsP = db
.select({ companyId: companyMemberships.companyId })
.from(companyMemberships)
.where(
and(eq(companyMemberships.clientId, interest.clientId), isNull(companyMemberships.endDate)),
);
const [clientNotesRow, yachtNotesRow, memberships] = await Promise.all([
clientNotesP,
yachtNotesP,
companyMembershipsP,
]);
const companyIds = memberships.map((m) => m.companyId);
let companyNotesCount = 0;
if (companyIds.length > 0) {
const [row] = await db
.select({ count: sql<number>`count(*)::int` })
.from(companyNotes)
.where(inArray(companyNotes.companyId, companyIds));
companyNotesCount = row?.count ?? 0;
}
const notesCountAggregated =
notesCount +
(clientNotesRow[0]?.count ?? 0) +
(yachtNotesRow[0]?.count ?? 0) +
companyNotesCount;
// Active reminder count for the interest's bell badge. Counts reminders
// directly linked via interestId - `pending` and `snoozed` only;
// completed/dismissed don't surface.
@@ -734,6 +776,7 @@ export async function getInterestById(id: string, portId: string) {
linkedBerthCount,
tags: tagRows,
notesCount,
notesCountAggregated,
recentNote: recentNote ?? null,
activeReminderCount,
assignedToName,