diff --git a/client/src/components/Planner/ReservationsPanel.test.tsx b/client/src/components/Planner/ReservationsPanel.test.tsx index 054db8410..279e41183 100644 --- a/client/src/components/Planner/ReservationsPanel.test.tsx +++ b/client/src/components/Planner/ReservationsPanel.test.tsx @@ -66,15 +66,16 @@ describe('ReservationsPanel', () => { it('FE-COMP-RES-005: shows Manual Booking add button', () => { render(); - // Button text is reservations.addManual = "Manual Booking" - expect(screen.getByText('Manual Booking')).toBeInTheDocument(); + // Button text is reservations.addManual = "Manual Booking" — in the toolbar + // and, on an empty list, in the empty state's call to action too (#2007). + expect(screen.getAllByText('Manual Booking').length).toBeGreaterThan(0); }); it('FE-COMP-RES-006: clicking Manual Booking button calls onAdd', async () => { const user = userEvent.setup(); const onAdd = vi.fn(); render(); - await user.click(screen.getByText('Manual Booking')); + await user.click(screen.getAllByText('Manual Booking')[0]); expect(onAdd).toHaveBeenCalled(); }); @@ -654,7 +655,7 @@ describe('ReservationsPanel', () => { it('FE-PLANNER-RESP-058: the add button dims on hover and restores on leave', () => { render(); - const add = screen.getByText('Manual Booking').closest('button') as HTMLButtonElement; + const add = screen.getAllByText('Manual Booking')[0].closest('button') as HTMLButtonElement; act(() => { add.dispatchEvent(new MouseEvent('mouseover', { bubbles: true })); }); expect(add.style.opacity).toBe('0.88'); act(() => { add.dispatchEvent(new MouseEvent('mouseout', { bubbles: true })); }); diff --git a/client/src/components/Planner/ReservationsPanel.tsx b/client/src/components/Planner/ReservationsPanel.tsx index fe97ad203..29f217f4d 100644 --- a/client/src/components/Planner/ReservationsPanel.tsx +++ b/client/src/components/Planner/ReservationsPanel.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useEffect } from 'react' +import { useState, useMemo, useEffect, type CSSProperties } from 'react' import { createPortal } from 'react-dom' import { useTripStore } from '../../store/tripStore' import { useCanDo } from '../../store/permissionsStore' @@ -705,6 +705,14 @@ interface ReservationsPanelProps { tripMembers?: TripMember[] } +/** The empty state's call-to-action buttons — same shape as the toolbar's. */ +const CTA_STYLE: CSSProperties = { + appearance: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit', + display: 'inline-flex', alignItems: 'center', gap: 6, + padding: '9px 14px', borderRadius: 10, + fontSize: 'calc(13px * var(--fs-scale-body, 1))', fontWeight: 500, +} + export default function ReservationsPanel({ tripId, reservations, days, assignments, files = [], onAdd, onImport, bookingImportAvailable, onAirTrailImport, airTrailAvailable, onEdit, onDelete, onNavigateToFiles, titleKey = 'reservations.title', addManualKey = 'reservations.addManual', contributionView = 'reservations', tripMembers = [] }: ReservationsPanelProps) { const { t, locale } = useTranslation() const can = useCanDo() @@ -928,7 +936,32 @@ export default function ReservationsPanel({ tripId, reservations, days, assignme {/* Content */}
{total === 0 && reservations.length === 0 ? ( - + + + {onImport && bookingImportAvailable && ( + + )} + {onAirTrailImport && airTrailAvailable && ( + + )} +
+ ) : undefined} + /> ) : total === 0 ? (

{t('places.noneFound')}

diff --git a/client/src/components/shared/EmptyState.tsx b/client/src/components/shared/EmptyState.tsx index 53d2133e6..35b838e90 100644 --- a/client/src/components/shared/EmptyState.tsx +++ b/client/src/components/shared/EmptyState.tsx @@ -1,4 +1,4 @@ -import type { CSSProperties } from 'react' +import type { CSSProperties, ReactNode } from 'react' import MDancingTrek, { type TrekScene, type TrekMood } from '../../mobile/components/MDancingTrek' /** @@ -18,6 +18,7 @@ export default function EmptyState({ size = 104, surface = 'var(--bg-card)', className = '', + action, }: { scene?: TrekScene mood?: TrekMood @@ -25,6 +26,8 @@ export default function EmptyState({ size?: number surface?: string className?: string + /** Optional call to action under the title, for states that have an obvious next step. */ + action?: ReactNode }) { return (

{title}

+ {action}
) }