diff --git a/client/src/components/Planner/BookingNotesFilesCosts.test.tsx b/client/src/components/Planner/BookingNotesFilesCosts.test.tsx new file mode 100644 index 000000000..2e17b2c39 --- /dev/null +++ b/client/src/components/Planner/BookingNotesFilesCosts.test.tsx @@ -0,0 +1,102 @@ +// FE-PLANNER-NOTESFILESCOSTS-001 to FE-PLANNER-NOTESFILESCOSTS-003 +import { createRef, type ComponentProps } from 'react'; +import { describe, expect, it, vi } from 'vitest'; + +import { fireEvent, render, screen } from '../../../tests/helpers/render'; +import { BookingNotesFilesCosts } from './BookingNotesFilesCosts'; + +const linkAndFiles = vi.fn(); +const costs = vi.fn(); + +vi.mock('./BookingLinkAndFiles', () => ({ + BookingLinkAndFiles: (props: Record) => { + linkAndFiles(props); + return
; + }, +})); + +vi.mock('./BookingCostsSection', () => ({ + BookingCostsSection: (props: Record) => { + costs(props); + return
; + }, +})); + +type Props = ComponentProps; + +function setup(overrides: Partial = {}) { + linkAndFiles.mockClear(); + costs.mockClear(); + const props: Props = { + notes: 'Gate B', + onNotesChange: vi.fn(), + url: 'https://example.com', + onUrlChange: vi.fn(), + reservationId: 9, + tripFiles: [], + pendingFiles: [], + fileInputRef: createRef(), + attach: { + uploadingFile: false, + handleFileChange: vi.fn(), + attachedFiles: [], + removePending: vi.fn(), + linkFile: vi.fn(), + detachFile: vi.fn(), + }, + canAttach: true, + showCosts: true, + pendingExpense: null, + expense: { reset: vi.fn(), take: vi.fn(), create: vi.fn(), edit: vi.fn(), remove: vi.fn() }, + ...overrides, + }; + render(); + return props; +} + +describe('BookingNotesFilesCosts', () => { + it('FE-PLANNER-NOTESFILESCOSTS-001: shows the notes and reports an edit', () => { + const props = setup(); + + const area = screen.getByPlaceholderText('Additional notes...'); + expect(area).toHaveValue('Gate B'); + fireEvent.change(area, { target: { value: 'Gate C' } }); + expect(props.onNotesChange).toHaveBeenCalledWith('Gate C'); + }); + + it('FE-PLANNER-NOTESFILESCOSTS-002: hands the link, the files and the costs their parts of the booking', () => { + const props = setup(); + + expect(linkAndFiles).toHaveBeenCalledWith( + expect.objectContaining({ + url: 'https://example.com', + onUrlChange: props.onUrlChange, + reservationId: 9, + canAttach: true, + uploading: false, + onFileChange: props.attach.handleFileChange, + onRemovePending: props.attach.removePending, + onLinked: props.attach.linkFile, + onDetached: props.attach.detachFile, + }) + ); + expect(costs).toHaveBeenCalledWith( + expect.objectContaining({ + reservationId: 9, + pendingExpense: null, + onCreate: props.expense.create, + onEdit: props.expense.edit, + onRemove: props.expense.remove, + customTooltips: true, + }) + ); + }); + + it('FE-PLANNER-NOTESFILESCOSTS-003: leaves the costs out without the budget, and passes no id for a new booking', () => { + setup({ showCosts: false, reservationId: undefined }); + + expect(screen.getByTestId('link-and-files')).toBeInTheDocument(); + expect(screen.queryByTestId('costs')).toBeNull(); + expect(linkAndFiles).toHaveBeenCalledWith(expect.objectContaining({ reservationId: undefined })); + }); +}); diff --git a/client/src/components/Planner/BookingNotesFilesCosts.tsx b/client/src/components/Planner/BookingNotesFilesCosts.tsx new file mode 100644 index 000000000..4ce102205 --- /dev/null +++ b/client/src/components/Planner/BookingNotesFilesCosts.tsx @@ -0,0 +1,90 @@ +import type { RefObject } from 'react'; + +import { useTranslation } from '../../i18n'; +import type { TripFile } from '../../types'; +import { EditorField, INPUT, LABEL, TEXTAREA } from '../shared/dialogParts'; +import { BookingCostsSection } from './BookingCostsSection'; +import { BookingLinkAndFiles } from './BookingLinkAndFiles'; +import type { useBookingExpenseIntent } from './useBookingExpenseIntent'; +import type { useBookingFileAttach } from './useBookingFileAttach'; + +/** + * The lower part the booking and transport dialogs share: notes, the link and + * files row, and the costs linked to this booking when the budget addon is on. + */ +export function BookingNotesFilesCosts({ + notes, + onNotesChange, + url, + onUrlChange, + reservationId, + tripFiles, + pendingFiles, + fileInputRef, + attach, + canAttach, + showCosts, + pendingExpense, + expense, +}: { + notes: string; + onNotesChange: (notes: string) => void; + url: string; + onUrlChange: (url: string) => void; + /** The saved booking; unset while it is being created. */ + reservationId: number | undefined; + tripFiles: TripFile[]; + pendingFiles: File[]; + fileInputRef: RefObject; + attach: ReturnType; + canAttach: boolean; + showCosts: boolean; + pendingExpense: { total_price: number; currency?: string | null; category: string } | null; + expense: ReturnType; +}) { + const { t } = useTranslation(); + return ( + <> + +