mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 05:56:53 +00:00
fix(bookings): make the reservation import findable on the desktop panel
The import is not missing from the desktop — it sits at the far right of the bookings toolbar, next to the accent "Manual Booking", above an empty page with nothing but the mascot on it. On a fresh trip that is exactly where nobody looks, which is how it reads as absent. The empty state now offers the ways in that apply: add manually, import from a file, and AirTrail where it is connected. EmptyState grows an optional action slot for it, so every other caller is untouched. Closes #2007
This commit is contained in:
@@ -66,15 +66,16 @@ describe('ReservationsPanel', () => {
|
||||
|
||||
it('FE-COMP-RES-005: shows Manual Booking add button', () => {
|
||||
render(<ReservationsPanel {...defaultProps} />);
|
||||
// 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(<ReservationsPanel {...defaultProps} onAdd={onAdd} />);
|
||||
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(<ReservationsPanel {...defaultProps} />);
|
||||
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 })); });
|
||||
|
||||
@@ -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 */}
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: '24px 28px 80px' }} className="max-md:!px-4 max-md:!pt-4">
|
||||
{total === 0 && reservations.length === 0 ? (
|
||||
<EmptyState scene={contributionView === 'transports' ? 'transport' : 'bookings'} title={t('reservations.empty')} />
|
||||
<EmptyState
|
||||
scene={contributionView === 'transports' ? 'transport' : 'bookings'}
|
||||
title={t('reservations.empty')}
|
||||
// On a fresh trip the toolbar controls sit far right above an empty
|
||||
// page, so the import in particular went unnoticed (#2007).
|
||||
action={canEdit ? (
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', justifyContent: 'center' }}>
|
||||
<button onClick={onAdd} className="bg-accent text-accent-text" style={CTA_STYLE}>
|
||||
<Plus size={14} strokeWidth={2} />
|
||||
{t(addManualKey)}
|
||||
</button>
|
||||
{onImport && bookingImportAvailable && (
|
||||
<button onClick={onImport} className="bg-surface-card text-content" style={{ ...CTA_STYLE, border: '1px solid var(--border-primary)' }}>
|
||||
<Download size={14} strokeWidth={2} />
|
||||
{t('reservations.import.cta')}
|
||||
</button>
|
||||
)}
|
||||
{onAirTrailImport && airTrailAvailable && (
|
||||
<button onClick={onAirTrailImport} className="bg-surface-secondary text-content" style={{ ...CTA_STYLE, border: '1px solid var(--border-primary)' }}>
|
||||
<Plane size={14} strokeWidth={2} />
|
||||
{t('reservations.airtrail.cta')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : undefined}
|
||||
/>
|
||||
) : total === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '60px 20px' }}>
|
||||
<p className="text-content-faint" style={{ fontSize: 'calc(13px * var(--fs-scale-body, 1))' }}>{t('places.noneFound')}</p>
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
@@ -33,6 +36,7 @@ export default function EmptyState({
|
||||
>
|
||||
<MDancingTrek scene={scene} mood={mood} size={size} />
|
||||
<p className="text-[15px] font-semibold text-content-secondary">{title}</p>
|
||||
{action}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user