feat(pipeline): 9→7 stage refactor + v1.1 hardening wave
Replaces the legacy 9-stage pipeline with 7 canonical stages
(enquiry → qualified → eoi → reservation → deposit_paid → contract →
nurturing) plus three doc sub-status columns (eoi_doc_status,
reservation_doc_status, contract_doc_status) that track sent/signed
within a single stage instead of branching it.
Schema (migration 0062):
- interests gains assigned_to, deposit_expected_amount/currency,
three doc-status columns, two documenso-id columns, and
date_reservation_signed.
- New tables: qualification_criteria (per-port admin-configurable),
interest_qualifications (per-interest state), payments (deposit /
balance / refund records keyed to interest + client).
- Default qualification criteria seeded for every existing port.
- Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into
the new stage + doc-status + outcome shape.
Migration 0063 adds interest_contact_log.voice_transcript and
template_used columns for v1.1-A/B (quick-template buttons + voice
transcription via Web Speech API).
v1.1 phase work bundled here:
- A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on
the contact-log compose dialog (useVoiceTranscription hook).
- C: berth-rules-engine wraps state writes in pg_advisory_xact_lock
with an idempotent re-read; emits rule_evaluated audit traces.
- D: Documenso webhook: reservation/contract sub-status stamping
moved out of the PDF-download try-block so a download failure
no longer swallows the stamp. New integration test coverage.
- E: /admin/qualification-criteria CRUD page + admin component.
- F: default_new_interest_owner exposed in System Settings.
- G: recentActivityCount + active_engagement deal-pulse signal
surfaced as a chip on interests + hot-deals card.
- H: interest_assigned notification on assignedTo change (skips
self-assign, uses a dedupe key).
Plus the supporting components: AssignedToChip, DealPulseChip,
PaymentsSection, QualificationChecklist, MultiEoiChip,
SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner,
SupplementalInfoRequestButton, UserPicker.
Tests: 1370/1370 vitest pass (added deal-health unit suite +
expanded constants/validators/pipeline-transitions coverage). tsc
clean, eslint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { useState } from 'react';
|
|
|
|
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
|
|
|
import { Plus, Trash2, Receipt } from 'lucide-react';
|
|
|
|
|
|
|
|
|
|
import { Button } from '@/components/ui/button';
|
2026-05-21 17:10:02 +02:00
|
|
|
import { DatePicker } from '@/components/ui/date-picker';
|
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave
Replaces the legacy 9-stage pipeline with 7 canonical stages
(enquiry → qualified → eoi → reservation → deposit_paid → contract →
nurturing) plus three doc sub-status columns (eoi_doc_status,
reservation_doc_status, contract_doc_status) that track sent/signed
within a single stage instead of branching it.
Schema (migration 0062):
- interests gains assigned_to, deposit_expected_amount/currency,
three doc-status columns, two documenso-id columns, and
date_reservation_signed.
- New tables: qualification_criteria (per-port admin-configurable),
interest_qualifications (per-interest state), payments (deposit /
balance / refund records keyed to interest + client).
- Default qualification criteria seeded for every existing port.
- Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into
the new stage + doc-status + outcome shape.
Migration 0063 adds interest_contact_log.voice_transcript and
template_used columns for v1.1-A/B (quick-template buttons + voice
transcription via Web Speech API).
v1.1 phase work bundled here:
- A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on
the contact-log compose dialog (useVoiceTranscription hook).
- C: berth-rules-engine wraps state writes in pg_advisory_xact_lock
with an idempotent re-read; emits rule_evaluated audit traces.
- D: Documenso webhook: reservation/contract sub-status stamping
moved out of the PDF-download try-block so a download failure
no longer swallows the stamp. New integration test coverage.
- E: /admin/qualification-criteria CRUD page + admin component.
- F: default_new_interest_owner exposed in System Settings.
- G: recentActivityCount + active_engagement deal-pulse signal
surfaced as a chip on interests + hot-deals card.
- H: interest_assigned notification on assignedTo change (skips
self-assign, uses a dedupe key).
Plus the supporting components: AssignedToChip, DealPulseChip,
PaymentsSection, QualificationChecklist, MultiEoiChip,
SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner,
SupplementalInfoRequestButton, UserPicker.
Tests: 1370/1370 vitest pass (added deal-health unit suite +
expanded constants/validators/pipeline-transitions coverage). tsc
clean, eslint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
|
|
|
import { Input } from '@/components/ui/input';
|
|
|
|
|
import { Label } from '@/components/ui/label';
|
|
|
|
|
import {
|
|
|
|
|
Sheet,
|
|
|
|
|
SheetContent,
|
|
|
|
|
SheetDescription,
|
|
|
|
|
SheetHeader,
|
|
|
|
|
SheetTitle,
|
|
|
|
|
SheetFooter,
|
|
|
|
|
} from '@/components/ui/sheet';
|
|
|
|
|
import {
|
|
|
|
|
Select,
|
|
|
|
|
SelectContent,
|
|
|
|
|
SelectItem,
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
SelectValue,
|
|
|
|
|
} from '@/components/ui/select';
|
|
|
|
|
import { PermissionGate } from '@/components/shared/permission-gate';
|
|
|
|
|
import { apiFetch } from '@/lib/api/client';
|
|
|
|
|
import { toastError } from '@/lib/api/toast-error';
|
|
|
|
|
|
|
|
|
|
interface PaymentRow {
|
|
|
|
|
id: string;
|
|
|
|
|
paymentType: string;
|
|
|
|
|
amount: string;
|
|
|
|
|
currency: string;
|
|
|
|
|
receivedAt: string;
|
|
|
|
|
receiptFileId: string | null;
|
|
|
|
|
notes: string | null;
|
|
|
|
|
recordedBy: string;
|
|
|
|
|
createdAt: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface PaymentsResponse {
|
|
|
|
|
data: {
|
|
|
|
|
payments: PaymentRow[];
|
|
|
|
|
depositTotal: { total: string; currency: string };
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const TYPE_LABELS: Record<string, string> = {
|
|
|
|
|
deposit: 'Deposit',
|
|
|
|
|
balance: 'Balance',
|
|
|
|
|
refund: 'Refund',
|
|
|
|
|
other: 'Other',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const TYPE_TINT: Record<string, string> = {
|
|
|
|
|
deposit: 'bg-emerald-50 text-emerald-700 border-emerald-200',
|
|
|
|
|
balance: 'bg-sky-50 text-sky-700 border-sky-200',
|
|
|
|
|
refund: 'bg-rose-50 text-rose-700 border-rose-200',
|
|
|
|
|
other: 'bg-slate-100 text-slate-700 border-slate-200',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function formatMoney(amount: string, currency: string): string {
|
|
|
|
|
const n = Number(amount);
|
|
|
|
|
if (!Number.isFinite(n)) return `${amount} ${currency}`;
|
|
|
|
|
try {
|
|
|
|
|
return new Intl.NumberFormat('en-EU', { style: 'currency', currency }).format(n);
|
|
|
|
|
} catch {
|
|
|
|
|
return `${n.toFixed(2)} ${currency}`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatDate(iso: string): string {
|
|
|
|
|
return new Date(iso).toLocaleDateString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function PaymentsSection({
|
|
|
|
|
interestId,
|
|
|
|
|
depositExpectedAmount,
|
|
|
|
|
depositExpectedCurrency,
|
|
|
|
|
}: {
|
|
|
|
|
interestId: string;
|
|
|
|
|
depositExpectedAmount: string | null;
|
|
|
|
|
depositExpectedCurrency: string | null;
|
|
|
|
|
}) {
|
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
const [recordOpen, setRecordOpen] = useState(false);
|
|
|
|
|
|
|
|
|
|
const { data, isLoading } = useQuery<PaymentsResponse>({
|
|
|
|
|
queryKey: ['interest-payments', interestId],
|
|
|
|
|
queryFn: () => apiFetch(`/api/v1/interests/${interestId}/payments`),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const deleteMutation = useMutation({
|
|
|
|
|
mutationFn: async (paymentId: string) =>
|
|
|
|
|
apiFetch(`/api/v1/payments/${paymentId}`, { method: 'DELETE' }),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ['interest-payments', interestId] });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ['interests', interestId] });
|
|
|
|
|
},
|
|
|
|
|
onError: (err) => toastError(err),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (isLoading) {
|
|
|
|
|
return (
|
|
|
|
|
<section className="rounded-lg border p-4 text-sm text-muted-foreground">
|
|
|
|
|
Loading payments…
|
|
|
|
|
</section>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const payments = data?.data.payments ?? [];
|
|
|
|
|
const total = data?.data.depositTotal;
|
|
|
|
|
const expectedAmount = depositExpectedAmount ? Number(depositExpectedAmount) : null;
|
|
|
|
|
const expectedCurrency = depositExpectedCurrency ?? 'EUR';
|
|
|
|
|
const runningTotal = total ? Number(total.total) : 0;
|
|
|
|
|
const remaining =
|
|
|
|
|
expectedAmount !== null && Number.isFinite(expectedAmount)
|
|
|
|
|
? Math.max(0, expectedAmount - runningTotal)
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<section className="rounded-lg border bg-card/40 p-4 space-y-3">
|
|
|
|
|
<div className="flex items-center justify-between gap-3">
|
|
|
|
|
<div>
|
|
|
|
|
<h3 className="text-sm font-semibold">Payments</h3>
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
Records that money was received or refunded. No invoices are issued — the bank handles
|
|
|
|
|
that.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
2026-05-14 03:46:01 +02:00
|
|
|
<PermissionGate resource="payments" action="record">
|
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave
Replaces the legacy 9-stage pipeline with 7 canonical stages
(enquiry → qualified → eoi → reservation → deposit_paid → contract →
nurturing) plus three doc sub-status columns (eoi_doc_status,
reservation_doc_status, contract_doc_status) that track sent/signed
within a single stage instead of branching it.
Schema (migration 0062):
- interests gains assigned_to, deposit_expected_amount/currency,
three doc-status columns, two documenso-id columns, and
date_reservation_signed.
- New tables: qualification_criteria (per-port admin-configurable),
interest_qualifications (per-interest state), payments (deposit /
balance / refund records keyed to interest + client).
- Default qualification criteria seeded for every existing port.
- Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into
the new stage + doc-status + outcome shape.
Migration 0063 adds interest_contact_log.voice_transcript and
template_used columns for v1.1-A/B (quick-template buttons + voice
transcription via Web Speech API).
v1.1 phase work bundled here:
- A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on
the contact-log compose dialog (useVoiceTranscription hook).
- C: berth-rules-engine wraps state writes in pg_advisory_xact_lock
with an idempotent re-read; emits rule_evaluated audit traces.
- D: Documenso webhook: reservation/contract sub-status stamping
moved out of the PDF-download try-block so a download failure
no longer swallows the stamp. New integration test coverage.
- E: /admin/qualification-criteria CRUD page + admin component.
- F: default_new_interest_owner exposed in System Settings.
- G: recentActivityCount + active_engagement deal-pulse signal
surfaced as a chip on interests + hot-deals card.
- H: interest_assigned notification on assignedTo change (skips
self-assign, uses a dedupe key).
Plus the supporting components: AssignedToChip, DealPulseChip,
PaymentsSection, QualificationChecklist, MultiEoiChip,
SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner,
SupplementalInfoRequestButton, UserPicker.
Tests: 1370/1370 vitest pass (added deal-health unit suite +
expanded constants/validators/pipeline-transitions coverage). tsc
clean, eslint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
|
|
|
<Button size="sm" className="h-8 px-3 text-xs" onClick={() => setRecordOpen(true)}>
|
|
|
|
|
<Plus className="size-3.5" aria-hidden />
|
|
|
|
|
Record payment
|
|
|
|
|
</Button>
|
|
|
|
|
</PermissionGate>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{expectedAmount !== null ? (
|
|
|
|
|
<div className="flex items-center justify-between rounded-md border border-border bg-muted/30 px-3 py-2 text-xs">
|
|
|
|
|
<span>
|
|
|
|
|
Expected deposit:{' '}
|
|
|
|
|
<strong>{formatMoney(String(expectedAmount), expectedCurrency)}</strong>
|
|
|
|
|
</span>
|
|
|
|
|
<span>
|
|
|
|
|
Received so far: <strong>{formatMoney(total?.total ?? '0', expectedCurrency)}</strong>
|
|
|
|
|
</span>
|
|
|
|
|
{remaining !== null ? (
|
|
|
|
|
<span className={remaining === 0 ? 'text-emerald-700' : 'text-amber-700'}>
|
|
|
|
|
{remaining === 0
|
|
|
|
|
? 'Fully received'
|
|
|
|
|
: `${formatMoney(String(remaining), expectedCurrency)} outstanding`}
|
|
|
|
|
</span>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
|
|
{payments.length === 0 ? (
|
|
|
|
|
<p className="rounded border border-dashed px-3 py-4 text-center text-xs text-muted-foreground">
|
|
|
|
|
No payments recorded yet.
|
|
|
|
|
</p>
|
|
|
|
|
) : (
|
|
|
|
|
<ul className="divide-y divide-border rounded border">
|
|
|
|
|
{payments.map((p) => (
|
|
|
|
|
<li key={p.id} className="flex items-center justify-between gap-3 px-3 py-2">
|
|
|
|
|
<div className="flex items-center gap-2.5">
|
|
|
|
|
<span
|
|
|
|
|
className={`inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-medium ${
|
|
|
|
|
TYPE_TINT[p.paymentType] ?? TYPE_TINT.other
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{TYPE_LABELS[p.paymentType] ?? p.paymentType}
|
|
|
|
|
</span>
|
|
|
|
|
<div className="text-sm">
|
|
|
|
|
<span className="font-medium">{formatMoney(p.amount, p.currency)}</span>
|
|
|
|
|
<span className="ml-2 text-xs text-muted-foreground">
|
|
|
|
|
{formatDate(p.receivedAt)}
|
|
|
|
|
</span>
|
|
|
|
|
{p.notes ? (
|
|
|
|
|
<span className="ml-2 text-xs text-muted-foreground">· {p.notes}</span>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
{p.receiptFileId ? (
|
|
|
|
|
<Receipt className="size-3 text-emerald-600" aria-hidden />
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
2026-05-14 03:46:01 +02:00
|
|
|
<PermissionGate resource="payments" action="delete">
|
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave
Replaces the legacy 9-stage pipeline with 7 canonical stages
(enquiry → qualified → eoi → reservation → deposit_paid → contract →
nurturing) plus three doc sub-status columns (eoi_doc_status,
reservation_doc_status, contract_doc_status) that track sent/signed
within a single stage instead of branching it.
Schema (migration 0062):
- interests gains assigned_to, deposit_expected_amount/currency,
three doc-status columns, two documenso-id columns, and
date_reservation_signed.
- New tables: qualification_criteria (per-port admin-configurable),
interest_qualifications (per-interest state), payments (deposit /
balance / refund records keyed to interest + client).
- Default qualification criteria seeded for every existing port.
- Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into
the new stage + doc-status + outcome shape.
Migration 0063 adds interest_contact_log.voice_transcript and
template_used columns for v1.1-A/B (quick-template buttons + voice
transcription via Web Speech API).
v1.1 phase work bundled here:
- A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on
the contact-log compose dialog (useVoiceTranscription hook).
- C: berth-rules-engine wraps state writes in pg_advisory_xact_lock
with an idempotent re-read; emits rule_evaluated audit traces.
- D: Documenso webhook: reservation/contract sub-status stamping
moved out of the PDF-download try-block so a download failure
no longer swallows the stamp. New integration test coverage.
- E: /admin/qualification-criteria CRUD page + admin component.
- F: default_new_interest_owner exposed in System Settings.
- G: recentActivityCount + active_engagement deal-pulse signal
surfaced as a chip on interests + hot-deals card.
- H: interest_assigned notification on assignedTo change (skips
self-assign, uses a dedupe key).
Plus the supporting components: AssignedToChip, DealPulseChip,
PaymentsSection, QualificationChecklist, MultiEoiChip,
SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner,
SupplementalInfoRequestButton, UserPicker.
Tests: 1370/1370 vitest pass (added deal-health unit suite +
expanded constants/validators/pipeline-transitions coverage). tsc
clean, eslint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
aria-label="Delete payment record"
|
|
|
|
|
className="rounded p-1 text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
|
|
|
|
|
disabled={deleteMutation.isPending}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
if (confirm('Delete this payment record? This cannot be undone.')) {
|
|
|
|
|
deleteMutation.mutate(p.id);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="size-3.5" aria-hidden />
|
|
|
|
|
</button>
|
|
|
|
|
</PermissionGate>
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<RecordPaymentSheet
|
|
|
|
|
open={recordOpen}
|
|
|
|
|
onOpenChange={setRecordOpen}
|
|
|
|
|
interestId={interestId}
|
|
|
|
|
defaultCurrency={expectedCurrency}
|
|
|
|
|
/>
|
|
|
|
|
</section>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function RecordPaymentSheet({
|
|
|
|
|
open,
|
|
|
|
|
onOpenChange,
|
|
|
|
|
interestId,
|
|
|
|
|
defaultCurrency,
|
|
|
|
|
}: {
|
|
|
|
|
open: boolean;
|
|
|
|
|
onOpenChange: (v: boolean) => void;
|
|
|
|
|
interestId: string;
|
|
|
|
|
defaultCurrency: string;
|
|
|
|
|
}) {
|
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
const [paymentType, setPaymentType] = useState<string>('deposit');
|
|
|
|
|
const [amount, setAmount] = useState('');
|
|
|
|
|
const [currency, setCurrency] = useState(defaultCurrency);
|
|
|
|
|
const [receivedAt, setReceivedAt] = useState(() => {
|
|
|
|
|
const today = new Date();
|
|
|
|
|
return today.toISOString().slice(0, 10);
|
|
|
|
|
});
|
|
|
|
|
const [notes, setNotes] = useState('');
|
|
|
|
|
const [acknowledgedNoReceipt, setAcknowledgedNoReceipt] = useState(false);
|
|
|
|
|
|
|
|
|
|
const mutation = useMutation({
|
|
|
|
|
mutationFn: async () =>
|
|
|
|
|
apiFetch(`/api/v1/interests/${interestId}/payments`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: {
|
|
|
|
|
interestId,
|
|
|
|
|
paymentType,
|
|
|
|
|
amount,
|
|
|
|
|
currency,
|
|
|
|
|
receivedAt: new Date(receivedAt).toISOString(),
|
|
|
|
|
notes: notes || null,
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ['interest-payments', interestId] });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ['interests', interestId] });
|
|
|
|
|
onOpenChange(false);
|
|
|
|
|
// Reset form for next use
|
|
|
|
|
setAmount('');
|
|
|
|
|
setNotes('');
|
|
|
|
|
setAcknowledgedNoReceipt(false);
|
|
|
|
|
},
|
|
|
|
|
onError: (err) => toastError(err),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const canSubmit =
|
|
|
|
|
amount.trim().length > 0 && receivedAt && acknowledgedNoReceipt && !mutation.isPending;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
|
|
|
<SheetContent side="right" className="w-full overflow-y-auto sm:max-w-md">
|
|
|
|
|
<SheetHeader>
|
|
|
|
|
<SheetTitle>Record payment</SheetTitle>
|
|
|
|
|
<SheetDescription>
|
|
|
|
|
Capture that money was received (or refunded). Reps don't issue invoices — the bank
|
|
|
|
|
does that — so this is just an audit record.
|
|
|
|
|
</SheetDescription>
|
|
|
|
|
</SheetHeader>
|
|
|
|
|
|
|
|
|
|
<form
|
|
|
|
|
className="mt-5 space-y-4"
|
|
|
|
|
onSubmit={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
mutation.mutate();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<Label htmlFor="payment-type">Type</Label>
|
|
|
|
|
<Select value={paymentType} onValueChange={setPaymentType}>
|
|
|
|
|
<SelectTrigger id="payment-type">
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="deposit">Deposit</SelectItem>
|
|
|
|
|
<SelectItem value="balance">Balance</SelectItem>
|
|
|
|
|
<SelectItem value="refund">Refund</SelectItem>
|
|
|
|
|
<SelectItem value="other">Other</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-[1fr_100px] gap-2">
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<Label htmlFor="payment-amount">Amount</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="payment-amount"
|
|
|
|
|
type="number"
|
|
|
|
|
step="0.01"
|
|
|
|
|
min="0"
|
|
|
|
|
placeholder="0.00"
|
|
|
|
|
value={amount}
|
|
|
|
|
onChange={(e) => setAmount(e.target.value)}
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<Label htmlFor="payment-currency">Currency</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="payment-currency"
|
|
|
|
|
value={currency}
|
|
|
|
|
onChange={(e) => setCurrency(e.target.value.toUpperCase())}
|
|
|
|
|
maxLength={3}
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<Label htmlFor="payment-date">Received on</Label>
|
2026-05-21 17:10:02 +02:00
|
|
|
<DatePicker
|
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave
Replaces the legacy 9-stage pipeline with 7 canonical stages
(enquiry → qualified → eoi → reservation → deposit_paid → contract →
nurturing) plus three doc sub-status columns (eoi_doc_status,
reservation_doc_status, contract_doc_status) that track sent/signed
within a single stage instead of branching it.
Schema (migration 0062):
- interests gains assigned_to, deposit_expected_amount/currency,
three doc-status columns, two documenso-id columns, and
date_reservation_signed.
- New tables: qualification_criteria (per-port admin-configurable),
interest_qualifications (per-interest state), payments (deposit /
balance / refund records keyed to interest + client).
- Default qualification criteria seeded for every existing port.
- Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into
the new stage + doc-status + outcome shape.
Migration 0063 adds interest_contact_log.voice_transcript and
template_used columns for v1.1-A/B (quick-template buttons + voice
transcription via Web Speech API).
v1.1 phase work bundled here:
- A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on
the contact-log compose dialog (useVoiceTranscription hook).
- C: berth-rules-engine wraps state writes in pg_advisory_xact_lock
with an idempotent re-read; emits rule_evaluated audit traces.
- D: Documenso webhook: reservation/contract sub-status stamping
moved out of the PDF-download try-block so a download failure
no longer swallows the stamp. New integration test coverage.
- E: /admin/qualification-criteria CRUD page + admin component.
- F: default_new_interest_owner exposed in System Settings.
- G: recentActivityCount + active_engagement deal-pulse signal
surfaced as a chip on interests + hot-deals card.
- H: interest_assigned notification on assignedTo change (skips
self-assign, uses a dedupe key).
Plus the supporting components: AssignedToChip, DealPulseChip,
PaymentsSection, QualificationChecklist, MultiEoiChip,
SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner,
SupplementalInfoRequestButton, UserPicker.
Tests: 1370/1370 vitest pass (added deal-health unit suite +
expanded constants/validators/pipeline-transitions coverage). tsc
clean, eslint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
|
|
|
id="payment-date"
|
|
|
|
|
value={receivedAt}
|
2026-05-21 17:10:02 +02:00
|
|
|
onChange={setReceivedAt}
|
|
|
|
|
toDate={new Date()}
|
|
|
|
|
placeholder="Pick a date"
|
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave
Replaces the legacy 9-stage pipeline with 7 canonical stages
(enquiry → qualified → eoi → reservation → deposit_paid → contract →
nurturing) plus three doc sub-status columns (eoi_doc_status,
reservation_doc_status, contract_doc_status) that track sent/signed
within a single stage instead of branching it.
Schema (migration 0062):
- interests gains assigned_to, deposit_expected_amount/currency,
three doc-status columns, two documenso-id columns, and
date_reservation_signed.
- New tables: qualification_criteria (per-port admin-configurable),
interest_qualifications (per-interest state), payments (deposit /
balance / refund records keyed to interest + client).
- Default qualification criteria seeded for every existing port.
- Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into
the new stage + doc-status + outcome shape.
Migration 0063 adds interest_contact_log.voice_transcript and
template_used columns for v1.1-A/B (quick-template buttons + voice
transcription via Web Speech API).
v1.1 phase work bundled here:
- A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on
the contact-log compose dialog (useVoiceTranscription hook).
- C: berth-rules-engine wraps state writes in pg_advisory_xact_lock
with an idempotent re-read; emits rule_evaluated audit traces.
- D: Documenso webhook: reservation/contract sub-status stamping
moved out of the PDF-download try-block so a download failure
no longer swallows the stamp. New integration test coverage.
- E: /admin/qualification-criteria CRUD page + admin component.
- F: default_new_interest_owner exposed in System Settings.
- G: recentActivityCount + active_engagement deal-pulse signal
surfaced as a chip on interests + hot-deals card.
- H: interest_assigned notification on assignedTo change (skips
self-assign, uses a dedupe key).
Plus the supporting components: AssignedToChip, DealPulseChip,
PaymentsSection, QualificationChecklist, MultiEoiChip,
SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner,
SupplementalInfoRequestButton, UserPicker.
Tests: 1370/1370 vitest pass (added deal-health unit suite +
expanded constants/validators/pipeline-transitions coverage). tsc
clean, eslint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<Label htmlFor="payment-notes">Notes (optional)</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="payment-notes"
|
|
|
|
|
placeholder="Reference, payer name, etc."
|
|
|
|
|
value={notes}
|
|
|
|
|
onChange={(e) => setNotes(e.target.value)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<label className="flex items-start gap-2 rounded border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-900">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={acknowledgedNoReceipt}
|
|
|
|
|
onChange={(e) => setAcknowledgedNoReceipt(e.target.checked)}
|
|
|
|
|
className="mt-0.5"
|
|
|
|
|
/>
|
|
|
|
|
<span>
|
|
|
|
|
I understand that recording a payment without an attached receipt may make later
|
|
|
|
|
verification harder, and that the bank-issued receipt is the canonical proof.
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
<SheetFooter className="gap-2">
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => onOpenChange(false)}
|
|
|
|
|
disabled={mutation.isPending}
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
<Button type="submit" disabled={!canSubmit}>
|
|
|
|
|
{mutation.isPending ? 'Saving…' : 'Record payment'}
|
|
|
|
|
</Button>
|
|
|
|
|
</SheetFooter>
|
|
|
|
|
</form>
|
|
|
|
|
</SheetContent>
|
|
|
|
|
</Sheet>
|
|
|
|
|
);
|
|
|
|
|
}
|