chore(style): codebase em-dash sweep + minor layout polish
Some checks failed
Build & Push Docker Images / lint (push) Failing after 1m18s
Build & Push Docker Images / build-and-push (push) Has been skipped

Replaces every em-dash and en-dash with regular ASCII hyphens
across comments, JSX strings, and dev-facing logs. Mostly cosmetic
but stops the inconsistent mix that crept in over the last few
months (some files used em-dashes in comments, others didn't,
some used both).

Bundles two small dashboard-layout tweaks that touch a couple of
already-modified files:
- (dashboard)/layout.tsx main padding goes from p-6 to pt-3 px-6
  pb-6 so page content sits closer to the topbar.
- Sidebar now receives the ports list it needs for the footer
  port switcher.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-05-04 22:57:01 +02:00
parent d62822c284
commit 8699f81879
225 changed files with 844 additions and 845 deletions

View File

@@ -30,7 +30,7 @@ interface InlineStagePickerProps {
/**
* Click-to-change stage chip. Replaces the modal-based InterestStagePicker
* for inline editing user clicks the chip, picks a new stage from the
* for inline editing - user clicks the chip, picks a new stage from the
* popover (with optional reason), commits in one click. The popover stays
* compact: a small reason field above the stage list, and clicking any stage
* fires the mutation immediately.
@@ -140,7 +140,7 @@ export function InlineStagePicker({
isCurrent && 'font-medium',
)}
>
{/* Colored chip (mirrors the inline stage badge) turns
{/* Colored chip (mirrors the inline stage badge) - turns
the picker into a visual scan rather than just a list. */}
<span
className={cn('inline-flex h-5 w-3 shrink-0 rounded-sm', STAGE_DOT[s])}

View File

@@ -78,7 +78,7 @@ export function getInterestColumns({
className="truncate font-medium text-primary hover:underline"
onClick={(e) => e.stopPropagation()}
>
{row.original.clientName ?? ''}
{row.original.clientName ?? '-'}
</Link>
{notesCount > 0 ? (
<span
@@ -99,7 +99,7 @@ export function getInterestColumns({
header: 'Berth',
cell: ({ row }) => {
if (!row.original.berthId || !row.original.berthMooringNumber) {
return <span className="text-muted-foreground"></span>;
return <span className="text-muted-foreground">-</span>;
}
return (
<Link
@@ -150,7 +150,7 @@ export function getInterestColumns({
header: 'Category',
cell: ({ getValue }) => {
const cat = getValue() as string | null;
if (!cat) return <span className="text-muted-foreground"></span>;
if (!cat) return <span className="text-muted-foreground">-</span>;
return (
<Badge variant="outline" className="text-xs capitalize">
{CATEGORY_LABELS[cat] ?? cat}
@@ -164,7 +164,7 @@ export function getInterestColumns({
header: 'Source',
cell: ({ getValue }) => {
const source = getValue() as string | null;
if (!source) return <span className="text-muted-foreground"></span>;
if (!source) return <span className="text-muted-foreground">-</span>;
return (
<Badge variant="outline" className="text-xs">
{SOURCE_LABELS[source] ?? source}
@@ -178,7 +178,7 @@ export function getInterestColumns({
enableSorting: false,
cell: ({ row }) => {
const rowTags = row.original.tags ?? [];
if (rowTags.length === 0) return <span className="text-muted-foreground"></span>;
if (rowTags.length === 0) return <span className="text-muted-foreground">-</span>;
return (
<div className="flex flex-wrap gap-1">
{rowTags.slice(0, 3).map((tag) => (
@@ -203,7 +203,7 @@ export function getInterestColumns({
cell: ({ row }) => {
const lastIso = row.original.dateLastContact ?? row.original.updatedAt ?? null;
if (!lastIso) {
return <span className="text-muted-foreground text-sm"></span>;
return <span className="text-muted-foreground text-sm">-</span>;
}
const d = new Date(lastIso);
return (

View File

@@ -30,9 +30,9 @@ import { cn } from '@/lib/utils';
const OUTCOME_BADGE: Record<string, { label: string; className: string }> = {
won: { label: 'Won', className: 'bg-emerald-100 text-emerald-700' },
lost_other_marina: { label: 'Lost other marina', className: 'bg-rose-100 text-rose-700' },
lost_unqualified: { label: 'Lost unqualified', className: 'bg-rose-100 text-rose-700' },
lost_no_response: { label: 'Lost no response', className: 'bg-rose-100 text-rose-700' },
lost_other_marina: { label: 'Lost - other marina', className: 'bg-rose-100 text-rose-700' },
lost_unqualified: { label: 'Lost - unqualified', className: 'bg-rose-100 text-rose-700' },
lost_no_response: { label: 'Lost - no response', className: 'bg-rose-100 text-rose-700' },
cancelled: { label: 'Cancelled', className: 'bg-slate-200 text-slate-700' },
};
@@ -69,7 +69,7 @@ interface InterestDetailHeaderProps {
clientPrimaryPhone?: string | null;
clientPrimaryPhoneE164?: string | null;
/** Pending/snoozed reminders attached to this interest. Drives the
* alarm-bell badge on the header surfaces follow-ups so the rep
* alarm-bell badge on the header - surfaces follow-ups so the rep
* doesn't have to remember to check /reminders. */
activeReminderCount?: number;
berthId: string | null;
@@ -107,7 +107,7 @@ export function InterestDetailHeader({ portSlug, interest }: InterestDetailHeade
const outcomeBadge = resolveOutcomeBadge(interest.outcome);
const isClosed = !!interest.outcome;
// Contact deep-links resolved from the linked client's primary channels.
// Contact deep-links - resolved from the linked client's primary channels.
// wa.me requires the digits-only E.164 number (no leading "+"); fall back to
// stripping non-digits from the display value when the canonical form is
// missing.
@@ -258,7 +258,7 @@ export function InterestDetailHeader({ portSlug, interest }: InterestDetailHeade
</div>
)}
{/* Contact deep-links let the rep email / call / WhatsApp the
{/* Contact deep-links - let the rep email / call / WhatsApp the
client without leaving the interest workspace. Resolved from
the linked client's primary contact channels (server-side
fetch in getInterestById). */}
@@ -343,7 +343,7 @@ export function InterestDetailHeader({ portSlug, interest }: InterestDetailHeade
the won/lost meaning (green vs rose). Adding a "Won" /
"Lost" text label inline blew out the cluster width and
forced the Email/Call/WhatsApp action-chip row above to
stack vertically bad trade. From sm up, the full
stack vertically - bad trade. From sm up, the full
"Mark won" / "Close as lost" labels read clearly. */}
<button
type="button"

View File

@@ -36,7 +36,7 @@ export function InterestDocumentsTab({ interestId }: InterestDocumentsTabProps)
});
const prerequisites = {
// Required (EOI Section 2 top paragraph): name, address, email.
// Required (EOI Section 2 - top paragraph): name, address, email.
hasName: Boolean(interest?.clientName),
hasEmail: Boolean(interest?.clientPrimaryEmail),
hasAddress: Boolean(interest?.clientHasAddress),

View File

@@ -314,7 +314,7 @@ export function InterestForm({ open, onOpenChange, defaultClientId, interest }:
<p className="text-xs text-muted-foreground">
Required before the interest can leave the &quot;Open&quot; stage.
</p>
{/* TODO: also include company-owned yachts where client is a member requires autocomplete owner=any|company filter */}
{/* TODO: also include company-owned yachts where client is a member - requires autocomplete owner=any|company filter */}
{/* TODO: add "Add new yacht" inline shortcut (requires YachtForm integration) */}
</div>
</div>

View File

@@ -71,7 +71,7 @@ export function InterestList() {
const bulkArchiveMutation = useMutation({
mutationFn: async (ids: string[]) => {
// Concurrent fan-out small batches in practice (page size cap = 100).
// Concurrent fan-out - small batches in practice (page size cap = 100).
// If a single delete fails the others still run; the rejected one
// surfaces a toast via the standard apiFetch error path.
await Promise.all(ids.map((id) => apiFetch(`/api/v1/interests/${id}`, { method: 'DELETE' })));
@@ -194,7 +194,7 @@ export function InterestList() {
/>
)}
{/* Mobile FAB primary "New interest" affordance for the bottom-tab UX.
{/* Mobile FAB - primary "New interest" affordance for the bottom-tab UX.
Sits above the bottom nav (pb-safe-bottom + 70px tab height + 16px
gap). Hidden on lg+ where the header button already does the job. */}
<PermissionGate resource="interests" action="create">

View File

@@ -26,9 +26,9 @@ import { type InterestOutcome } from '@/lib/validators/interests';
const OUTCOME_LABELS: Record<InterestOutcome, string> = {
won: 'Won',
lost_other_marina: 'Lost went to another marina',
lost_unqualified: 'Lost unqualified',
lost_no_response: 'Lost no response',
lost_other_marina: 'Lost - went to another marina',
lost_unqualified: 'Lost - unqualified',
lost_no_response: 'Lost - no response',
cancelled: 'Cancelled',
};

View File

@@ -2,12 +2,7 @@
import { useQuery } from '@tanstack/react-query';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { useFeatureFlag } from '@/hooks/use-feature-flag';
import { apiFetch } from '@/lib/api/client';
import type { InterestScore } from '@/lib/services/interest-scoring.service';
@@ -15,9 +10,12 @@ import type { InterestScore } from '@/lib/services/interest-scoring.service';
// ─── Score tier helpers ───────────────────────────────────────────────────────
function getScoreTier(score: number): { label: string; className: string } {
if (score >= 80) return { label: 'Hot', className: 'bg-green-100 text-green-800 border-green-200' };
if (score >= 60) return { label: 'Warm', className: 'bg-yellow-100 text-yellow-800 border-yellow-200' };
if (score >= 40) return { label: 'Cool', className: 'bg-orange-100 text-orange-800 border-orange-200' };
if (score >= 80)
return { label: 'Hot', className: 'bg-green-100 text-green-800 border-green-200' };
if (score >= 60)
return { label: 'Warm', className: 'bg-yellow-100 text-yellow-800 border-yellow-200' };
if (score >= 40)
return { label: 'Cool', className: 'bg-orange-100 text-orange-800 border-orange-200' };
return { label: 'Cold', className: 'bg-gray-100 text-gray-700 border-gray-200' };
}
@@ -34,7 +32,7 @@ export function InterestScoreBadge({ interestId }: InterestScoreBadgeProps) {
queryKey: ['interest-score', interestId],
queryFn: () => apiFetch(`/api/v1/ai/interest-score?interestId=${interestId}`),
enabled: featureEnabled,
staleTime: 60 * 60 * 1000, // 1 hour mirrors server-side cache TTL
staleTime: 60 * 60 * 1000, // 1 hour - mirrors server-side cache TTL
});
if (!featureEnabled) return null;

View File

@@ -55,7 +55,7 @@ interface InterestTabsOptions {
reminderLastFired: string | null;
notes: string | null;
/** Surfaced by getInterestById for the Overview "most recent note"
* teaser saves a click into the Notes tab to peek at the latest. */
* teaser - saves a click into the Notes tab to peek at the latest. */
notesCount?: number;
recentNote?: {
id: string;
@@ -145,7 +145,7 @@ interface MilestoneSectionProps {
onAdvance: (stage: string) => void;
isPending: boolean;
/** Current pipelineStage. Used to mark steps as done when the pipeline has
* moved past their advanceStage even if the date stamp is missing e.g.
* moved past their advanceStage even if the date stamp is missing - e.g.
* a seed-data interest that started already at eoi_signed will show both
* EOI sub-steps as done. Stage truth > date truth. */
currentStage: string;
@@ -158,7 +158,7 @@ interface MilestoneSectionProps {
}
/**
* One milestone section (EOI / Deposit / Contract) shows a vertical lifecycle
* One milestone section (EOI / Deposit / Contract) - shows a vertical lifecycle
* with completed steps checked, the next step exposing a quick "mark as…"
* button that bumps the pipeline stage. Each stage flip auto-stamps its date
* via the service layer (interests.service.ts). When external systems wire in
@@ -308,7 +308,7 @@ function OverviewTab({
return (
<div className="space-y-6">
{/* Sales-process milestones the heart of the system. Each section is a
{/* Sales-process milestones - the heart of the system. Each section is a
mini lifecycle that auto-completes as actions happen on the platform
(Documenso webhook, paid deposit invoice, signed contract). Until the
automation lands, salespeople nudge stages forward via the inline
@@ -420,7 +420,7 @@ function OverviewTab({
</dl>
</div>
{/* Contact dates (read-only kept compact next to Lead) */}
{/* Contact dates (read-only - kept compact next to Lead) */}
<div className="space-y-1">
<h3 className="text-sm font-medium mb-2">Contact</h3>
<dl>
@@ -484,7 +484,7 @@ function OverviewTab({
variant="textarea"
value={interest.notes}
onSave={save('notes')}
emptyText="No notes click to add"
emptyText="No notes - click to add"
/>
</div>

View File

@@ -2,7 +2,7 @@
* Sales-triage urgency badges for interest list rows + cards.
*
* Derived purely from the dates we already return on the row, so this is a
* pure function no DB hits, no extra fetch. Mirrors the logic the
* pure function - no DB hits, no extra fetch. Mirrors the logic the
* server-side alert-rules engine uses, but for at-a-glance rendering on
* the list itself.
*/
@@ -47,7 +47,7 @@ export function computeUrgencyBadges(row: InterestUrgencyInput): UrgencyBadge[]
const badges: UrgencyBadge[] = [];
// Silent in mid-funnel stages most actionable.
// Silent in mid-funnel stages - most actionable.
if (ACTIVE_MID_FUNNEL_STAGES.has(row.pipelineStage)) {
const lastTouchIso = row.dateLastContact ?? row.updatedAt ?? null;
const days = daysSince(lastTouchIso);