Files
pn-new-crm/src/components/interests/interest-picker.tsx

117 lines
3.6 KiB
TypeScript
Raw Normal View History

feat(documents-wizard): replace UUID-paste fields with searchable pickers + inline upload Reps no longer have to copy/paste UUIDs into the New-document wizard. Three UUID inputs replaced: - Template id Input → DocumentTemplatePicker (queries /api/v1/document-templates with name search; filters to isActive=true) - Uploaded file id Input → inline FileUploadZone (drop or browse PDF; surfaces the uploaded file id directly to the wizard via the new onUploadComplete signature) - Subject id Input → conditional picker: ClientPicker / CompanyPicker / YachtPicker / InterestPicker depending on the subject-type dropdown. Reservation falls back to Input for now (no ReservationPicker yet). Other polish in the wizard: - SIGNER_ROLES labels capitalized in the role select (client → Client, etc.) via a formatSignerRole() helper. Internal values stay lowercase. - Pinned h-9 on Select triggers so the type/subject row + signer-role select vertically align with their adjacent inputs. - Subject-type change now resets subjectId — picker options are type-specific and a stale id from a different entity table would be invalid. Infrastructure for hub uploads (will be consumed in a follow-up dropdown + drag-drop pass): - /api/v1/files/upload route now parses folderId from FormData (schema already supported it). - FileUploadZone accepts a folderId prop and forwards it, plus a new onUploadComplete(file) callback shape that surfaces { id, filename } on each successful upload. Existing per-entity callers (Files tab on clients, companies, yachts, interests) ignore the arg, no behaviour change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:17:02 +02:00
'use client';
import { useState } from 'react';
import { Check, ChevronsUpDown } from 'lucide-react';
import { useQuery } from '@tanstack/react-query';
import { Button } from '@/components/ui/button';
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/components/ui/command';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { useDebounce } from '@/hooks/use-debounce';
import { apiFetch } from '@/lib/api/client';
feat(audit-session): legacy-stage canonicalization + multi-berth label sweep + PDF/UI polish Critical data-correctness fixes - external-eoi.service: stage-advance list rewritten against canonical 7-stage vocab (enquiry/qualified/nurturing → eoi). Was hardcoded to legacy 9-stage names (open/details_sent/in_communication/eoi_sent), so EOI uploads from 'qualified' silently skipped the stage flip. Now also writes eoiDocStatus='signed' alongside eoiStatus='signed'. - public-interest.service + api/public/interests/route: pipelineStage 'open' → 'enquiry' for new public interests. - interests.service: legacy 'open' gate → 'enquiry'; inline-stage-picker comments updated. - Display fallbacks canonicalized: dashboard.service, dashboard-report-data, pdf/templates/{interest,client}-summary, interest-picker, timeline route all route through canonicalizeStage / stageLabelFor. Multi-berth interest label sweep - New helper src/lib/templates/interest-berth-label.ts with 9 unit tests (deriveInterestBerthLabel reuses formatBerthRange + caps at 5 segments, falls back to 'first + N more'). - New batched aggregator getAllBerthMooringsForInterests on the interest-berths service. - BoardInterestRow + listInterests + getInterest extended with berthMoorings: string[]. - Swept render sites: interest-detail-header, pipeline-card + pipeline-column (kanban), interest-columns (list), interest-card, interest-detail (breadcrumb), client-pipeline-summary + client-interests-tab, yacht-tabs, shared interest-picker. - PDF report "New interests (in period)" Source column → Berth column. Dashboard PDF report fixes - Hardcoded EUR → reads ports.default_currency once at the top of resolveDashboardReportData. Falls back to USD. - 'maintenance' berth-status bucket removed everywhere (wasn't in canonical BERTH_STATUSES); cleaned from dashboard.service, dashboard-report-data, occupancy-report, berth-status-chart, fixture. - Berth demand ranking: dropped placeholder Tier column (resolver hardcoded 'A' — heat-tier never plumbed through). - Deal pulse distribution: tier values capitalized (hot → Hot etc.). - Validator widgetIds.max 20 → 40 (catalog has 25 entries; was throwing "Validation failed" when all sections checked). - Export dialog: badges tightened (text-[8px] py-px whitespace-nowrap, no more 2-line wraps on "needs date range"); accepts initialRange?: DateRange so the dashboard's active range pre-fills dateFrom/dateTo via rangeToBounds. Interest banner overcounts fix - interest-berth-status-banner: filters out self-caused under-offer berths (where the only active deal touching the berth IS this same interest). Waits for all competing-queries before committing the count. Was showing "3 berths unavailable" when only 1 actually had a competitor. Sessions list ordering - sessions-list: client-side sort by lastAt desc + displays lastAt instead of firstAt so visible timestamp matches the sort key. Audit log polish - Details button: side Sheet → Popover anchored to the button (in-place inline dropdown). Works with the virtualized table. - From/To date pickers: width w-44 → w-52, wrapper gap-3 → gap-x-4 gap-y-3. EntityFolderView (Documents Hub entity view) - Per-row Download button (hover-reveal icon). - File-type icon prefix + tighter row layout. - Per-row interest-berth badge: files.ts attaches interestBerthLabel via one batched getAllBerthMooringsForInterests call across all groups. AggregatedFile type + EntityFolderView render the badge linking back to the parent interest. External EOI upload dialog - Title input pre-fills from the derived default via controlled displayTitle = title || defaultTitle (no setState-in-effect). EOI Generate dialog - Success toast on mutation success. - Primary berth's "Include in EOI" checkbox is now forced-on + disabled with tooltip: the primary IS the canonical "berth for this deal", excluding it is semantically nonsense. Primary berth must always be in EOI bundle (service + backfill) - interest-berths.service: insert path forces is_in_eoi_bundle=true whenever is_primary=true; update path coerces back to true when the caller tries to set false on a primary. Backfilled 7 existing rows. Documenso redirect URL fallback - port-config getPortDocumensoConfig: resolution chain extended to documenso_redirect_url → public_site_url → null. Operators with public_site_url configured (most ports) now get sensible signer landing without setting two settings. World-map click → navigate - website-analytics-shell: country click navigates to the nationality- filtered Clients page via router.push instead of copying a URL to clipboard. Documents Hub: subfolder grid in main panel - Subfolder cards rendered above the documents list when the current folder has children. Lets reps drill into subfolders from the main content area, not only via the sidebar tree. Interest list initial sort - usePaginatedQuery gains initialSort option (used when URL has no sort param). Interest list passes updatedAt desc so the table header surfaces the active sort visibly + most-recently-added/edited bubble to the top. Interest auto-assign on create - interests.service createInterest: three-tier owner resolution chain — explicit input → port's default_new_interest_owner setting → creator (when not super-admin). Super-admins skipped since they often create on behalf of other reps. Backfills - 12 interests with eoi_status='signed' + missing eoi_doc_status='signed' aligned. - 7 interest_berths rows with is_primary=true but is_in_eoi_bundle=false flipped to true. Verified - pnpm tsc --noEmit: clean - pnpm exec vitest run: 1463 / 1463 passed Captured 25+ additional UAT findings to docs/superpowers/audits/alpha-uat-master.md across all 4 buckets, including two OPEN QUESTIONS (Reservations module re-imagine, Reports dedicated page promotion). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:41:27 +02:00
import { stageLabelFor } from '@/lib/constants';
feat(documents-wizard): replace UUID-paste fields with searchable pickers + inline upload Reps no longer have to copy/paste UUIDs into the New-document wizard. Three UUID inputs replaced: - Template id Input → DocumentTemplatePicker (queries /api/v1/document-templates with name search; filters to isActive=true) - Uploaded file id Input → inline FileUploadZone (drop or browse PDF; surfaces the uploaded file id directly to the wizard via the new onUploadComplete signature) - Subject id Input → conditional picker: ClientPicker / CompanyPicker / YachtPicker / InterestPicker depending on the subject-type dropdown. Reservation falls back to Input for now (no ReservationPicker yet). Other polish in the wizard: - SIGNER_ROLES labels capitalized in the role select (client → Client, etc.) via a formatSignerRole() helper. Internal values stay lowercase. - Pinned h-9 on Select triggers so the type/subject row + signer-role select vertically align with their adjacent inputs. - Subject-type change now resets subjectId — picker options are type-specific and a stale id from a different entity table would be invalid. Infrastructure for hub uploads (will be consumed in a follow-up dropdown + drag-drop pass): - /api/v1/files/upload route now parses folderId from FormData (schema already supported it). - FileUploadZone accepts a folderId prop and forwards it, plus a new onUploadComplete(file) callback shape that surfaces { id, filename } on each successful upload. Existing per-entity callers (Files tab on clients, companies, yachts, interests) ignore the arg, no behaviour change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:17:02 +02:00
import { cn } from '@/lib/utils';
interface InterestOption {
id: string;
clientId: string;
clientName?: string;
pipelineStage?: string;
// Some list endpoints surface the linked client inline; we display whatever's
// available with a fallback to a short id.
}
interface InterestPickerProps {
value: string | null;
onChange: (interestId: string | null) => void;
placeholder?: string;
disabled?: boolean;
}
export function InterestPicker({
value,
onChange,
placeholder = 'Select interest...',
disabled,
}: InterestPickerProps) {
const [open, setOpen] = useState(false);
const [search, setSearch] = useState('');
const debounced = useDebounce(search, 300);
const { data } = useQuery<{ data: InterestOption[] }>({
queryKey: ['interest-picker', debounced],
queryFn: () =>
apiFetch(
`/api/v1/interests?search=${encodeURIComponent(debounced)}&page=1&limit=10&order=desc&includeArchived=false`,
),
enabled: open,
});
const options = data?.data ?? [];
const selectedLabel = (() => {
if (!value) return placeholder;
const match = options.find((o) => o.id === value);
if (!match) return `Interest ${value.slice(0, 8)}`;
feat(audit-session): legacy-stage canonicalization + multi-berth label sweep + PDF/UI polish Critical data-correctness fixes - external-eoi.service: stage-advance list rewritten against canonical 7-stage vocab (enquiry/qualified/nurturing → eoi). Was hardcoded to legacy 9-stage names (open/details_sent/in_communication/eoi_sent), so EOI uploads from 'qualified' silently skipped the stage flip. Now also writes eoiDocStatus='signed' alongside eoiStatus='signed'. - public-interest.service + api/public/interests/route: pipelineStage 'open' → 'enquiry' for new public interests. - interests.service: legacy 'open' gate → 'enquiry'; inline-stage-picker comments updated. - Display fallbacks canonicalized: dashboard.service, dashboard-report-data, pdf/templates/{interest,client}-summary, interest-picker, timeline route all route through canonicalizeStage / stageLabelFor. Multi-berth interest label sweep - New helper src/lib/templates/interest-berth-label.ts with 9 unit tests (deriveInterestBerthLabel reuses formatBerthRange + caps at 5 segments, falls back to 'first + N more'). - New batched aggregator getAllBerthMooringsForInterests on the interest-berths service. - BoardInterestRow + listInterests + getInterest extended with berthMoorings: string[]. - Swept render sites: interest-detail-header, pipeline-card + pipeline-column (kanban), interest-columns (list), interest-card, interest-detail (breadcrumb), client-pipeline-summary + client-interests-tab, yacht-tabs, shared interest-picker. - PDF report "New interests (in period)" Source column → Berth column. Dashboard PDF report fixes - Hardcoded EUR → reads ports.default_currency once at the top of resolveDashboardReportData. Falls back to USD. - 'maintenance' berth-status bucket removed everywhere (wasn't in canonical BERTH_STATUSES); cleaned from dashboard.service, dashboard-report-data, occupancy-report, berth-status-chart, fixture. - Berth demand ranking: dropped placeholder Tier column (resolver hardcoded 'A' — heat-tier never plumbed through). - Deal pulse distribution: tier values capitalized (hot → Hot etc.). - Validator widgetIds.max 20 → 40 (catalog has 25 entries; was throwing "Validation failed" when all sections checked). - Export dialog: badges tightened (text-[8px] py-px whitespace-nowrap, no more 2-line wraps on "needs date range"); accepts initialRange?: DateRange so the dashboard's active range pre-fills dateFrom/dateTo via rangeToBounds. Interest banner overcounts fix - interest-berth-status-banner: filters out self-caused under-offer berths (where the only active deal touching the berth IS this same interest). Waits for all competing-queries before committing the count. Was showing "3 berths unavailable" when only 1 actually had a competitor. Sessions list ordering - sessions-list: client-side sort by lastAt desc + displays lastAt instead of firstAt so visible timestamp matches the sort key. Audit log polish - Details button: side Sheet → Popover anchored to the button (in-place inline dropdown). Works with the virtualized table. - From/To date pickers: width w-44 → w-52, wrapper gap-3 → gap-x-4 gap-y-3. EntityFolderView (Documents Hub entity view) - Per-row Download button (hover-reveal icon). - File-type icon prefix + tighter row layout. - Per-row interest-berth badge: files.ts attaches interestBerthLabel via one batched getAllBerthMooringsForInterests call across all groups. AggregatedFile type + EntityFolderView render the badge linking back to the parent interest. External EOI upload dialog - Title input pre-fills from the derived default via controlled displayTitle = title || defaultTitle (no setState-in-effect). EOI Generate dialog - Success toast on mutation success. - Primary berth's "Include in EOI" checkbox is now forced-on + disabled with tooltip: the primary IS the canonical "berth for this deal", excluding it is semantically nonsense. Primary berth must always be in EOI bundle (service + backfill) - interest-berths.service: insert path forces is_in_eoi_bundle=true whenever is_primary=true; update path coerces back to true when the caller tries to set false on a primary. Backfilled 7 existing rows. Documenso redirect URL fallback - port-config getPortDocumensoConfig: resolution chain extended to documenso_redirect_url → public_site_url → null. Operators with public_site_url configured (most ports) now get sensible signer landing without setting two settings. World-map click → navigate - website-analytics-shell: country click navigates to the nationality- filtered Clients page via router.push instead of copying a URL to clipboard. Documents Hub: subfolder grid in main panel - Subfolder cards rendered above the documents list when the current folder has children. Lets reps drill into subfolders from the main content area, not only via the sidebar tree. Interest list initial sort - usePaginatedQuery gains initialSort option (used when URL has no sort param). Interest list passes updatedAt desc so the table header surfaces the active sort visibly + most-recently-added/edited bubble to the top. Interest auto-assign on create - interests.service createInterest: three-tier owner resolution chain — explicit input → port's default_new_interest_owner setting → creator (when not super-admin). Super-admins skipped since they often create on behalf of other reps. Backfills - 12 interests with eoi_status='signed' + missing eoi_doc_status='signed' aligned. - 7 interest_berths rows with is_primary=true but is_in_eoi_bundle=false flipped to true. Verified - pnpm tsc --noEmit: clean - pnpm exec vitest run: 1463 / 1463 passed Captured 25+ additional UAT findings to docs/superpowers/audits/alpha-uat-master.md across all 4 buckets, including two OPEN QUESTIONS (Reservations module re-imagine, Reports dedicated page promotion). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:41:27 +02:00
if (match.clientName) return `${match.clientName} - ${stageLabelFor(match.pipelineStage)}`;
feat(documents-wizard): replace UUID-paste fields with searchable pickers + inline upload Reps no longer have to copy/paste UUIDs into the New-document wizard. Three UUID inputs replaced: - Template id Input → DocumentTemplatePicker (queries /api/v1/document-templates with name search; filters to isActive=true) - Uploaded file id Input → inline FileUploadZone (drop or browse PDF; surfaces the uploaded file id directly to the wizard via the new onUploadComplete signature) - Subject id Input → conditional picker: ClientPicker / CompanyPicker / YachtPicker / InterestPicker depending on the subject-type dropdown. Reservation falls back to Input for now (no ReservationPicker yet). Other polish in the wizard: - SIGNER_ROLES labels capitalized in the role select (client → Client, etc.) via a formatSignerRole() helper. Internal values stay lowercase. - Pinned h-9 on Select triggers so the type/subject row + signer-role select vertically align with their adjacent inputs. - Subject-type change now resets subjectId — picker options are type-specific and a stale id from a different entity table would be invalid. Infrastructure for hub uploads (will be consumed in a follow-up dropdown + drag-drop pass): - /api/v1/files/upload route now parses folderId from FormData (schema already supported it). - FileUploadZone accepts a folderId prop and forwards it, plus a new onUploadComplete(file) callback shape that surfaces { id, filename } on each successful upload. Existing per-entity callers (Files tab on clients, companies, yachts, interests) ignore the arg, no behaviour change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:17:02 +02:00
return `Interest ${match.id.slice(0, 8)}`;
})();
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
<Popover open={open} onOpenChange={setOpen} modal>
feat(documents-wizard): replace UUID-paste fields with searchable pickers + inline upload Reps no longer have to copy/paste UUIDs into the New-document wizard. Three UUID inputs replaced: - Template id Input → DocumentTemplatePicker (queries /api/v1/document-templates with name search; filters to isActive=true) - Uploaded file id Input → inline FileUploadZone (drop or browse PDF; surfaces the uploaded file id directly to the wizard via the new onUploadComplete signature) - Subject id Input → conditional picker: ClientPicker / CompanyPicker / YachtPicker / InterestPicker depending on the subject-type dropdown. Reservation falls back to Input for now (no ReservationPicker yet). Other polish in the wizard: - SIGNER_ROLES labels capitalized in the role select (client → Client, etc.) via a formatSignerRole() helper. Internal values stay lowercase. - Pinned h-9 on Select triggers so the type/subject row + signer-role select vertically align with their adjacent inputs. - Subject-type change now resets subjectId — picker options are type-specific and a stale id from a different entity table would be invalid. Infrastructure for hub uploads (will be consumed in a follow-up dropdown + drag-drop pass): - /api/v1/files/upload route now parses folderId from FormData (schema already supported it). - FileUploadZone accepts a folderId prop and forwards it, plus a new onUploadComplete(file) callback shape that surfaces { id, filename } on each successful upload. Existing per-entity callers (Files tab on clients, companies, yachts, interests) ignore the arg, no behaviour change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:17:02 +02:00
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
disabled={disabled}
className={cn('w-full justify-between', !value && 'text-muted-foreground')}
>
<span className="truncate">{selectedLabel}</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" aria-hidden />
feat(documents-wizard): replace UUID-paste fields with searchable pickers + inline upload Reps no longer have to copy/paste UUIDs into the New-document wizard. Three UUID inputs replaced: - Template id Input → DocumentTemplatePicker (queries /api/v1/document-templates with name search; filters to isActive=true) - Uploaded file id Input → inline FileUploadZone (drop or browse PDF; surfaces the uploaded file id directly to the wizard via the new onUploadComplete signature) - Subject id Input → conditional picker: ClientPicker / CompanyPicker / YachtPicker / InterestPicker depending on the subject-type dropdown. Reservation falls back to Input for now (no ReservationPicker yet). Other polish in the wizard: - SIGNER_ROLES labels capitalized in the role select (client → Client, etc.) via a formatSignerRole() helper. Internal values stay lowercase. - Pinned h-9 on Select triggers so the type/subject row + signer-role select vertically align with their adjacent inputs. - Subject-type change now resets subjectId — picker options are type-specific and a stale id from a different entity table would be invalid. Infrastructure for hub uploads (will be consumed in a follow-up dropdown + drag-drop pass): - /api/v1/files/upload route now parses folderId from FormData (schema already supported it). - FileUploadZone accepts a folderId prop and forwards it, plus a new onUploadComplete(file) callback shape that surfaces { id, filename } on each successful upload. Existing per-entity callers (Files tab on clients, companies, yachts, interests) ignore the arg, no behaviour change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:17:02 +02:00
</Button>
</PopoverTrigger>
<PopoverContent className="w-[320px] p-0" align="start">
<Command shouldFilter={false}>
<CommandInput
placeholder="Search by client name…"
value={search}
onValueChange={setSearch}
/>
<CommandList>
<CommandEmpty>No interests found.</CommandEmpty>
<CommandGroup>
{options.map((i) => (
<CommandItem
key={i.id}
value={i.id}
onSelect={() => {
onChange(i.id);
setOpen(false);
}}
>
<Check
className={cn('mr-2 h-4 w-4', value === i.id ? 'opacity-100' : 'opacity-0')}
/>
<span className="truncate">
{i.clientName ?? `Interest ${i.id.slice(0, 8)}`}
{i.pipelineStage ? (
<span className="ml-2 text-xs text-muted-foreground">{i.pipelineStage}</span>
) : null}
</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}