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:
2026-05-14 23:42:27 +02:00
parent 3e78c2d4ab
commit 84468386d9
6 changed files with 97 additions and 17 deletions

View File

@@ -5,6 +5,7 @@ import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Loader2, ChevronsUpDown, Check, Plus } from 'lucide-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
@@ -85,6 +86,9 @@ interface InterestFormProps {
export function InterestForm({ open, onOpenChange, defaultClientId, interest }: InterestFormProps) {
const queryClient = useQueryClient();
const router = useRouter();
const params = useParams<{ portSlug: string }>();
const portSlug = params?.portSlug ?? '';
const isEdit = !!interest;
const [clientOpen, setClientOpen] = useState(false);
@@ -220,13 +224,23 @@ export function InterestForm({ open, onOpenChange, defaultClientId, interest }:
body: { tagIds: tIds },
});
}
} else {
await apiFetch('/api/v1/interests', { method: 'POST', body: enriched });
return { id: interest!.id, created: false };
}
const res = await apiFetch<{ data: { id: string } }>('/api/v1/interests', {
method: 'POST',
body: enriched,
});
return { id: res.data.id, created: true };
},
onSuccess: () => {
onSuccess: (result) => {
queryClient.invalidateQueries({ queryKey: ['interests'] });
onOpenChange(false);
// F20: navigate to the new interest's detail page so the rep can
// start the workflow immediately. Edits stay in place — no point
// re-loading the same row's detail page they just came from.
if (result.created && portSlug) {
router.push(`/${portSlug}/interests/${result.id}` as never);
}
},
});