chore(style): codebase em-dash sweep + minor layout polish
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:
@@ -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])}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 "Open" 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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user