fix(ux): pass-3 — yacht/company headers, reminder filters wrap, client tab counts
Some checks failed
Build & Push Docker Images / lint (push) Successful in 1m14s
Build & Push Docker Images / build-and-push (push) Failing after 4m51s

Five small fixes from the third audit pass on previously-unchecked surfaces:

Yacht detail header (mobile):
  - Stack the action cluster (Edit / Transfer / Archive) below the title
    block on phone widths. Previously the three buttons crowded the right
    side enough to truncate the status pill to "A..." and force the owner
    name to wrap to two lines. Same fix that landed for berth / client /
    company headers.

Company detail header (mobile):
  - Same mobile stacking fix; legal-name + Tax-ID metadata no longer
    wraps awkwardly.

Company detail Incorporation Date (all viewports):
  - Strip the time portion of the ISO timestamp before passing to the
    inline editor. Previously rendered the raw "2019-03-14T00:00:00.000Z"
    Postgres-serialized form. Now reads "2019-03-14" and round-trips
    through the YYYY-MM-DD inline editor cleanly.

Reminders list filter row:
  - Allow flex-wrap on the My/All tabs + status filter + priority filter
    cluster. At 390px, the priority filter dropdown was being pushed off
    the right edge of the screen.

Client detail tab counts:
  - Add interestCount + noteCount to getClientById response, surface as
    badges on the Interests + Notes tabs. Brings them into parity with
    Yachts/Companies/Reservations/Addresses which already showed counts;
    Files + Activity are still stubs and don't get a count yet.

Verification: 0 tsc errors, 926/926 vitest passing, lint clean.

Out of scope (deferred):
  - Residential clients / interests pages still render plain HTML tables
    on phone widths (header columns clip at the right edge). Needs the
    DataView card-on-mobile treatment that the main /clients and
    /interests pages already have. Substantial separate work.
  - Phone contacts in the legacy seed have value set but valueE164 NULL,
    so InlinePhoneField shows "—" even though metadata is technically
    populated. Fix is a one-time backfill via libphonenumber-js, not a
    UI change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-05-03 17:09:27 +02:00
parent a792d9a182
commit b703684285
6 changed files with 35 additions and 4 deletions

View File

@@ -116,6 +116,8 @@ interface ClientTabsOptions {
tenureType: string; tenureType: string;
status: string; status: string;
}>; }>;
interestCount?: number;
noteCount?: number;
tags?: Array<{ id: string; name: string; color: string }>; tags?: Array<{ id: string; name: string; color: string }>;
}; };
} }
@@ -224,6 +226,7 @@ export function getClientTabs({ clientId, currentUserId, client }: ClientTabsOpt
{ {
id: 'interests', id: 'interests',
label: 'Interests', label: 'Interests',
badge: client.interestCount,
content: <ClientInterestsTab clientId={clientId} />, content: <ClientInterestsTab clientId={clientId} />,
}, },
{ {
@@ -261,6 +264,7 @@ export function getClientTabs({ clientId, currentUserId, client }: ClientTabsOpt
{ {
id: 'notes', id: 'notes',
label: 'Notes', label: 'Notes',
badge: client.noteCount,
content: <NotesList entityType="clients" entityId={clientId} currentUserId={currentUserId} />, content: <NotesList entityType="clients" entityId={clientId} currentUserId={currentUserId} />,
}, },
{ {

View File

@@ -77,7 +77,9 @@ export function CompanyDetailHeader({ company }: CompanyDetailHeaderProps) {
return ( return (
<> <>
<DetailHeaderStrip> <DetailHeaderStrip>
<div className="flex items-start gap-3 flex-wrap"> {/* Stack actions below the title block on phone widths; horizontal
beside it from sm up. */}
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:flex-wrap sm:gap-3">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<h1 className="hidden sm:block text-2xl font-bold text-foreground truncate"> <h1 className="hidden sm:block text-2xl font-bold text-foreground truncate">

View File

@@ -146,7 +146,11 @@ function OverviewTab({ companyId, company }: { companyId: string; company: Compa
</EditableRow> </EditableRow>
<EditableRow label="Incorporation Date"> <EditableRow label="Incorporation Date">
<InlineEditableField <InlineEditableField
value={company.incorporationDate} // The API returns this as an ISO timestamp ("2019-03-14T00:00:00.000Z")
// because Postgres `date` columns are serialized through JSON. Strip
// the time portion so the read-only state shows just YYYY-MM-DD,
// which is also the format the user types when editing.
value={company.incorporationDate ? company.incorporationDate.slice(0, 10) : null}
placeholder="YYYY-MM-DD" placeholder="YYYY-MM-DD"
onSave={save('incorporationDate')} onSave={save('incorporationDate')}
/> />

View File

@@ -249,7 +249,9 @@ export function ReminderList() {
} }
/> />
<div className="flex items-center gap-4 mb-4"> {/* Wrap on phone widths so the priority filter doesn't get pushed
off-screen by the My/All tabs + status filter taking the full row. */}
<div className="flex flex-wrap items-center gap-3 mb-4 sm:gap-4">
{canViewAll && ( {canViewAll && (
<Tabs value={viewMode} onValueChange={(v) => setViewMode(v as 'my' | 'all')}> <Tabs value={viewMode} onValueChange={(v) => setViewMode(v as 'my' | 'all')}>
<TabsList> <TabsList>

View File

@@ -142,7 +142,10 @@ export function YachtDetailHeader({ yacht }: YachtDetailHeaderProps) {
return ( return (
<> <>
<DetailHeaderStrip> <DetailHeaderStrip>
<div className="flex items-start gap-3 flex-wrap"> {/* Stacks vertically on phone widths so the action cluster doesn't
crush the status pill / owner row. From sm up, title block sits
beside actions in the original layout. */}
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:gap-3 sm:flex-wrap">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<h1 className="hidden sm:block text-2xl font-bold text-foreground truncate"> <h1 className="hidden sm:block text-2xl font-bold text-foreground truncate">

View File

@@ -4,6 +4,7 @@ import { db } from '@/lib/db';
import { import {
clients, clients,
clientContacts, clientContacts,
clientNotes,
clientRelationships, clientRelationships,
clientTags, clientTags,
clientAddresses, clientAddresses,
@@ -251,6 +252,19 @@ export async function getClientById(id: string, portId: string) {
const portalEnabled = await isPortalEnabledForPort(portId); const portalEnabled = await isPortalEnabledForPort(portId);
// Counts surfaced for tab badges (Interests + Notes — Yachts/Companies/etc
// get their counts from the corresponding row arrays we already fetched).
const [interestCountRow] = await db
.select({ count: count() })
.from(interests)
.where(
and(eq(interests.portId, portId), eq(interests.clientId, id), isNull(interests.archivedAt)),
);
const [noteCountRow] = await db
.select({ count: count() })
.from(clientNotes)
.where(eq(clientNotes.clientId, id));
return { return {
...client, ...client,
contacts, contacts,
@@ -259,6 +273,8 @@ export async function getClientById(id: string, portId: string) {
yachts: yachtRows, yachts: yachtRows,
companies: membershipRows, companies: membershipRows,
activeReservations, activeReservations,
interestCount: interestCountRow?.count ?? 0,
noteCount: noteCountRow?.count ?? 0,
clientPortalEnabled: portalEnabled, clientPortalEnabled: portalEnabled,
}; };
} }