feat(wizard-refactor): drop inapp pathway + upload branch + per-port template defaults + mark-signed dropdown
Phase 2 of the comprehensive UAT round. Locked decisions from the
2026-05-26 question round (see docs/superpowers/audits/active-uat.md
"Decisions locked" block).
P2.1 — drop the inapp template pathway
Removed the dead pathway dropdown. Generate-from-template flow is
now exclusively documenso-template; the inapp (pdf-lib + CRM-render)
branch was never surfaced as a deliberate choice and was a config
trap. Server-side route still accepts pathway='inapp' for backcompat
with older clients - wizard now always sends 'documenso-template'.
P2.2 — delete the wizard's upload branch
Reps who want to upload a finished PDF go through the New-document
dropdown -> "Upload & send for signature" (UploadForSigningDialog,
the proper field-placement flow) instead of the wizard's
half-implemented upload sub-form. Wizard's Source section becomes
a one-line explainer + the template picker; no more redundant
radio-then-pathway-then-template layering.
P2.3 — per-port doc-type template defaults
New GET /api/v1/documents/template-defaults endpoint returns
{ eoi, contract, reservation_agreement } template ids from
getPortDocumensoConfig. Settings registry keys already existed for
contract + reservation; config + resolver already plumbed them.
CreateDocumentWizard now fetches the map on mount and auto-sets
templateId whenever documentType changes (empty picker OR currently
showing a different doc-type's default both get re-aligned). Admin
override via the picker still works.
P2.4 — surface flow 3 (mark signed offline) from the dropdown
NewDocumentMenu gains a 4th item: "Mark as signed (offline)".
Opens a small dialog that asks for the interest + doc type
(eoi/reservation/contract), then navigates to the matching
per-interest tab with ?tab=...&action=upload-signed query param.
Per-interest tabs are the single source of truth for the
pipeline-stage + doc-status side effects of the mark-signed flow;
the hub-level dropdown just routes the rep to the right place.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
43
src/app/api/v1/documents/template-defaults/route.ts
Normal file
43
src/app/api/v1/documents/template-defaults/route.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
import { withAuth, withPermission } from '@/lib/api/helpers';
|
||||||
|
import { errorResponse } from '@/lib/errors';
|
||||||
|
import { getPortDocumensoConfig } from '@/lib/services/port-config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET `/api/v1/documents/template-defaults`
|
||||||
|
*
|
||||||
|
* Returns the per-port default Documenso template id keyed by
|
||||||
|
* documentType. The CreateDocumentWizard reads this to auto-resolve
|
||||||
|
* the template the rep doesn't have to remember — picking "EOI" /
|
||||||
|
* "Reservation Agreement" / "Contract" defaults to the matching port
|
||||||
|
* template id. Admins with the explicit perm can still override via
|
||||||
|
* the DocumentTemplatePicker.
|
||||||
|
*
|
||||||
|
* Permission: documents.create — the only caller is the wizard which
|
||||||
|
* already requires this permission to complete the flow. View-only
|
||||||
|
* roles don't see the wizard at all.
|
||||||
|
*
|
||||||
|
* Response:
|
||||||
|
* { data: { eoi: number | null, contract: number | null,
|
||||||
|
* reservation_agreement: number | null } }
|
||||||
|
*
|
||||||
|
* `null` means no template configured for that doc type (rep must
|
||||||
|
* pick one manually via the override picker).
|
||||||
|
*/
|
||||||
|
export const GET = withAuth(
|
||||||
|
withPermission('documents', 'create', async (_req, ctx) => {
|
||||||
|
try {
|
||||||
|
const cfg = await getPortDocumensoConfig(ctx.portId);
|
||||||
|
return NextResponse.json({
|
||||||
|
data: {
|
||||||
|
eoi: cfg.eoiTemplateId > 0 ? cfg.eoiTemplateId : null,
|
||||||
|
contract: cfg.contractTemplateId,
|
||||||
|
reservation_agreement: cfg.reservationTemplateId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -22,7 +22,6 @@ import { CompanyPicker } from '@/components/companies/company-picker';
|
|||||||
import { YachtPicker } from '@/components/yachts/yacht-picker';
|
import { YachtPicker } from '@/components/yachts/yacht-picker';
|
||||||
import { InterestPicker } from '@/components/interests/interest-picker';
|
import { InterestPicker } from '@/components/interests/interest-picker';
|
||||||
import { DocumentTemplatePicker } from '@/components/documents/document-template-picker';
|
import { DocumentTemplatePicker } from '@/components/documents/document-template-picker';
|
||||||
import { FileUploadZone } from '@/components/files/file-upload-zone';
|
|
||||||
import { apiFetch } from '@/lib/api/client';
|
import { apiFetch } from '@/lib/api/client';
|
||||||
import { toastError } from '@/lib/api/toast-error';
|
import { toastError } from '@/lib/api/toast-error';
|
||||||
import { DOCUMENT_TYPES, DOCUMENT_TYPE_LABELS } from '@/lib/constants';
|
import { DOCUMENT_TYPES, DOCUMENT_TYPE_LABELS } from '@/lib/constants';
|
||||||
@@ -65,12 +64,14 @@ interface CreateDocumentWizardProps {
|
|||||||
export function CreateDocumentWizard({ portSlug }: CreateDocumentWizardProps) {
|
export function CreateDocumentWizard({ portSlug }: CreateDocumentWizardProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [source, setSource] = useState<'template' | 'upload'>('template');
|
// Wizard is generation-only — the upload branch was removed per the
|
||||||
const [pathway, setPathway] = useState<'documenso-template' | 'inapp' | 'upload'>(
|
// 2026-05-26 wizard-refactor decision. Reps who want to upload a
|
||||||
'documenso-template',
|
// finished PDF go through the New-document dropdown → "Upload & send
|
||||||
);
|
// for signature" (UploadForSigningDialog) or → "Mark as signed
|
||||||
|
// offline" (ExternalEoiUploadDialog) instead. The `pathway` state is
|
||||||
|
// also gone: `inapp` was a dead CRM-rendered branch that no UI ever
|
||||||
|
// surfaced as a deliberate choice; everything flows through Documenso.
|
||||||
const [templateId, setTemplateId] = useState('');
|
const [templateId, setTemplateId] = useState('');
|
||||||
const [uploadedFileId, setUploadedFileId] = useState('');
|
|
||||||
const [documentType, setDocumentType] = useState<(typeof DOCUMENT_TYPES)[number]>('eoi');
|
const [documentType, setDocumentType] = useState<(typeof DOCUMENT_TYPES)[number]>('eoi');
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('');
|
||||||
const [notes, setNotes] = useState('');
|
const [notes, setNotes] = useState('');
|
||||||
@@ -78,6 +79,42 @@ export function CreateDocumentWizard({ portSlug }: CreateDocumentWizardProps) {
|
|||||||
const [subjectType, setSubjectType] = useState<(typeof SUBJECT_TYPES)[number]['key']>('interest');
|
const [subjectType, setSubjectType] = useState<(typeof SUBJECT_TYPES)[number]['key']>('interest');
|
||||||
const [subjectId, setSubjectId] = useState('');
|
const [subjectId, setSubjectId] = useState('');
|
||||||
|
|
||||||
|
// Per-port template defaults keyed by documentType. Loaded once on
|
||||||
|
// mount; whenever documentType changes (or this map lands after the
|
||||||
|
// first paint) we auto-populate templateId so the rep doesn't have to
|
||||||
|
// know "which template did the admin set for EOI on this port?". The
|
||||||
|
// rep can still override via the picker — auto-fill only writes when
|
||||||
|
// templateId is currently empty OR matches a different doc-type's
|
||||||
|
// default (so they don't get stuck on the wrong template after
|
||||||
|
// switching doc types).
|
||||||
|
const [templateDefaults, setTemplateDefaults] = useState<Record<string, number | null>>({});
|
||||||
|
useEffect(() => {
|
||||||
|
void apiFetch<{
|
||||||
|
data: { eoi: number | null; contract: number | null; reservation_agreement: number | null };
|
||||||
|
}>('/api/v1/documents/template-defaults')
|
||||||
|
.then((res) => setTemplateDefaults(res.data ?? {}))
|
||||||
|
.catch(() => setTemplateDefaults({}));
|
||||||
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
const fallbackId = templateDefaults[documentType];
|
||||||
|
if (fallbackId == null) return;
|
||||||
|
const allTemplateDefaultIds = new Set(
|
||||||
|
Object.values(templateDefaults).filter((v): v is number => v != null),
|
||||||
|
);
|
||||||
|
// Set when picker is empty OR when current pick is a different
|
||||||
|
// doc-type's default (rep switched doc types — auto-realign). This
|
||||||
|
// is the canonical controlled-state-sync pattern: external input
|
||||||
|
// (doc-type selection) drives a dependent field (template id)
|
||||||
|
// whose default the rep can still override via the picker. Pure
|
||||||
|
// useMemo derivation isn't an option because the picker also writes
|
||||||
|
// templateId on explicit user input.
|
||||||
|
|
||||||
|
if (!templateId || allTemplateDefaultIds.has(Number(templateId))) {
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
|
setTemplateId(String(fallbackId));
|
||||||
|
}
|
||||||
|
}, [documentType, templateDefaults, templateId]);
|
||||||
|
|
||||||
// Watchers picked at create-time. Each selected user gets an in-app
|
// Watchers picked at create-time. Each selected user gets an in-app
|
||||||
// notification on every signing event (opened, signed, declined,
|
// notification on every signing event (opened, signed, declined,
|
||||||
// completed) once the document is created. Same surface the
|
// completed) once the document is created. Same surface the
|
||||||
@@ -107,15 +144,6 @@ export function CreateDocumentWizard({ portSlug }: CreateDocumentWizardProps) {
|
|||||||
|
|
||||||
const subjectField = SUBJECT_TYPES.find((s) => s.key === subjectType)!.field;
|
const subjectField = SUBJECT_TYPES.find((s) => s.key === subjectType)!.field;
|
||||||
|
|
||||||
const setSourceAndPathway = (next: 'template' | 'upload'): void => {
|
|
||||||
setSource(next);
|
|
||||||
if (next === 'upload') {
|
|
||||||
setPathway('upload');
|
|
||||||
} else if (pathway === 'upload') {
|
|
||||||
setPathway('documenso-template');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateSigner = (idx: number, patch: Partial<SignerRow>): void => {
|
const updateSigner = (idx: number, patch: Partial<SignerRow>): void => {
|
||||||
setSigners((current) => current.map((s, i) => (i === idx ? { ...s, ...patch } : s)));
|
setSigners((current) => current.map((s, i) => (i === idx ? { ...s, ...patch } : s)));
|
||||||
};
|
};
|
||||||
@@ -147,25 +175,24 @@ export function CreateDocumentWizard({ portSlug }: CreateDocumentWizardProps) {
|
|||||||
toast.error(`Provide a ${subjectType} id`);
|
toast.error(`Provide a ${subjectType} id`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (source === 'template' && !templateId.trim()) {
|
if (!templateId.trim()) {
|
||||||
toast.error('Pick a template');
|
toast.error('Pick a template');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (source === 'upload' && !uploadedFileId.trim()) {
|
|
||||||
toast.error('Provide an uploaded file id');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const cleanSigners = signers.filter((s) => s.signerEmail.trim() && s.signerName.trim());
|
const cleanSigners = signers.filter((s) => s.signerEmail.trim() && s.signerName.trim());
|
||||||
if (source === 'upload' && cleanSigners.length === 0) {
|
|
||||||
toast.error('Upload path requires at least one signer');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
|
// Wizard is generation-only: source is always 'template', pathway
|
||||||
|
// is always 'documenso-template'. The upload branch + inapp
|
||||||
|
// pathway were removed per the 2026-05-26 wizard-refactor
|
||||||
|
// decision. The server-side wizard route still accepts both
|
||||||
|
// fields for backcompat with older clients; we pass the fixed
|
||||||
|
// values explicitly so the API contract stays stable while the
|
||||||
|
// UI surface narrows.
|
||||||
const body: Record<string, unknown> = {
|
const body: Record<string, unknown> = {
|
||||||
source,
|
source: 'template',
|
||||||
pathway,
|
pathway: 'documenso-template',
|
||||||
documentType,
|
documentType,
|
||||||
title: title.trim(),
|
title: title.trim(),
|
||||||
notes: notes.trim() || undefined,
|
notes: notes.trim() || undefined,
|
||||||
@@ -175,13 +202,10 @@ export function CreateDocumentWizard({ portSlug }: CreateDocumentWizardProps) {
|
|||||||
autoPlaceFields: true,
|
autoPlaceFields: true,
|
||||||
sendImmediately: false,
|
sendImmediately: false,
|
||||||
remindersDisabled: reminderMode === 'disabled',
|
remindersDisabled: reminderMode === 'disabled',
|
||||||
|
templateId: templateId.trim(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (source === 'template') body.templateId = templateId.trim();
|
if (cleanSigners.length > 0) {
|
||||||
if (source === 'upload') {
|
|
||||||
body.uploadedFileId = uploadedFileId.trim();
|
|
||||||
body.signers = cleanSigners;
|
|
||||||
} else if (cleanSigners.length > 0) {
|
|
||||||
body.signers = cleanSigners;
|
body.signers = cleanSigners;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,79 +242,22 @@ export function CreateDocumentWizard({ portSlug }: CreateDocumentWizardProps) {
|
|||||||
<div className="grid gap-4 lg:grid-cols-2">
|
<div className="grid gap-4 lg:grid-cols-2">
|
||||||
<section className="rounded-md border bg-white p-4">
|
<section className="rounded-md border bg-white p-4">
|
||||||
<h2 className="mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
<h2 className="mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
||||||
Source
|
Template
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex flex-col gap-3 text-sm">
|
<div className="flex flex-col gap-3 text-sm">
|
||||||
<label className="flex items-center gap-2">
|
<p className="text-xs text-muted-foreground">
|
||||||
<input
|
Generates a Documenso envelope from a stored template. To upload a finished PDF and
|
||||||
type="radio"
|
place fields yourself, use <strong>New document</strong> -> Upload & send for
|
||||||
checked={source === 'template'}
|
signature instead. To record a paper-signed document, use{' '}
|
||||||
onChange={() => setSourceAndPathway('template')}
|
<strong>New document</strong> -> Mark as signed offline.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label className="text-xs">Template</Label>
|
||||||
|
<DocumentTemplatePicker
|
||||||
|
value={templateId || null}
|
||||||
|
onChange={(id) => setTemplateId(id ?? '')}
|
||||||
/>
|
/>
|
||||||
<span>Generate from a template</span>
|
</div>
|
||||||
</label>
|
|
||||||
<label className="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
checked={source === 'upload'}
|
|
||||||
onChange={() => setSourceAndPathway('upload')}
|
|
||||||
/>
|
|
||||||
<span>Upload a finished PDF</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{source === 'template' ? (
|
|
||||||
<>
|
|
||||||
<div className="mt-2 flex flex-col gap-2">
|
|
||||||
<Label className="text-xs">Pathway</Label>
|
|
||||||
<Select value={pathway} onValueChange={(v) => setPathway(v as typeof pathway)}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="documenso-template">
|
|
||||||
Generated EOI - rendered + signed externally
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="inapp">
|
|
||||||
Manual EOI - rendered in CRM, sent for e-signature
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<Label className="text-xs">Template</Label>
|
|
||||||
<DocumentTemplatePicker
|
|
||||||
value={templateId || null}
|
|
||||||
onChange={(id) => setTemplateId(id ?? '')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<Label className="text-xs">Upload PDF</Label>
|
|
||||||
{uploadedFileId ? (
|
|
||||||
<div className="flex items-center justify-between rounded-md border bg-muted/30 px-3 py-2 text-xs">
|
|
||||||
<span className="truncate">File ready (id: {uploadedFileId.slice(0, 8)}…)</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setUploadedFileId('')}
|
|
||||||
className="text-muted-foreground hover:text-destructive"
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<FileUploadZone
|
|
||||||
onUploadComplete={(file) => {
|
|
||||||
if (file?.id) setUploadedFileId(file.id);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Drop a PDF or click to browse. The file is stored, then the wizard wires it as the
|
|
||||||
source for signing.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,16 @@
|
|||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { ChevronDown, FileSignature, Pen, Plus, Upload } from 'lucide-react';
|
import { CheckCircle2, ChevronDown, FileSignature, Pen, Plus, Upload } from 'lucide-react';
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
@@ -19,7 +21,10 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||||
import { FileUploadZone } from '@/components/files/file-upload-zone';
|
import { FileUploadZone } from '@/components/files/file-upload-zone';
|
||||||
|
import { InterestPicker } from '@/components/interests/interest-picker';
|
||||||
import { UploadForSigningDialog } from '@/components/documents/upload-for-signing-dialog';
|
import { UploadForSigningDialog } from '@/components/documents/upload-for-signing-dialog';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -57,7 +62,13 @@ export function NewDocumentMenu({
|
|||||||
}: NewDocumentMenuProps) {
|
}: NewDocumentMenuProps) {
|
||||||
const [uploadOpen, setUploadOpen] = useState(false);
|
const [uploadOpen, setUploadOpen] = useState(false);
|
||||||
const [uploadForSigningOpen, setUploadForSigningOpen] = useState(false);
|
const [uploadForSigningOpen, setUploadForSigningOpen] = useState(false);
|
||||||
|
const [markSignedOpen, setMarkSignedOpen] = useState(false);
|
||||||
|
const [markSignedInterestId, setMarkSignedInterestId] = useState<string | null>(null);
|
||||||
|
const [markSignedDocType, setMarkSignedDocType] = useState<'eoi' | 'reservation' | 'contract'>(
|
||||||
|
'eoi',
|
||||||
|
);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -99,9 +110,83 @@ export function NewDocumentMenu({
|
|||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onSelect={() => setMarkSignedOpen(true)} className="gap-2 py-2.5">
|
||||||
|
<CheckCircle2 className="h-4 w-4" aria-hidden />
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span>Mark as signed (offline)</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
Already paper-signed - record without going through Documenso
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
|
{/* Mark-as-signed picker dialog. Reps choose interest + doc type;
|
||||||
|
we navigate to the matching per-interest tab with an
|
||||||
|
action=upload-signed query param so the existing tab's own
|
||||||
|
ExternalEoi / external-reservation / external-contract upload
|
||||||
|
dialog handles the file capture + the pipeline transition.
|
||||||
|
This keeps a single source of truth for the per-type side
|
||||||
|
effects (eoiStatus / reservationDocStatus / contractDocStatus
|
||||||
|
flips, stage advance, audit log) instead of recreating them
|
||||||
|
at the hub level. */}
|
||||||
|
<Dialog open={markSignedOpen} onOpenChange={setMarkSignedOpen}>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Mark as signed (offline)</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Pick the deal + document type. We'll open the relevant tab where you can upload
|
||||||
|
the signed copy (or just mark it signed without a file).
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4 py-2">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-xs">Interest / deal</Label>
|
||||||
|
<InterestPicker
|
||||||
|
value={markSignedInterestId}
|
||||||
|
onChange={(id) => setMarkSignedInterestId(id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-xs">Document type</Label>
|
||||||
|
<RadioGroup
|
||||||
|
value={markSignedDocType}
|
||||||
|
onValueChange={(v) => setMarkSignedDocType(v as 'eoi' | 'reservation' | 'contract')}
|
||||||
|
className="flex flex-col gap-2"
|
||||||
|
>
|
||||||
|
<Label className="flex cursor-pointer items-center gap-2 text-sm">
|
||||||
|
<RadioGroupItem value="eoi" /> EOI
|
||||||
|
</Label>
|
||||||
|
<Label className="flex cursor-pointer items-center gap-2 text-sm">
|
||||||
|
<RadioGroupItem value="reservation" /> Reservation Agreement
|
||||||
|
</Label>
|
||||||
|
<Label className="flex cursor-pointer items-center gap-2 text-sm">
|
||||||
|
<RadioGroupItem value="contract" /> Contract
|
||||||
|
</Label>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setMarkSignedOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={!markSignedInterestId}
|
||||||
|
onClick={() => {
|
||||||
|
if (!markSignedInterestId) return;
|
||||||
|
router.push(
|
||||||
|
`/${portSlug}/interests/${markSignedInterestId}?tab=${markSignedDocType}&action=upload-signed` as never,
|
||||||
|
);
|
||||||
|
setMarkSignedOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Open
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<Dialog open={uploadOpen} onOpenChange={setUploadOpen}>
|
<Dialog open={uploadOpen} onOpenChange={setUploadOpen}>
|
||||||
<DialogContent className="sm:max-w-2xl">
|
<DialogContent className="sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
|
|||||||
Reference in New Issue
Block a user