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

@@ -333,12 +333,12 @@ export function InlineStagePicker({
) : isCurrent ? (
<Check className="size-3.5 text-muted-foreground" aria-hidden />
) : isOverride && canOverride ? (
<span
className="text-[10px] uppercase tracking-wide text-amber-600"
title="Override required"
>
</span>
// F22: was ⚑ unicode glyph — replaced with a Lucide
// icon to match the rest of the visual system.
<AlertTriangle
className="size-3.5 text-amber-600"
aria-label="Override required"
/>
) : null}
</button>
</li>

View File

@@ -2,6 +2,7 @@
import { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import {
Pencil,
Archive,
@@ -144,6 +145,9 @@ export function InterestDetailHeader({ portSlug, interest }: InterestDetailHeade
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['interests', interest.id] });
queryClient.invalidateQueries({ queryKey: ['interests'] });
// F26: confirm to the user that the action ran — pre-fix the
// button gave no feedback and reps weren't sure if it took.
toast.success('Outcome cleared — interest is open again.');
},
});

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);
}
},
});