fix: resolve 3 UX bugs from gitlooper ticket sweep (#45 #47 #48)

- #47: Remove misleading asterisk from Budget (EUR) label in project
  wizard — budget is optional per canGoNext() logic
- #48: Parse Zod validation JSON in wizard submit error handler so users
  see "Responsible person is required" instead of raw JSON array
- #45: Expose isEntriesError from timeline query context; TimelineView
  now renders an explicit error message instead of a silent empty canvas
  when the getEntriesView query fails

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-04-03 11:25:42 +02:00
parent 339ae47540
commit 5a8dc6c166
3 changed files with 30 additions and 4 deletions
@@ -423,7 +423,7 @@ function Step2({ state, onChange }: Step2Props) {
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div> <div>
<label className={LABEL_CLS}>Budget (EUR) *<InfoTooltip content="Total project budget in EUR. Stored internally as cents. Used to track spending against assignments." /></label> <label className={LABEL_CLS}>Budget (EUR)<InfoTooltip content="Total project budget in EUR. Stored internally as cents. Used to track spending against assignments." /></label>
<input <input
type="number" type="number"
min={0} min={0}
@@ -1160,7 +1160,23 @@ export function ProjectWizard({ open, onClose }: ProjectWizardProps) {
handleClose(); handleClose();
}, 1200); }, 1200);
} catch (err) { } catch (err) {
setSubmitError(err instanceof Error ? err.message : "Failed to create project"); let errorMessage = "Failed to create project";
if (err instanceof Error) {
try {
const parsed: unknown = JSON.parse(err.message);
if (Array.isArray(parsed) && parsed.length > 0) {
errorMessage = (parsed as { message?: string }[])
.map((e) => e.message)
.filter(Boolean)
.join("; ");
} else {
errorMessage = err.message;
}
} catch {
errorMessage = err.message;
}
}
setSubmitError(errorMessage);
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
@@ -203,6 +203,7 @@ export interface TimelineContextValue {
// ─ Loading // ─ Loading
isLoading: boolean; isLoading: boolean;
isInitialLoading: boolean; isInitialLoading: boolean;
isEntriesError: boolean;
totalAllocCount: number; totalAllocCount: number;
activeFilterCount: number; activeFilterCount: number;
@@ -328,6 +329,7 @@ export function TimelineProvider({
) as { ) as {
data: TimelineEntriesView | undefined; data: TimelineEntriesView | undefined;
isLoading: boolean; isLoading: boolean;
isError: boolean;
refetch: () => Promise<unknown>; refetch: () => Promise<unknown>;
}; };
@@ -344,11 +346,12 @@ export function TimelineProvider({
) as { ) as {
data: TimelineEntriesView | undefined; data: TimelineEntriesView | undefined;
isLoading: boolean; isLoading: boolean;
isError: boolean;
refetch: () => Promise<unknown>; refetch: () => Promise<unknown>;
}; };
const entriesViewQuery = isSelfServiceTimeline ? selfEntriesViewQuery : staffEntriesViewQuery; const entriesViewQuery = isSelfServiceTimeline ? selfEntriesViewQuery : staffEntriesViewQuery;
const { data: entriesView, isLoading, refetch: refetchEntriesView } = entriesViewQuery; const { data: entriesView, isLoading, isError: isEntriesError, refetch: refetchEntriesView } = entriesViewQuery;
const assignments = entriesView?.assignments ?? []; const assignments = entriesView?.assignments ?? [];
const demands = entriesView?.demands ?? []; const demands = entriesView?.demands ?? [];
@@ -774,6 +777,7 @@ export function TimelineProvider({
blinkOverbookedDays, blinkOverbookedDays,
isLoading, isLoading,
isInitialLoading, isInitialLoading,
isEntriesError,
totalAllocCount, totalAllocCount,
activeFilterCount, activeFilterCount,
}), }),
@@ -800,6 +804,7 @@ export function TimelineProvider({
blinkOverbookedDays, blinkOverbookedDays,
isLoading, isLoading,
isInitialLoading, isInitialLoading,
isEntriesError,
totalAllocCount, totalAllocCount,
activeFilterCount, activeFilterCount,
], ],
@@ -332,6 +332,7 @@ function TimelineViewContent({
today, today,
isLoading, isLoading,
isInitialLoading, isInitialLoading,
isEntriesError,
totalAllocCount, totalAllocCount,
} = ctx; } = ctx;
@@ -708,7 +709,11 @@ function TimelineViewContent({
onScroll={handleContainerScroll} onScroll={handleContainerScroll}
className="app-surface relative z-0 flex-1 overflow-auto" className="app-surface relative z-0 flex-1 overflow-auto"
> >
{isInitialLoading ? ( {isEntriesError ? (
<div className="flex flex-col items-center justify-center gap-3 py-24 text-sm text-red-600 dark:text-red-400">
<span>Failed to load timeline data. Please try refreshing the page.</span>
</div>
) : isInitialLoading ? (
<div className="flex items-center justify-center py-24 text-sm text-gray-500 dark:text-gray-400"> <div className="flex items-center justify-center py-24 text-sm text-gray-500 dark:text-gray-400">
Loading timeline... Loading timeline...
</div> </div>