Files
pn-new-crm/src/components/reminders/reminder-list.tsx

421 lines
14 KiB
TypeScript
Raw Normal View History

'use client';
import { useState, useEffect, useCallback } from 'react';
import { type ColumnDef } from '@tanstack/react-table';
feat(ui): broad consistency sweep — sources, dates, comboboxes, milestones Mobile + responsive - berth-form full-width on phones (was 480px fixed → overflowed iPhone) - currency-input switched to inputMode=decimal with live thousands separator - client-form Country/Timezone/Source/Preferred-Contact full-width <sm - contacts row restructured so Primary toggle + Remove get their own strip - customize-dashboard footer stacks vertically on mobile; Done full-width - interest-form client/berth pickers no longer cmdk-filter on UUID (typing "Carlos" now returns Carlos Vega instead of "No clients found") Data + consistency - SOURCES + SOURCE_LABELS + formatSource() in lib/constants; 9 surfaces now resolve interest/client source from one place - INTEREST_OUTCOMES adds lost_other (picker, badge, timeline) - Berth options natural-sort A1 → A2 → … → A10 via lib/utils/mooring-sort - archiver downgraded ^8 → ^7.0.1 so the GDPR export route compiles - TableBody last-row uses border-b-0 (not border-0); colored left-accent on the bottom berth row now renders - Hide Invite-to-Portal until port setting === true (was !== false default-show) - OwnerPicker primer query resolves entity name on first paint (no more UUID flash before the popover opens) Terminology - Replaced user-facing "Documenso" with "signing service" / "Generated EOI" / "Manual EOI" in 8 components (admin/internal references kept) - Plainer status-change copy on berth-detail-header Forms + editing - InlineEditableField gained a `date` variant (native picker); applied to company incorporation date and ready for other YYYY-MM-DD plaintext fields - Inline source picker on interest-tabs detail (was free text) - TagPicker self-hides when port has no tags AND nothing is selected - New ReminderDaysInput with preset chips (1d / 3d / 1wk / 2wk / 1mo / custom) - Compose dialog follow-up is now a toggle that reveals datetime picker Pipeline milestones - changeStageSchema accepts optional milestoneDate; service stamps it on the matching date column instead of always using now - MilestoneAdvanceButton popover collects a back-date before stage advance - Applied to every "Mark X manually" surface on the interest overview EOI / linked-berths polish - Add-bypass row aligned inline with toggle descriptions - Tooltips on "Specifically pitching" / "Mark in EOI bundle" explain their legal vs. public-map consequences Surfaces - Companies list now has the column picker + persisted hidden-column prefs - NotesList aggregate flag enabled on clients, companies, residential_clients (yachts already aggregated) ft/m unit toggle (interim, before drift fix) - "Berth size desired" gets a section-level ft/m toggle; per-field hint shows the converted value. Storage stays canonical-ft for now; the drift-safe persistence migration is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:50:58 +02:00
import { Plus, CheckCircle2, Clock, Pencil, XCircle, AlertTriangle, Bell } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
import { useParams } from 'next/navigation';
import { DataTable } from '@/components/shared/data-table';
import { PageHeader } from '@/components/shared/page-header';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
feat(ui): broad consistency sweep — sources, dates, comboboxes, milestones Mobile + responsive - berth-form full-width on phones (was 480px fixed → overflowed iPhone) - currency-input switched to inputMode=decimal with live thousands separator - client-form Country/Timezone/Source/Preferred-Contact full-width <sm - contacts row restructured so Primary toggle + Remove get their own strip - customize-dashboard footer stacks vertically on mobile; Done full-width - interest-form client/berth pickers no longer cmdk-filter on UUID (typing "Carlos" now returns Carlos Vega instead of "No clients found") Data + consistency - SOURCES + SOURCE_LABELS + formatSource() in lib/constants; 9 surfaces now resolve interest/client source from one place - INTEREST_OUTCOMES adds lost_other (picker, badge, timeline) - Berth options natural-sort A1 → A2 → … → A10 via lib/utils/mooring-sort - archiver downgraded ^8 → ^7.0.1 so the GDPR export route compiles - TableBody last-row uses border-b-0 (not border-0); colored left-accent on the bottom berth row now renders - Hide Invite-to-Portal until port setting === true (was !== false default-show) - OwnerPicker primer query resolves entity name on first paint (no more UUID flash before the popover opens) Terminology - Replaced user-facing "Documenso" with "signing service" / "Generated EOI" / "Manual EOI" in 8 components (admin/internal references kept) - Plainer status-change copy on berth-detail-header Forms + editing - InlineEditableField gained a `date` variant (native picker); applied to company incorporation date and ready for other YYYY-MM-DD plaintext fields - Inline source picker on interest-tabs detail (was free text) - TagPicker self-hides when port has no tags AND nothing is selected - New ReminderDaysInput with preset chips (1d / 3d / 1wk / 2wk / 1mo / custom) - Compose dialog follow-up is now a toggle that reveals datetime picker Pipeline milestones - changeStageSchema accepts optional milestoneDate; service stamps it on the matching date column instead of always using now - MilestoneAdvanceButton popover collects a back-date before stage advance - Applied to every "Mark X manually" surface on the interest overview EOI / linked-berths polish - Add-bypass row aligned inline with toggle descriptions - Tooltips on "Specifically pitching" / "Mark in EOI bundle" explain their legal vs. public-map consequences Surfaces - Companies list now has the column picker + persisted hidden-column prefs - NotesList aggregate flag enabled on clients, companies, residential_clients (yachts already aggregated) ft/m unit toggle (interim, before drift fix) - "Berth size desired" gets a section-level ft/m toggle; per-field hint shows the converted value. Storage stays canonical-ft for now; the drift-safe persistence migration is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:50:58 +02:00
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { apiFetch } from '@/lib/api/client';
feat(ui): broad consistency sweep — sources, dates, comboboxes, milestones Mobile + responsive - berth-form full-width on phones (was 480px fixed → overflowed iPhone) - currency-input switched to inputMode=decimal with live thousands separator - client-form Country/Timezone/Source/Preferred-Contact full-width <sm - contacts row restructured so Primary toggle + Remove get their own strip - customize-dashboard footer stacks vertically on mobile; Done full-width - interest-form client/berth pickers no longer cmdk-filter on UUID (typing "Carlos" now returns Carlos Vega instead of "No clients found") Data + consistency - SOURCES + SOURCE_LABELS + formatSource() in lib/constants; 9 surfaces now resolve interest/client source from one place - INTEREST_OUTCOMES adds lost_other (picker, badge, timeline) - Berth options natural-sort A1 → A2 → … → A10 via lib/utils/mooring-sort - archiver downgraded ^8 → ^7.0.1 so the GDPR export route compiles - TableBody last-row uses border-b-0 (not border-0); colored left-accent on the bottom berth row now renders - Hide Invite-to-Portal until port setting === true (was !== false default-show) - OwnerPicker primer query resolves entity name on first paint (no more UUID flash before the popover opens) Terminology - Replaced user-facing "Documenso" with "signing service" / "Generated EOI" / "Manual EOI" in 8 components (admin/internal references kept) - Plainer status-change copy on berth-detail-header Forms + editing - InlineEditableField gained a `date` variant (native picker); applied to company incorporation date and ready for other YYYY-MM-DD plaintext fields - Inline source picker on interest-tabs detail (was free text) - TagPicker self-hides when port has no tags AND nothing is selected - New ReminderDaysInput with preset chips (1d / 3d / 1wk / 2wk / 1mo / custom) - Compose dialog follow-up is now a toggle that reveals datetime picker Pipeline milestones - changeStageSchema accepts optional milestoneDate; service stamps it on the matching date column instead of always using now - MilestoneAdvanceButton popover collects a back-date before stage advance - Applied to every "Mark X manually" surface on the interest overview EOI / linked-berths polish - Add-bypass row aligned inline with toggle descriptions - Tooltips on "Specifically pitching" / "Mark in EOI bundle" explain their legal vs. public-map consequences Surfaces - Companies list now has the column picker + persisted hidden-column prefs - NotesList aggregate flag enabled on clients, companies, residential_clients (yachts already aggregated) ft/m unit toggle (interim, before drift fix) - "Berth size desired" gets a section-level ft/m toggle; per-field hint shows the converted value. Storage stays canonical-ft for now; the drift-safe persistence migration is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:50:58 +02:00
import { useCreateFromUrl } from '@/hooks/use-create-from-url';
import { usePermissions } from '@/hooks/use-permissions';
import { ReminderCard } from './reminder-card';
import { ReminderForm } from './reminder-form';
import { SnoozeDialog } from './snooze-dialog';
interface Reminder {
id: string;
title: string;
note: string | null;
dueAt: string;
priority: 'low' | 'medium' | 'high' | 'urgent';
status: 'pending' | 'snoozed' | 'completed' | 'dismissed';
assignedTo: string | null;
createdBy: string;
clientId: string | null;
interestId: string | null;
berthId: string | null;
autoGenerated: boolean;
snoozedUntil: string | null;
completedAt: string | null;
createdAt: string;
client?: { id: string; fullName: string } | null;
interest?: { id: string; pipelineStage: string } | null;
berth?: { id: string; mooringNumber: string } | null;
}
const PRIORITY_CONFIG = {
urgent: { label: 'Urgent', className: 'bg-red-600 text-white' },
high: { label: 'High', className: 'bg-orange-500 text-white' },
medium: { label: 'Medium', className: 'bg-blue-500 text-white' },
low: { label: 'Low', className: 'bg-gray-400 text-white' },
} as const;
const STATUS_CONFIG = {
pending: { label: 'Pending', icon: Bell },
snoozed: { label: 'Snoozed', icon: Clock },
completed: { label: 'Completed', icon: CheckCircle2 },
dismissed: { label: 'Dismissed', icon: XCircle },
} as const;
feat(ui): broad consistency sweep — sources, dates, comboboxes, milestones Mobile + responsive - berth-form full-width on phones (was 480px fixed → overflowed iPhone) - currency-input switched to inputMode=decimal with live thousands separator - client-form Country/Timezone/Source/Preferred-Contact full-width <sm - contacts row restructured so Primary toggle + Remove get their own strip - customize-dashboard footer stacks vertically on mobile; Done full-width - interest-form client/berth pickers no longer cmdk-filter on UUID (typing "Carlos" now returns Carlos Vega instead of "No clients found") Data + consistency - SOURCES + SOURCE_LABELS + formatSource() in lib/constants; 9 surfaces now resolve interest/client source from one place - INTEREST_OUTCOMES adds lost_other (picker, badge, timeline) - Berth options natural-sort A1 → A2 → … → A10 via lib/utils/mooring-sort - archiver downgraded ^8 → ^7.0.1 so the GDPR export route compiles - TableBody last-row uses border-b-0 (not border-0); colored left-accent on the bottom berth row now renders - Hide Invite-to-Portal until port setting === true (was !== false default-show) - OwnerPicker primer query resolves entity name on first paint (no more UUID flash before the popover opens) Terminology - Replaced user-facing "Documenso" with "signing service" / "Generated EOI" / "Manual EOI" in 8 components (admin/internal references kept) - Plainer status-change copy on berth-detail-header Forms + editing - InlineEditableField gained a `date` variant (native picker); applied to company incorporation date and ready for other YYYY-MM-DD plaintext fields - Inline source picker on interest-tabs detail (was free text) - TagPicker self-hides when port has no tags AND nothing is selected - New ReminderDaysInput with preset chips (1d / 3d / 1wk / 2wk / 1mo / custom) - Compose dialog follow-up is now a toggle that reveals datetime picker Pipeline milestones - changeStageSchema accepts optional milestoneDate; service stamps it on the matching date column instead of always using now - MilestoneAdvanceButton popover collects a back-date before stage advance - Applied to every "Mark X manually" surface on the interest overview EOI / linked-berths polish - Add-bypass row aligned inline with toggle descriptions - Tooltips on "Specifically pitching" / "Mark in EOI bundle" explain their legal vs. public-map consequences Surfaces - Companies list now has the column picker + persisted hidden-column prefs - NotesList aggregate flag enabled on clients, companies, residential_clients (yachts already aggregated) ft/m unit toggle (interim, before drift fix) - "Berth size desired" gets a section-level ft/m toggle; per-field hint shows the converted value. Storage stays canonical-ft for now; the drift-safe persistence migration is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:50:58 +02:00
interface ReminderListProps {
/**
* Embedded mode (used by the Inbox page) drops the PageHeader and
* surfaces the "New Reminder" button inline so the section can render
* alongside the Alerts section without duplicating page chrome.
*/
embedded?: boolean;
}
export function ReminderList({ embedded = false }: ReminderListProps = {}) {
const [reminders, setReminders] = useState<Reminder[]>([]);
const [loading, setLoading] = useState(true);
const [formOpen, setFormOpen] = useState(false);
feat(ui): broad consistency sweep — sources, dates, comboboxes, milestones Mobile + responsive - berth-form full-width on phones (was 480px fixed → overflowed iPhone) - currency-input switched to inputMode=decimal with live thousands separator - client-form Country/Timezone/Source/Preferred-Contact full-width <sm - contacts row restructured so Primary toggle + Remove get their own strip - customize-dashboard footer stacks vertically on mobile; Done full-width - interest-form client/berth pickers no longer cmdk-filter on UUID (typing "Carlos" now returns Carlos Vega instead of "No clients found") Data + consistency - SOURCES + SOURCE_LABELS + formatSource() in lib/constants; 9 surfaces now resolve interest/client source from one place - INTEREST_OUTCOMES adds lost_other (picker, badge, timeline) - Berth options natural-sort A1 → A2 → … → A10 via lib/utils/mooring-sort - archiver downgraded ^8 → ^7.0.1 so the GDPR export route compiles - TableBody last-row uses border-b-0 (not border-0); colored left-accent on the bottom berth row now renders - Hide Invite-to-Portal until port setting === true (was !== false default-show) - OwnerPicker primer query resolves entity name on first paint (no more UUID flash before the popover opens) Terminology - Replaced user-facing "Documenso" with "signing service" / "Generated EOI" / "Manual EOI" in 8 components (admin/internal references kept) - Plainer status-change copy on berth-detail-header Forms + editing - InlineEditableField gained a `date` variant (native picker); applied to company incorporation date and ready for other YYYY-MM-DD plaintext fields - Inline source picker on interest-tabs detail (was free text) - TagPicker self-hides when port has no tags AND nothing is selected - New ReminderDaysInput with preset chips (1d / 3d / 1wk / 2wk / 1mo / custom) - Compose dialog follow-up is now a toggle that reveals datetime picker Pipeline milestones - changeStageSchema accepts optional milestoneDate; service stamps it on the matching date column instead of always using now - MilestoneAdvanceButton popover collects a back-date before stage advance - Applied to every "Mark X manually" surface on the interest overview EOI / linked-berths polish - Add-bypass row aligned inline with toggle descriptions - Tooltips on "Specifically pitching" / "Mark in EOI bundle" explain their legal vs. public-map consequences Surfaces - Companies list now has the column picker + persisted hidden-column prefs - NotesList aggregate flag enabled on clients, companies, residential_clients (yachts already aggregated) ft/m unit toggle (interim, before drift fix) - "Berth size desired" gets a section-level ft/m toggle; per-field hint shows the converted value. Storage stays canonical-ft for now; the drift-safe persistence migration is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:50:58 +02:00
useCreateFromUrl(() => setFormOpen(true));
const [editingReminder, setEditingReminder] = useState<Reminder | null>(null);
const [snoozingId, setSnoozingId] = useState<string | null>(null);
const [viewMode, setViewMode] = useState<'my' | 'all'>('my');
const [statusFilter, setStatusFilter] = useState<string>('active');
const [priorityFilter, setPriorityFilter] = useState<string>('all');
const [total, setTotal] = useState(0);
const { can } = usePermissions();
const canViewAll = can('reminders', 'view_all');
const params = useParams<{ portSlug: string }>();
const portSlug = params?.portSlug ?? '';
const fetchReminders = useCallback(async () => {
setLoading(true);
try {
if (viewMode === 'my') {
const res = await apiFetch<{ data: Reminder[] }>('/api/v1/reminders/my');
let filtered = res.data;
if (priorityFilter !== 'all') {
filtered = filtered.filter((r) => r.priority === priorityFilter);
}
setReminders(filtered);
setTotal(filtered.length);
} else {
const params = new URLSearchParams({ limit: '50', order: 'asc', sort: 'dueAt' });
if (statusFilter === 'active') {
params.set('status', 'pending');
} else if (statusFilter !== 'all') {
params.set('status', statusFilter);
}
if (priorityFilter !== 'all') {
params.set('priority', priorityFilter);
}
const res = await apiFetch<{
data: Reminder[];
pagination: { total: number };
}>(`/api/v1/reminders?${params}`);
setReminders(res.data);
setTotal(res.pagination.total);
}
} finally {
setLoading(false);
}
}, [viewMode, statusFilter, priorityFilter]);
useEffect(() => {
void fetchReminders();
}, [fetchReminders]);
async function handleComplete(id: string) {
await apiFetch(`/api/v1/reminders/${id}/complete`, { method: 'POST' });
await fetchReminders();
}
async function handleDismiss(id: string) {
await apiFetch(`/api/v1/reminders/${id}/dismiss`, { method: 'POST' });
await fetchReminders();
}
function isOverdue(dueAt: string, status: string): boolean {
return (status === 'pending' || status === 'snoozed') && new Date(dueAt) < new Date();
}
const columns: ColumnDef<Reminder, unknown>[] = [
{
accessorKey: 'priority',
header: '',
cell: ({ row }) => {
const config = PRIORITY_CONFIG[row.original.priority];
return <Badge className={`${config.className} text-[10px] px-1.5`}>{config.label}</Badge>;
},
size: 70,
},
{
accessorKey: 'title',
header: 'Reminder',
cell: ({ row }) => {
const overdue = isOverdue(row.original.dueAt, row.original.status);
return (
<div className="flex flex-col gap-0.5">
<div className="flex items-center gap-2">
<span className="font-medium">{row.original.title}</span>
{row.original.autoGenerated && (
<Badge variant="outline" className="text-[10px]">
Auto
</Badge>
)}
{overdue && <AlertTriangle className="h-3.5 w-3.5 text-destructive" />}
</div>
{row.original.client && (
<span className="text-xs text-muted-foreground">
Client: {row.original.client.fullName}
</span>
)}
{row.original.berth && (
<span className="text-xs text-muted-foreground">
Berth: {row.original.berth.mooringNumber}
</span>
)}
</div>
);
},
},
{
accessorKey: 'dueAt',
header: 'Due',
cell: ({ row }) => {
const overdue = isOverdue(row.original.dueAt, row.original.status);
const date = new Date(row.original.dueAt);
return (
<div className={overdue ? 'text-destructive font-medium' : ''}>
<div className="text-sm">{date.toLocaleDateString()}</div>
<div className="text-xs text-muted-foreground">
{formatDistanceToNow(date, { addSuffix: true })}
</div>
</div>
);
},
},
{
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => {
const config = STATUS_CONFIG[row.original.status];
const Icon = config.icon;
return (
<div className="flex items-center gap-1.5 text-sm">
<Icon className="h-3.5 w-3.5" />
{config.label}
</div>
);
},
},
{
id: 'actions',
header: '',
cell: ({ row }) => {
if (row.original.status === 'completed' || row.original.status === 'dismissed') {
return null;
}
return (
feat(ui): broad consistency sweep — sources, dates, comboboxes, milestones Mobile + responsive - berth-form full-width on phones (was 480px fixed → overflowed iPhone) - currency-input switched to inputMode=decimal with live thousands separator - client-form Country/Timezone/Source/Preferred-Contact full-width <sm - contacts row restructured so Primary toggle + Remove get their own strip - customize-dashboard footer stacks vertically on mobile; Done full-width - interest-form client/berth pickers no longer cmdk-filter on UUID (typing "Carlos" now returns Carlos Vega instead of "No clients found") Data + consistency - SOURCES + SOURCE_LABELS + formatSource() in lib/constants; 9 surfaces now resolve interest/client source from one place - INTEREST_OUTCOMES adds lost_other (picker, badge, timeline) - Berth options natural-sort A1 → A2 → … → A10 via lib/utils/mooring-sort - archiver downgraded ^8 → ^7.0.1 so the GDPR export route compiles - TableBody last-row uses border-b-0 (not border-0); colored left-accent on the bottom berth row now renders - Hide Invite-to-Portal until port setting === true (was !== false default-show) - OwnerPicker primer query resolves entity name on first paint (no more UUID flash before the popover opens) Terminology - Replaced user-facing "Documenso" with "signing service" / "Generated EOI" / "Manual EOI" in 8 components (admin/internal references kept) - Plainer status-change copy on berth-detail-header Forms + editing - InlineEditableField gained a `date` variant (native picker); applied to company incorporation date and ready for other YYYY-MM-DD plaintext fields - Inline source picker on interest-tabs detail (was free text) - TagPicker self-hides when port has no tags AND nothing is selected - New ReminderDaysInput with preset chips (1d / 3d / 1wk / 2wk / 1mo / custom) - Compose dialog follow-up is now a toggle that reveals datetime picker Pipeline milestones - changeStageSchema accepts optional milestoneDate; service stamps it on the matching date column instead of always using now - MilestoneAdvanceButton popover collects a back-date before stage advance - Applied to every "Mark X manually" surface on the interest overview EOI / linked-berths polish - Add-bypass row aligned inline with toggle descriptions - Tooltips on "Specifically pitching" / "Mark in EOI bundle" explain their legal vs. public-map consequences Surfaces - Companies list now has the column picker + persisted hidden-column prefs - NotesList aggregate flag enabled on clients, companies, residential_clients (yachts already aggregated) ft/m unit toggle (interim, before drift fix) - "Berth size desired" gets a section-level ft/m toggle; per-field hint shows the converted value. Storage stays canonical-ft for now; the drift-safe persistence migration is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:50:58 +02:00
<TooltipProvider delayDuration={150}>
<div className="flex items-center justify-end gap-1">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
aria-label="Mark complete"
className="text-green-600 hover:text-green-700"
onClick={() => handleComplete(row.original.id)}
>
<CheckCircle2 className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Mark complete</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
aria-label="Snooze"
onClick={() => setSnoozingId(row.original.id)}
>
<Clock className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Snooze</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
aria-label="Edit reminder"
className="text-muted-foreground hover:text-foreground"
onClick={() => {
setEditingReminder(row.original);
setFormOpen(true);
}}
>
<Pencil className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Edit</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
aria-label="Dismiss"
className="text-muted-foreground hover:text-foreground"
onClick={() => handleDismiss(row.original.id)}
>
<XCircle className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Dismiss</TooltipContent>
</Tooltip>
</div>
</TooltipProvider>
);
},
enableSorting: false,
feat(ui): broad consistency sweep — sources, dates, comboboxes, milestones Mobile + responsive - berth-form full-width on phones (was 480px fixed → overflowed iPhone) - currency-input switched to inputMode=decimal with live thousands separator - client-form Country/Timezone/Source/Preferred-Contact full-width <sm - contacts row restructured so Primary toggle + Remove get their own strip - customize-dashboard footer stacks vertically on mobile; Done full-width - interest-form client/berth pickers no longer cmdk-filter on UUID (typing "Carlos" now returns Carlos Vega instead of "No clients found") Data + consistency - SOURCES + SOURCE_LABELS + formatSource() in lib/constants; 9 surfaces now resolve interest/client source from one place - INTEREST_OUTCOMES adds lost_other (picker, badge, timeline) - Berth options natural-sort A1 → A2 → … → A10 via lib/utils/mooring-sort - archiver downgraded ^8 → ^7.0.1 so the GDPR export route compiles - TableBody last-row uses border-b-0 (not border-0); colored left-accent on the bottom berth row now renders - Hide Invite-to-Portal until port setting === true (was !== false default-show) - OwnerPicker primer query resolves entity name on first paint (no more UUID flash before the popover opens) Terminology - Replaced user-facing "Documenso" with "signing service" / "Generated EOI" / "Manual EOI" in 8 components (admin/internal references kept) - Plainer status-change copy on berth-detail-header Forms + editing - InlineEditableField gained a `date` variant (native picker); applied to company incorporation date and ready for other YYYY-MM-DD plaintext fields - Inline source picker on interest-tabs detail (was free text) - TagPicker self-hides when port has no tags AND nothing is selected - New ReminderDaysInput with preset chips (1d / 3d / 1wk / 2wk / 1mo / custom) - Compose dialog follow-up is now a toggle that reveals datetime picker Pipeline milestones - changeStageSchema accepts optional milestoneDate; service stamps it on the matching date column instead of always using now - MilestoneAdvanceButton popover collects a back-date before stage advance - Applied to every "Mark X manually" surface on the interest overview EOI / linked-berths polish - Add-bypass row aligned inline with toggle descriptions - Tooltips on "Specifically pitching" / "Mark in EOI bundle" explain their legal vs. public-map consequences Surfaces - Companies list now has the column picker + persisted hidden-column prefs - NotesList aggregate flag enabled on clients, companies, residential_clients (yachts already aggregated) ft/m unit toggle (interim, before drift fix) - "Berth size desired" gets a section-level ft/m toggle; per-field hint shows the converted value. Storage stays canonical-ft for now; the drift-safe persistence migration is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:50:58 +02:00
size: 160,
},
];
return (
<div>
feat(ui): broad consistency sweep — sources, dates, comboboxes, milestones Mobile + responsive - berth-form full-width on phones (was 480px fixed → overflowed iPhone) - currency-input switched to inputMode=decimal with live thousands separator - client-form Country/Timezone/Source/Preferred-Contact full-width <sm - contacts row restructured so Primary toggle + Remove get their own strip - customize-dashboard footer stacks vertically on mobile; Done full-width - interest-form client/berth pickers no longer cmdk-filter on UUID (typing "Carlos" now returns Carlos Vega instead of "No clients found") Data + consistency - SOURCES + SOURCE_LABELS + formatSource() in lib/constants; 9 surfaces now resolve interest/client source from one place - INTEREST_OUTCOMES adds lost_other (picker, badge, timeline) - Berth options natural-sort A1 → A2 → … → A10 via lib/utils/mooring-sort - archiver downgraded ^8 → ^7.0.1 so the GDPR export route compiles - TableBody last-row uses border-b-0 (not border-0); colored left-accent on the bottom berth row now renders - Hide Invite-to-Portal until port setting === true (was !== false default-show) - OwnerPicker primer query resolves entity name on first paint (no more UUID flash before the popover opens) Terminology - Replaced user-facing "Documenso" with "signing service" / "Generated EOI" / "Manual EOI" in 8 components (admin/internal references kept) - Plainer status-change copy on berth-detail-header Forms + editing - InlineEditableField gained a `date` variant (native picker); applied to company incorporation date and ready for other YYYY-MM-DD plaintext fields - Inline source picker on interest-tabs detail (was free text) - TagPicker self-hides when port has no tags AND nothing is selected - New ReminderDaysInput with preset chips (1d / 3d / 1wk / 2wk / 1mo / custom) - Compose dialog follow-up is now a toggle that reveals datetime picker Pipeline milestones - changeStageSchema accepts optional milestoneDate; service stamps it on the matching date column instead of always using now - MilestoneAdvanceButton popover collects a back-date before stage advance - Applied to every "Mark X manually" surface on the interest overview EOI / linked-berths polish - Add-bypass row aligned inline with toggle descriptions - Tooltips on "Specifically pitching" / "Mark in EOI bundle" explain their legal vs. public-map consequences Surfaces - Companies list now has the column picker + persisted hidden-column prefs - NotesList aggregate flag enabled on clients, companies, residential_clients (yachts already aggregated) ft/m unit toggle (interim, before drift fix) - "Berth size desired" gets a section-level ft/m toggle; per-field hint shows the converted value. Storage stays canonical-ft for now; the drift-safe persistence migration is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:50:58 +02:00
{!embedded ? (
<PageHeader
title="Reminders"
description={`${total} reminder${total !== 1 ? 's' : ''}`}
actions={
<Button
onClick={() => {
setEditingReminder(null);
setFormOpen(true);
}}
>
<Plus className="mr-1.5 h-4 w-4" />
New Reminder
</Button>
}
/>
) : (
<div className="mb-3 flex justify-end">
<Button
feat(ui): broad consistency sweep — sources, dates, comboboxes, milestones Mobile + responsive - berth-form full-width on phones (was 480px fixed → overflowed iPhone) - currency-input switched to inputMode=decimal with live thousands separator - client-form Country/Timezone/Source/Preferred-Contact full-width <sm - contacts row restructured so Primary toggle + Remove get their own strip - customize-dashboard footer stacks vertically on mobile; Done full-width - interest-form client/berth pickers no longer cmdk-filter on UUID (typing "Carlos" now returns Carlos Vega instead of "No clients found") Data + consistency - SOURCES + SOURCE_LABELS + formatSource() in lib/constants; 9 surfaces now resolve interest/client source from one place - INTEREST_OUTCOMES adds lost_other (picker, badge, timeline) - Berth options natural-sort A1 → A2 → … → A10 via lib/utils/mooring-sort - archiver downgraded ^8 → ^7.0.1 so the GDPR export route compiles - TableBody last-row uses border-b-0 (not border-0); colored left-accent on the bottom berth row now renders - Hide Invite-to-Portal until port setting === true (was !== false default-show) - OwnerPicker primer query resolves entity name on first paint (no more UUID flash before the popover opens) Terminology - Replaced user-facing "Documenso" with "signing service" / "Generated EOI" / "Manual EOI" in 8 components (admin/internal references kept) - Plainer status-change copy on berth-detail-header Forms + editing - InlineEditableField gained a `date` variant (native picker); applied to company incorporation date and ready for other YYYY-MM-DD plaintext fields - Inline source picker on interest-tabs detail (was free text) - TagPicker self-hides when port has no tags AND nothing is selected - New ReminderDaysInput with preset chips (1d / 3d / 1wk / 2wk / 1mo / custom) - Compose dialog follow-up is now a toggle that reveals datetime picker Pipeline milestones - changeStageSchema accepts optional milestoneDate; service stamps it on the matching date column instead of always using now - MilestoneAdvanceButton popover collects a back-date before stage advance - Applied to every "Mark X manually" surface on the interest overview EOI / linked-berths polish - Add-bypass row aligned inline with toggle descriptions - Tooltips on "Specifically pitching" / "Mark in EOI bundle" explain their legal vs. public-map consequences Surfaces - Companies list now has the column picker + persisted hidden-column prefs - NotesList aggregate flag enabled on clients, companies, residential_clients (yachts already aggregated) ft/m unit toggle (interim, before drift fix) - "Berth size desired" gets a section-level ft/m toggle; per-field hint shows the converted value. Storage stays canonical-ft for now; the drift-safe persistence migration is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:50:58 +02:00
size="sm"
onClick={() => {
setEditingReminder(null);
setFormOpen(true);
}}
>
<Plus className="mr-1.5 h-4 w-4" />
New Reminder
</Button>
feat(ui): broad consistency sweep — sources, dates, comboboxes, milestones Mobile + responsive - berth-form full-width on phones (was 480px fixed → overflowed iPhone) - currency-input switched to inputMode=decimal with live thousands separator - client-form Country/Timezone/Source/Preferred-Contact full-width <sm - contacts row restructured so Primary toggle + Remove get their own strip - customize-dashboard footer stacks vertically on mobile; Done full-width - interest-form client/berth pickers no longer cmdk-filter on UUID (typing "Carlos" now returns Carlos Vega instead of "No clients found") Data + consistency - SOURCES + SOURCE_LABELS + formatSource() in lib/constants; 9 surfaces now resolve interest/client source from one place - INTEREST_OUTCOMES adds lost_other (picker, badge, timeline) - Berth options natural-sort A1 → A2 → … → A10 via lib/utils/mooring-sort - archiver downgraded ^8 → ^7.0.1 so the GDPR export route compiles - TableBody last-row uses border-b-0 (not border-0); colored left-accent on the bottom berth row now renders - Hide Invite-to-Portal until port setting === true (was !== false default-show) - OwnerPicker primer query resolves entity name on first paint (no more UUID flash before the popover opens) Terminology - Replaced user-facing "Documenso" with "signing service" / "Generated EOI" / "Manual EOI" in 8 components (admin/internal references kept) - Plainer status-change copy on berth-detail-header Forms + editing - InlineEditableField gained a `date` variant (native picker); applied to company incorporation date and ready for other YYYY-MM-DD plaintext fields - Inline source picker on interest-tabs detail (was free text) - TagPicker self-hides when port has no tags AND nothing is selected - New ReminderDaysInput with preset chips (1d / 3d / 1wk / 2wk / 1mo / custom) - Compose dialog follow-up is now a toggle that reveals datetime picker Pipeline milestones - changeStageSchema accepts optional milestoneDate; service stamps it on the matching date column instead of always using now - MilestoneAdvanceButton popover collects a back-date before stage advance - Applied to every "Mark X manually" surface on the interest overview EOI / linked-berths polish - Add-bypass row aligned inline with toggle descriptions - Tooltips on "Specifically pitching" / "Mark in EOI bundle" explain their legal vs. public-map consequences Surfaces - Companies list now has the column picker + persisted hidden-column prefs - NotesList aggregate flag enabled on clients, companies, residential_clients (yachts already aggregated) ft/m unit toggle (interim, before drift fix) - "Berth size desired" gets a section-level ft/m toggle; per-field hint shows the converted value. Storage stays canonical-ft for now; the drift-safe persistence migration is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:50:58 +02:00
</div>
)}
fix(ux): pass-3 — yacht/company headers, reminder filters wrap, client tab counts 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>
2026-05-03 17:09:27 +02:00
{/* 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 && (
<Tabs value={viewMode} onValueChange={(v) => setViewMode(v as 'my' | 'all')}>
<TabsList>
<TabsTrigger value="my">My Reminders</TabsTrigger>
<TabsTrigger value="all">All Reminders</TabsTrigger>
</TabsList>
</Tabs>
)}
{viewMode === 'all' && (
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="pending">Pending</SelectItem>
<SelectItem value="snoozed">Snoozed</SelectItem>
<SelectItem value="completed">Completed</SelectItem>
<SelectItem value="dismissed">Dismissed</SelectItem>
<SelectItem value="all">All</SelectItem>
</SelectContent>
</Select>
)}
<Select value={priorityFilter} onValueChange={setPriorityFilter}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Priorities</SelectItem>
<SelectItem value="urgent">Urgent</SelectItem>
<SelectItem value="high">High</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="low">Low</SelectItem>
</SelectContent>
</Select>
</div>
<DataTable
columns={columns}
data={reminders}
isLoading={loading}
getRowId={(row) => row.id}
cardRender={(row) => (
<ReminderCard
reminder={row.original}
portSlug={portSlug}
onComplete={handleComplete}
onSnooze={setSnoozingId}
onDismiss={handleDismiss}
onEdit={(r) => {
setEditingReminder(r);
setFormOpen(true);
}}
/>
)}
emptyState={
<div className="text-center py-8">
<Bell className="mx-auto h-8 w-8 text-muted-foreground mb-2" />
<p className="text-muted-foreground">No reminders.</p>
<Button
variant="link"
onClick={() => {
setEditingReminder(null);
setFormOpen(true);
}}
className="mt-2"
>
Create your first reminder
</Button>
</div>
}
/>
<ReminderForm
open={formOpen}
onOpenChange={setFormOpen}
reminder={editingReminder}
onSuccess={fetchReminders}
/>
<SnoozeDialog
open={!!snoozingId}
onOpenChange={(open) => {
if (!open) setSnoozingId(null);
}}
reminderId={snoozingId}
onSuccess={fetchReminders}
/>
</div>
);
}