feat(eoi-generate): Include-yacht toggle to omit Section 3 when yacht is a placeholder
EoiGenerateDialog gains an inline "Include on EOI" checkbox in the Section 3 header (renders only when ctx.yacht is set; defaults ON so existing behaviour is unchanged). When OFF, the generate-and-sign POST flips includeYachtDetails=false on the body; service blanks eoiContext.yacht before either pathway runs: - Documenso template payload: buildDocumensoPayload reads no yacht so yacht.* and owner.* merge fields ship empty. Existing template tolerates blanks per the "left blank if absent" copy. - In-app PDF fill (pdf-lib): generateEoiPdfFromTemplate sees no yacht so AcroForm field writes for the yacht block are skipped. Persists the rep's choice in the document-create audit log (metadata.includeYachtDetails) so an audit trail records explicit opt-outs even though documents has no JSONB metadata column today. ft/m unit toggle in the Section 3 header now hides when Include is OFF (unit choice is meaningless without yacht details). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -191,6 +191,13 @@ export function EoiGenerateDialog({
|
||||
const [addressOverride, setAddressOverride] = useState<AddressOverrideState | null>(null);
|
||||
// Phase 3c - yacht spawn flow.
|
||||
const [yachtSpawnOpen, setYachtSpawnOpen] = useState(false);
|
||||
// Section 3 (yacht details) inclusion toggle. Defaults to ON; the toggle
|
||||
// only renders when a yacht is actually linked (no toggle = always ON,
|
||||
// which matches today's behaviour). Set OFF to blank yacht.* + owner.*
|
||||
// merge fields on the generated PDF even though the yacht record exists
|
||||
// (early-stage clients, multi-berth deals where yacht-dims don't apply,
|
||||
// explicit client request to keep Section 3 off the document).
|
||||
const [includeYachtDetails, setIncludeYachtDetails] = useState(true);
|
||||
|
||||
// Resolved EOI context - the actual values the document will be
|
||||
// auto-filled with. Loaded only while the dialog is open so we don't
|
||||
@@ -564,6 +571,9 @@ export function EoiGenerateDialog({
|
||||
// the yacht's own `lengthUnit` column when unspecified.
|
||||
dimensionUnit: effectiveDimensionUnit,
|
||||
...(hasAnyOverride ? { overrides } : {}),
|
||||
// Only send the flag when the rep explicitly opted out; default
|
||||
// (toggle ON) keeps existing behaviour without bloating the body.
|
||||
...(ctx?.yacht && !includeYachtDetails ? { includeYachtDetails: false } : {}),
|
||||
},
|
||||
});
|
||||
// Bounce every cache that surfaces the interest's EOI state so the
|
||||
@@ -679,41 +689,60 @@ export function EoiGenerateDialog({
|
||||
</dl>
|
||||
</div>
|
||||
<div className="space-y-1 border-t pt-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
Optional (Section 3 - left blank if absent)
|
||||
Section 3 - yacht details
|
||||
</p>
|
||||
{ctx.yacht ? (
|
||||
<div className="inline-flex rounded-md border bg-muted/30 p-0.5 text-[11px]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDimensionUnit('ft')}
|
||||
className={
|
||||
'rounded px-2 py-0.5 transition-colors ' +
|
||||
(effectiveDimensionUnit === 'ft'
|
||||
? 'bg-background font-medium shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground')
|
||||
}
|
||||
aria-pressed={effectiveDimensionUnit === 'ft'}
|
||||
>
|
||||
ft
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDimensionUnit('m')}
|
||||
className={
|
||||
'rounded px-2 py-0.5 transition-colors ' +
|
||||
(effectiveDimensionUnit === 'm'
|
||||
? 'bg-background font-medium shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground')
|
||||
}
|
||||
aria-pressed={effectiveDimensionUnit === 'm'}
|
||||
>
|
||||
m
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex items-center gap-2">
|
||||
{ctx.yacht ? (
|
||||
<label className="inline-flex cursor-pointer items-center gap-1.5 text-[11px] text-muted-foreground">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={includeYachtDetails}
|
||||
onChange={(e) => setIncludeYachtDetails(e.target.checked)}
|
||||
className="h-3.5 w-3.5 cursor-pointer accent-primary"
|
||||
aria-label="Include yacht details on the EOI"
|
||||
/>
|
||||
<span>Include on EOI</span>
|
||||
</label>
|
||||
) : null}
|
||||
{ctx.yacht && includeYachtDetails ? (
|
||||
<div className="inline-flex rounded-md border bg-muted/30 p-0.5 text-[11px]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDimensionUnit('ft')}
|
||||
className={
|
||||
'rounded px-2 py-0.5 transition-colors ' +
|
||||
(effectiveDimensionUnit === 'ft'
|
||||
? 'bg-background font-medium shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground')
|
||||
}
|
||||
aria-pressed={effectiveDimensionUnit === 'ft'}
|
||||
>
|
||||
ft
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDimensionUnit('m')}
|
||||
className={
|
||||
'rounded px-2 py-0.5 transition-colors ' +
|
||||
(effectiveDimensionUnit === 'm'
|
||||
? 'bg-background font-medium shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground')
|
||||
}
|
||||
aria-pressed={effectiveDimensionUnit === 'm'}
|
||||
>
|
||||
m
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{ctx.yacht && !includeYachtDetails ? (
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
Yacht details will be omitted from the EOI even though a yacht is linked.
|
||||
</p>
|
||||
) : null}
|
||||
<dl className="space-y-1.5">
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="flex-1">
|
||||
|
||||
Reference in New Issue
Block a user