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>
<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
type="number"
min={0}
@@ -1160,7 +1160,23 @@ export function ProjectWizard({ open, onClose }: ProjectWizardProps) {
handleClose();
}, 1200);
} 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 {
setIsSubmitting(false);
}
@@ -203,6 +203,7 @@ export interface TimelineContextValue {
// ─ Loading
isLoading: boolean;
isInitialLoading: boolean;
isEntriesError: boolean;
totalAllocCount: number;
activeFilterCount: number;
@@ -328,6 +329,7 @@ export function TimelineProvider({
) as {
data: TimelineEntriesView | undefined;
isLoading: boolean;
isError: boolean;
refetch: () => Promise<unknown>;
};
@@ -344,11 +346,12 @@ export function TimelineProvider({
) as {
data: TimelineEntriesView | undefined;
isLoading: boolean;
isError: boolean;
refetch: () => Promise<unknown>;
};
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 demands = entriesView?.demands ?? [];
@@ -774,6 +777,7 @@ export function TimelineProvider({
blinkOverbookedDays,
isLoading,
isInitialLoading,
isEntriesError,
totalAllocCount,
activeFilterCount,
}),
@@ -800,6 +804,7 @@ export function TimelineProvider({
blinkOverbookedDays,
isLoading,
isInitialLoading,
isEntriesError,
totalAllocCount,
activeFilterCount,
],
@@ -332,6 +332,7 @@ function TimelineViewContent({
today,
isLoading,
isInitialLoading,
isEntriesError,
totalAllocCount,
} = ctx;
@@ -708,7 +709,11 @@ function TimelineViewContent({
onScroll={handleContainerScroll}
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">
Loading timeline...
</div>