feat(post-audit): Phase 4 polish + Phase 2 wiring + Phase 6 cron + CLAUDE.md

Three of the master plan's "suggested execution order" items shipped this
session; Phase 3b (EOI dialog overrides) deferred — estimate exceeded the
remaining session time.

- Phase 4 polish: yachtId field on <ReminderForm> via the existing
  YachtPicker, Ship-icon subtitle on <ReminderCard>, listReminders filter
  by yachtId, getReminder joins the yacht relation.
- Phase 2 risk-signal data wiring: getInterestById derives the 3 dates
  (dateDocumentDeclined / dateReservationCancelled / dateBerthSoldToOther)
  from document_events / berth_reservations / cross-interest interest_berths
  in parallel — chosen over new schema columns to keep the master plan's
  "no new tables" promise. Threaded through to DealPulseChip.
- Phase 6 cron + UI: src/jobs/processors/imap-bounce-poller.ts polls the
  configured IMAP mailbox (IMAP_* env), matches NDRs to recent
  document_sends rows via recipient + 7-day window, idempotent via
  bounceDetectedAt, fires email_bounced notifications on hard/soft
  (skips OOO). State persisted to system_settings.bounce_poller_state.
  Wired into maintenance queue at */15 * * * *. Admin /admin/sends page
  surfaces the bounce badge + reason inline.
- CLAUDE.md: trimmed 27KB → ~19.5KB (~28% smaller bytes). Prose-heavy
  Documenso webhook / v1-v2 routing / Document folders sections rewritten
  as scannable bullets. Added a new "Working in this repo — skills, MCPs,
  agents" section promoting brainstorming/TDD/debugging/frontend-design
  skills, Context7/Playwright/Serena MCPs, and the Explore/feature-dev
  agents. Documented Phase 2 derivation choice in the data-model section.

Quality gates: 1374/1374 vitest pass, tsc --noEmit clean, lint 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-18 15:38:37 +02:00
parent a6e79231f3
commit 503207ef68
13 changed files with 561 additions and 123 deletions

View File

@@ -8,6 +8,7 @@ import {
Clock,
FileText,
MoreHorizontal,
Ship,
User,
XCircle,
} from 'lucide-react';
@@ -35,6 +36,7 @@ interface Reminder {
clientId: string | null;
interestId: string | null;
berthId: string | null;
yachtId: string | null;
autoGenerated: boolean;
snoozedUntil: string | null;
completedAt: string | null;
@@ -42,6 +44,7 @@ interface Reminder {
client?: { id: string; fullName: string } | null;
interest?: { id: string; pipelineStage: string } | null;
berth?: { id: string; mooringNumber: string } | null;
yacht?: { id: string; name: string } | null;
}
const STATUS_CONFIG = {
@@ -111,6 +114,9 @@ export function ReminderCard({
} else if (reminder.berth) {
subtitleIcon = <Anchor className="h-3.5 w-3.5 shrink-0 text-muted-foreground/70" aria-hidden />;
subtitleText = `Berth ${reminder.berth.mooringNumber}`;
} else if (reminder.yacht) {
subtitleIcon = <Ship className="h-3.5 w-3.5 shrink-0 text-muted-foreground/70" aria-hidden />;
subtitleText = reminder.yacht.name;
} else if (reminder.interest) {
subtitleIcon = (
<FileText className="h-3.5 w-3.5 shrink-0 text-muted-foreground/70" aria-hidden />

View File

@@ -17,6 +17,7 @@ import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetFooter } from '@/com
import { ClientPicker } from '@/components/shared/client-picker';
import { InterestPicker } from '@/components/shared/interest-picker';
import { BerthPicker } from '@/components/shared/berth-picker';
import { YachtPicker } from '@/components/yachts/yacht-picker';
import { apiFetch } from '@/lib/api/client';
import { usePermissions } from '@/hooks/use-permissions';
@@ -50,11 +51,13 @@ interface ReminderFormProps {
clientId: string | null;
interestId: string | null;
berthId: string | null;
yachtId: string | null;
} | null;
// Pre-fill entity link when creating from entity detail pages
defaultClientId?: string;
defaultInterestId?: string;
defaultBerthId?: string;
defaultYachtId?: string;
onSuccess: () => void;
}
@@ -78,6 +81,7 @@ function ReminderFormBody({
defaultClientId,
defaultInterestId,
defaultBerthId,
defaultYachtId,
onSuccess,
}: ReminderFormProps) {
const isEdit = !!reminder;
@@ -106,6 +110,7 @@ function ReminderFormBody({
const [clientId, setClientId] = useState(reminder?.clientId ?? defaultClientId ?? '');
const [interestId, setInterestId] = useState(reminder?.interestId ?? defaultInterestId ?? '');
const [berthId, setBerthId] = useState(reminder?.berthId ?? defaultBerthId ?? '');
const [yachtId, setYachtId] = useState(reminder?.yachtId ?? defaultYachtId ?? '');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { can } = usePermissions();
@@ -133,6 +138,7 @@ function ReminderFormBody({
clientId: clientId || undefined,
interestId: interestId || undefined,
berthId: berthId || undefined,
yachtId: yachtId || undefined,
};
if (isEdit) {
@@ -249,7 +255,9 @@ function ReminderFormBody({
)}
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">Attach to client / deal / berth</Label>
<Label className="text-xs text-muted-foreground">
Attach to client / deal / berth / yacht
</Label>
<p className="text-[11px] text-muted-foreground">
Linking a reminder pins it onto that record so anyone who opens the page sees it on
the Reminders tab. Useful for &ldquo;chase this client for signed EOI&rdquo;,
@@ -281,6 +289,11 @@ function ReminderFormBody({
onChange={(id) => setBerthId(id ?? '')}
clientId={clientId || null}
/>
<YachtPicker
value={yachtId || null}
onChange={(id) => setYachtId(id ?? '')}
placeholder="Search yachts..."
/>
</div>
</div>

View File

@@ -39,6 +39,7 @@ interface Reminder {
clientId: string | null;
interestId: string | null;
berthId: string | null;
yachtId: string | null;
autoGenerated: boolean;
snoozedUntil: string | null;
completedAt: string | null;