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:
@@ -176,9 +176,30 @@ export function ClientForm({
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (data: CreateClientInput) => {
|
||||
// F19: drop contact rows whose value is empty/whitespace before
|
||||
// submitting. The form pre-adds an "empty primary" contact row
|
||||
// for convenience; reps who only want to record a name shouldn't
|
||||
// be forced to either fill it or delete it.
|
||||
const cleanedContacts = (data.contacts ?? []).filter(
|
||||
(c) => typeof c.value === 'string' && c.value.trim().length > 0,
|
||||
);
|
||||
if (cleanedContacts.length === 0) {
|
||||
// The API still requires ≥1 contact. The form-level required
|
||||
// marker on the email input also fires HTML5 validation; this
|
||||
// is the fall-back if the rep wiped the value after focus.
|
||||
throw Object.assign(new Error('At least one contact is required.'), { status: 400 });
|
||||
}
|
||||
// If none of the remaining contacts is flagged primary, promote
|
||||
// the first one — guards against a rep removing the originally-
|
||||
// primary row and leaving an orphan set.
|
||||
if (!cleanedContacts.some((c) => c.isPrimary)) {
|
||||
cleanedContacts[0]!.isPrimary = true;
|
||||
}
|
||||
const payload: CreateClientInput = { ...data, contacts: cleanedContacts };
|
||||
|
||||
if (isEdit) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { contacts, tagIds: tIds, ...rest } = data;
|
||||
const { contacts, tagIds: tIds, ...rest } = payload;
|
||||
await apiFetch(`/api/v1/clients/${client!.id}`, { method: 'PATCH', body: rest });
|
||||
if (tIds) {
|
||||
await apiFetch(`/api/v1/clients/${client!.id}/tags`, {
|
||||
@@ -187,7 +208,7 @@ export function ClientForm({
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await apiFetch('/api/v1/clients', { method: 'POST', body: data });
|
||||
await apiFetch('/api/v1/clients', { method: 'POST', body: payload });
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
|
||||
Reference in New Issue
Block a user