Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,009 changes: 920 additions & 89 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"csv-stringify": "^6.7.0",
"cypress": "^15.14.2",
"del": "^8.0.1",
"exceljs": "^4.4.0",
"firebase": "^12.13.0",
"gulp": "^5.0.1",
"gulp-env": "^0.4.0",
Expand All @@ -58,6 +59,7 @@
"immutability-helper": "^3.1.1",
"jest": "^30.4.2",
"jest-environment-jsdom": "^30.4.1",
"jszip": "^3.10.1",
"moment": "^2.30.1",
"moment-timezone": "^0.6.2",
"pdfmake": "^0.3.8",
Expand Down
45 changes: 45 additions & 0 deletions src/components/ReportForm/FormatDropdown.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react';
import { renderWithTheme, screen, fireEvent } from '../../../test/renderWithTheme';
import FormatDropdown from './FormatDropdown';

jest.mock('react-i18next', () => ({
useTranslation: () => ({ t: key => key }),
withTranslation: () => Component => Component,
}));

jest.mock('scroll-into-view', () => jest.fn());

describe('FormatDropdown', () => {
it('renders an input element', () => {
renderWithTheme(<FormatDropdown onChange={jest.fn()} />);
expect(screen.getByRole('textbox')).toBeInTheDocument();
});

it('offers the pdf and excel options when focused', () => {
renderWithTheme(<FormatDropdown onChange={jest.fn()} />);
fireEvent.focus(screen.getByRole('textbox'));
expect(screen.getByText('invoicesReport.formatPdf')).toBeInTheDocument();
expect(screen.getByText('invoicesReport.formatExcel')).toBeInTheDocument();
});

it('calls onChange with the selected format key', () => {
const onChange = jest.fn();
renderWithTheme(<FormatDropdown onChange={onChange} />);
fireEvent.focus(screen.getByRole('textbox'));
fireEvent.mouseDown(screen.getByText('invoicesReport.formatExcel'));
expect(onChange).toHaveBeenCalledWith('excel');
});

it('renders the current value', () => {
renderWithTheme(<FormatDropdown value="excel" onChange={jest.fn()} />);
expect(screen.getByRole('textbox')).toHaveValue('invoicesReport.formatExcel');
});

it('does not throw when no onChange is given', () => {
renderWithTheme(<FormatDropdown />);
fireEvent.focus(screen.getByRole('textbox'));
expect(() =>
fireEvent.mouseDown(screen.getByText('invoicesReport.formatPdf'))
).not.toThrow();
});
});
57 changes: 57 additions & 0 deletions src/components/ReportForm/FormatDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import PropTypes from 'prop-types';
import React from 'react';
import styled from 'styled-components';
import Dropdown from '../Dropdown';
import { useTranslation } from 'react-i18next';

const Option = styled.div`
padding: 0.2em;
`;

const filterOptions = (options, filter) =>
options.filter(option => option.label.toUpperCase().indexOf(filter.toUpperCase()) > -1);

const renderOption = option => <Option>{option.label}</Option>;

const renderValue = option => option.label;

const handleChange = (onChange, value) => {
if (typeof onChange === 'function') {
onChange(value);
}
};

const FormatDropdown = props => {
const { t } = useTranslation();
const options = [{
key: 'pdf',
label: t('invoicesReport.formatPdf')
}, {
key: 'excel',
label: t('invoicesReport.formatExcel')
}];
return (
<Dropdown
className={props.className}
options={options}
value={props.value}
onChange={handleChange.bind(null, props.onChange)}
readOnly={props.readOnly}
optionFilter={filterOptions}
optionRenderer={renderOption}
valueRenderer={renderValue}
optionsRenderLimit={options.length}
noOptionsText={t('dropdown.notFound')}
mustSelect
/>
);
};

FormatDropdown.propTypes = {
className: PropTypes.string,
value: PropTypes.string,
onChange: PropTypes.func,
readOnly: PropTypes.bool
};

export default FormatDropdown;
40 changes: 40 additions & 0 deletions src/components/ReportForm/ReportForm.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,22 @@ jest.mock('./DelimiterDropdown', () => {
};
});

jest.mock('./FormatDropdown', () => {
const React = require('react');
return function MockFormatDropdown({ value, onChange }) {
return (
<select
data-testid="format-dropdown"
value={value}
onChange={e => onChange(e.target.value)}
>
<option value="pdf">pdf</option>
<option value="excel">excel</option>
</select>
);
};
});

import ReportForm from './ReportForm';

const baseProps = {
Expand Down Expand Up @@ -124,6 +140,30 @@ describe('ReportForm', () => {
expect(screen.getByTestId('delimiter-dropdown')).toBeInTheDocument();
});

it('does not render format dropdown by default', () => {
renderWithTheme(<ReportForm {...baseProps} />);
expect(screen.queryByTestId('format-dropdown')).not.toBeInTheDocument();
});

it('renders format dropdown when withFormat is true', () => {
renderWithTheme(<ReportForm {...baseProps} withFormat={true} setFormat={jest.fn()} />);
expect(screen.getByTestId('format-dropdown')).toBeInTheDocument();
});

it('passes the format value through to the dropdown', () => {
renderWithTheme(
<ReportForm {...baseProps} withFormat={true} format="excel" setFormat={jest.fn()} />
);
expect(screen.getByTestId('format-dropdown')).toHaveValue('excel');
});

it('calls setFormat when the format dropdown changes', () => {
const setFormat = jest.fn();
renderWithTheme(<ReportForm {...baseProps} withFormat={true} setFormat={setFormat} />);
fireEvent.change(screen.getByTestId('format-dropdown'), { target: { value: 'excel' } });
expect(setFormat).toHaveBeenCalledWith('excel');
});

it('does not render delimiter dropdown when withDelimiter is false', () => {
renderWithTheme(<ReportForm {...baseProps} withDelimiter={false} />);
expect(screen.queryByTestId('delimiter-dropdown')).not.toBeInTheDocument();
Expand Down
18 changes: 18 additions & 0 deletions src/components/ReportForm/ReportForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Button from '../Button';
import LabeledComponent from '../LabeledComponent';
import MonthDropdown from '../MonthDropdown';
import DelimiterDropdown from './DelimiterDropdown';
import FormatDropdown from './FormatDropdown';

const StyledLabeledComponent = styled(LabeledComponent)`
width: 50%;
Expand Down Expand Up @@ -52,21 +53,27 @@ const ReportForm = ({
children,
date,
delimiter = ',',
format = 'pdf',
withMonth = true,
withDelimiter = true,
withFormat = false,
setDate,
setDelimiter,
setFormat,
generate,
parameters,
}: {
disabled?: boolean;
children?: React.ReactNode;
date?: { year?: number | null; month?: number | null };
delimiter?: string;
format?: string;
withMonth?: boolean;
withDelimiter?: boolean;
withFormat?: boolean;
setDate: (date: any) => void;
setDelimiter?: (delimiter: string) => void;
setFormat?: (format: string) => void;
generate: (date: any, parameters: any) => void;
parameters?: any;
}) => {
Expand Down Expand Up @@ -95,6 +102,13 @@ const ReportForm = ({
/>
)

const formatInput = (
<FormatDropdown
value={format}
onChange={setFormat}
/>
)

return (
<form
className="ReportForm"
Expand All @@ -104,6 +118,7 @@ const ReportForm = ({
<StyledLabeledComponent label={t('report.year')} component={yearInput}/>
{withMonth && <StyledLabeledComponent label={t('report.month')} component={monthInput}/>}
{withDelimiter && <StyledLabeledComponent label={t('report.delimiter')} component={delimiterInput}/>}
{withFormat && <StyledLabeledComponent label={t('report.format')} component={formatInput}/>}
{children}
<Button
type="submit"
Expand All @@ -124,10 +139,13 @@ ReportForm.propTypes = {
month: PropTypes.number,
}),
delimiter: PropTypes.string,
format: PropTypes.string,
withMonth: PropTypes.bool,
withDelimiter: PropTypes.bool,
withFormat: PropTypes.bool,
setDate: PropTypes.func.isRequired,
setDelimiter: PropTypes.func,
setFormat: PropTypes.func,
generate: PropTypes.func.isRequired,
};

Expand Down
10 changes: 9 additions & 1 deletion src/containers/InvoicesReportFormContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useEffect } from 'react';
import {connect} from 'react-redux';
import {generateReport, initReport, setReportDate} from '../modules/reports';
import {generateReport, initReport, setReportDate, setReportParameter} from '../modules/reports';
import ReportForm from '../components/ReportForm';
import {RootState} from '../modules';

Expand All @@ -12,9 +12,11 @@ interface ReportDate {
interface Props {
initialized: boolean;
date?: ReportDate;
format?: string;
generationInProgress?: boolean;
initReport: () => void;
setDate: (date: ReportDate) => void;
setFormat: (format: string) => void;
generate: () => void;
}

Expand All @@ -28,9 +30,12 @@ const InvoicesReportFormContainer = (props: Props) => {
<ReportForm
disabled={!props.initialized || props.generationInProgress}
date={props.date}
format={props.format}
setDate={props.setDate}
setFormat={props.setFormat}
generate={props.generate}
withDelimiter={false}
withFormat
/>
);
};
Expand All @@ -47,13 +52,16 @@ const mapStateToProps = (state: RootState) => {
return {
initialized,
date: report.date,
format: report.parameters.format || 'pdf',
generationInProgress: report.generationInProgress === true,
};
};

const mapDispatchToProps = (dispatch: any) => ({
initReport: () => dispatch(initReport('invoices')),
setDate: (date: any) => dispatch(setReportDate('invoices', date)),
setFormat: (format: string) =>
dispatch(setReportParameter('invoices', 'format', format)),
generate: () => dispatch(generateReport('invoices')),
});

Expand Down
51 changes: 47 additions & 4 deletions src/containers/containers.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,17 @@ jest.mock('../components/YearlySummaryReportForm', () => ({
default: () => <div data-testid="yearly-summary-report-form" />,
}));

jest.mock('../components/ReportForm', () => ({
__esModule: true,
default: () => <div data-testid="report-form" />,
}));
// A jest.fn() wrapper (rather than a plain stub) so the invoices format
// wiring test below can inspect the props InvoicesReportFormContainer passes
// down and drive its setFormat callback directly.
jest.mock('../components/ReportForm', () => {
const React = require('react');
const mockReportForm = jest.fn((_props: any) => React.createElement('div', {'data-testid': 'report-form'}));
return {
__esModule: true,
default: mockReportForm,
};
});

jest.mock('../components/AirstatReportForm', () => ({
__esModule: true,
Expand Down Expand Up @@ -239,6 +246,42 @@ describe('container mount dispatches', () => {
expect(inits[0].payload.name).toBe('invoices');
});

it('InvoicesReportFormContainer defaults format to pdf when no parameter is set', () => {
const store = makeStore({ reports: { invoices: { date: {}, parameters: {} } } });
render(wrap(store, <InvoicesReportFormContainer />));
const ReportForm = require('../components/ReportForm').default;
const lastProps = ReportForm.mock.calls[ReportForm.mock.calls.length - 1][0];
expect(lastProps.format).toBe('pdf');
expect(lastProps.withFormat).toBe(true);
});

it('InvoicesReportFormContainer passes through a format already in state', () => {
const store = makeStore({
reports: { invoices: { date: {}, parameters: { format: 'excel' } } },
});
render(wrap(store, <InvoicesReportFormContainer />));
const ReportForm = require('../components/ReportForm').default;
const lastProps = ReportForm.mock.calls[ReportForm.mock.calls.length - 1][0];
expect(lastProps.format).toBe('excel');
});

it('InvoicesReportFormContainer dispatches SET_REPORT_PARAMETER when the format is changed', () => {
const store = makeStore({ reports: { invoices: { date: {}, parameters: {} } } });
render(wrap(store, <InvoicesReportFormContainer />));
const ReportForm = require('../components/ReportForm').default;
const lastProps = ReportForm.mock.calls[ReportForm.mock.calls.length - 1][0];

lastProps.setFormat('excel');

const paramActions = store.actions.filter(a => a.type === 'SET_REPORT_PARAMETER');
expect(paramActions).toHaveLength(1);
expect(paramActions[0].payload).toEqual({
report: 'invoices',
parameterName: 'format',
parameterValue: 'excel',
});
});

it('AerodromeStatusBannerContainer dispatches WATCH_CURRENT_AERODROME_STATUS exactly once on mount', () => {
const store = makeStore({
settings: {
Expand Down
10 changes: 7 additions & 3 deletions src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@
"adminExport": {
"bazlReport": "BAZL-Report herunterladen (CSV)",
"landingList": "Landeliste herunterladen (CSV)",
"invoiceReports": "Rechnungsberichte herunterladen (PDF)",
"invoiceReports": "Rechnungsberichte herunterladen",
"yearlySummary": "Jahreszusammenfassung herunterladen (CSV)"
},
"adminAircraft": {
Expand Down Expand Up @@ -432,7 +432,8 @@
"report": {
"year": "Jahr",
"month": "Monat",
"delimiter": "Trennzeichen"
"delimiter": "Trennzeichen",
"format": "Format"
},
"yearlySummary": {
"intro": "Die Jahreszusammenfassung enthält für jeden Monat des Jahres eine Zeile mit den folgenden Informationen:",
Expand Down Expand Up @@ -602,6 +603,9 @@
"colEmail": "E-Mail",
"colDirection": "Einflug / Ausflug",
"directionArrival": "Einflug",
"directionDeparture": "Ausflug"
"directionDeparture": "Ausflug",
"formatPdf": "PDF",
"formatExcel": "Excel",
"sheetNameFallback": "Rechnungsempfänger"
}
}
Loading
Loading