fix(ux): T4 polish wave — empty-contact filter, redirect-on-create, friendly stage errors
F19: client form drops empty-value contacts on submit; auto-promotes first remaining row to primary if none flagged. F20: new-interest dialog redirects to the detail page on create instead of bouncing back to the list. F21: stage-transition validation errors render with STAGE_LABELS — "Yacht is required before leaving the Enquiry stage." (was "yachtId is required before leaving stage=enquiry"). F22: blocked-stage marker swapped from the ⚑ unicode glyph to a Lucide AlertTriangle with aria-label. F25: documents-hub folder selection moves to ?folder=<id> querystring so deep-link / browser-back / refresh round-trip the current folder. F26: reopen-outcome action now toasts "Outcome cleared — interest is open again." F27: stage PATCH where target === current short-circuits to a no-op return; downstream callers don't see a phantom stage_change audit row. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -832,13 +832,22 @@ export async function changeInterestStage(
|
||||
throw new NotFoundError('Interest');
|
||||
}
|
||||
|
||||
// F27: same-stage write is a no-op. Return the existing row without
|
||||
// bumping updatedAt or emitting an audit log entry — pre-fix every
|
||||
// re-submit (e.g. accidental double-click) wrote a "Same → Same"
|
||||
// audit entry and triggered downstream invalidations.
|
||||
if (existing.pipelineStage === data.pipelineStage) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
// Plan: yachtId required to leave the initial enquiry stage
|
||||
if (
|
||||
existing.pipelineStage === 'enquiry' &&
|
||||
data.pipelineStage !== 'enquiry' &&
|
||||
!existing.yachtId
|
||||
) {
|
||||
throw new ValidationError('yachtId is required before leaving stage=enquiry');
|
||||
// F21: user-readable; was "yachtId is required before leaving stage=enquiry"
|
||||
throw new ValidationError('A yacht must be linked before leaving the Enquiry stage.');
|
||||
}
|
||||
|
||||
// Block egregious skips. The transition table allows reasonable forward
|
||||
@@ -848,8 +857,9 @@ export async function changeInterestStage(
|
||||
// gates this on the `interests.override_stage` permission and requires
|
||||
// a reason, recorded in the audit log below.
|
||||
if (!data.override && !canTransitionStage(existing.pipelineStage, data.pipelineStage)) {
|
||||
// F21: use the human-readable stage labels in error copy.
|
||||
throw new ValidationError(
|
||||
`Cannot move interest from "${existing.pipelineStage}" directly to "${data.pipelineStage}". Use the override option if you need to skip stages — requires a reason.`,
|
||||
`Cannot move interest from "${STAGE_LABELS[existing.pipelineStage as PipelineStage] ?? existing.pipelineStage}" directly to "${STAGE_LABELS[data.pipelineStage as PipelineStage] ?? data.pipelineStage}". Use the override option if you need to skip stages — requires a reason.`,
|
||||
);
|
||||
}
|
||||
if (data.override && (!data.reason || data.reason.trim().length < 5)) {
|
||||
|
||||
Reference in New Issue
Block a user