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:
@@ -21,6 +21,16 @@ interface HubRootFile {
|
||||
filename: string;
|
||||
mimeType: string | null;
|
||||
createdAt: string;
|
||||
folderId: string | null;
|
||||
folderName: string | null;
|
||||
clientId: string | null;
|
||||
clientName: string | null;
|
||||
yachtId: string | null;
|
||||
yachtName: string | null;
|
||||
companyId: string | null;
|
||||
companyName: string | null;
|
||||
interestId: string | null;
|
||||
interestSummary: { stage: string; clientName: string | null } | null;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -94,23 +104,76 @@ export function HubRootView({ portSlug }: Props) {
|
||||
<div className="p-3 text-sm text-muted-foreground">No files yet.</div>
|
||||
) : (
|
||||
<ul className="divide-y">
|
||||
{filesData.map((f) => (
|
||||
<li key={f.id} className="flex items-center justify-between px-3 py-2 text-sm">
|
||||
<button
|
||||
type="button"
|
||||
className="truncate text-left hover:text-brand hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40 rounded-sm"
|
||||
onClick={() =>
|
||||
setPreviewFile({ id: f.id, name: f.filename, mimeType: f.mimeType })
|
||||
}
|
||||
aria-label={`Preview ${f.filename}`}
|
||||
{filesData.map((f) => {
|
||||
const entityBadge = (() => {
|
||||
if (f.interestId)
|
||||
return {
|
||||
label: f.interestSummary?.clientName
|
||||
? `Interest: ${f.interestSummary.clientName}`
|
||||
: 'Interest',
|
||||
href: `/${portSlug}/interests/${f.interestId}`,
|
||||
};
|
||||
if (f.clientId)
|
||||
return {
|
||||
label: f.clientName ?? 'Client',
|
||||
href: `/${portSlug}/clients/${f.clientId}`,
|
||||
};
|
||||
if (f.yachtId)
|
||||
return {
|
||||
label: f.yachtName ?? 'Yacht',
|
||||
href: `/${portSlug}/yachts/${f.yachtId}`,
|
||||
};
|
||||
if (f.companyId)
|
||||
return {
|
||||
label: f.companyName ?? 'Company',
|
||||
href: `/${portSlug}/companies/${f.companyId}`,
|
||||
};
|
||||
return null;
|
||||
})();
|
||||
return (
|
||||
<li
|
||||
key={f.id}
|
||||
className="flex items-center justify-between gap-3 px-3 py-2 text-sm"
|
||||
>
|
||||
{f.filename}
|
||||
</button>
|
||||
<span className="text-xs text-muted-foreground tabular-nums">
|
||||
{new Date(f.createdAt).toLocaleDateString(undefined)}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
<div className="min-w-0 flex-1">
|
||||
<button
|
||||
type="button"
|
||||
className="block truncate text-left hover:text-brand hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40 rounded-sm"
|
||||
onClick={() =>
|
||||
setPreviewFile({ id: f.id, name: f.filename, mimeType: f.mimeType })
|
||||
}
|
||||
aria-label={`Preview ${f.filename}`}
|
||||
>
|
||||
{f.filename}
|
||||
</button>
|
||||
<div className="mt-0.5 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||
{f.folderId && f.folderName ? (
|
||||
<Link
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
href={`/${portSlug}/documents?folderId=${f.folderId}` as any}
|
||||
className="inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
<span aria-hidden>📁</span>
|
||||
{f.folderName}
|
||||
</Link>
|
||||
) : null}
|
||||
{entityBadge ? (
|
||||
<Link
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
href={entityBadge.href as any}
|
||||
className="inline-flex items-center rounded-full border bg-muted px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground hover:bg-muted/70"
|
||||
>
|
||||
{entityBadge.label}
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground tabular-nums shrink-0">
|
||||
{new Date(f.createdAt).toLocaleDateString(undefined)}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user