diff --git a/pro/src/app/App/layout/Layout.module.scss b/pro/src/app/App/layout/Layout.module.scss index 9ff4964fa64..d23a72f35a5 100644 --- a/pro/src/app/App/layout/Layout.module.scss +++ b/pro/src/app/App/layout/Layout.module.scss @@ -81,7 +81,7 @@ $connect-as-header-height: rem.torem(52px); .content-container { width: 100%; flex-grow: 1; - padding: rem.torem(24px) rem.torem(24px) 0; + padding: size.$main-content-padding-xs size.$main-content-padding-xs 0; background-color: var(--color-white); display: flex; flex-direction: column; diff --git a/pro/src/commons/config/swrQueryKeys.ts b/pro/src/commons/config/swrQueryKeys.ts index 185e959a9c9..580b8c2539f 100644 --- a/pro/src/commons/config/swrQueryKeys.ts +++ b/pro/src/commons/config/swrQueryKeys.ts @@ -57,3 +57,4 @@ export const GET_VENUE_TYPES_QUERY_KEY = 'getVenueTypes' export const GET_VENUES_QUERY_KEY = 'getVenues' export const LOG_CATALOG_VIEW_QUERY_KEY = 'logCatalogView' export const LOG_TRACKING_FILTER_QUERY_KEY = 'logTrackingFilter' +export const GET_STATISTICS_QUERY_KEY = 'getStatistics' diff --git a/pro/src/commons/utils/factories/statisticsFactories.ts b/pro/src/commons/utils/factories/statisticsFactories.ts new file mode 100644 index 00000000000..5920899e39f --- /dev/null +++ b/pro/src/commons/utils/factories/statisticsFactories.ts @@ -0,0 +1,46 @@ +import { StatisticsModel } from "apiClient/v1"; + +export const statisticsFactory = ({ + emptyYear = '', + individualRevenueOnlyYear = '', + collectiveRevenueOnlyYear = '', + collectiveAndIndividualRevenueYear = '2024', +}): StatisticsModel => { + const incomeByYear = { + ...(emptyYear && {[emptyYear]: {}}), + ...(individualRevenueOnlyYear && { + [individualRevenueOnlyYear]: { + revenue: { + individual: 1000, + }, + expectedRevenue: { + individual: 2000, + } + } + }), + ...(collectiveRevenueOnlyYear && { + [collectiveRevenueOnlyYear]: { + revenue: { + collective: 3000, + }, + expectedRevenue: { + collective: 4000, + }, + } + }), + [collectiveAndIndividualRevenueYear]: { + revenue: { + total: 11_000, + individual: 5000, + collective: 6000, + }, + expectedRevenue: { + total: 15_000, + individual: 7000, + collective: 8000, + }, + }, + } + + return { incomeByYear } +} \ No newline at end of file diff --git a/pro/src/pages/Reimbursements/Income/Income.module.scss b/pro/src/pages/Reimbursements/Income/Income.module.scss index 2b6799d645c..f4f7732c1b3 100644 --- a/pro/src/pages/Reimbursements/Income/Income.module.scss +++ b/pro/src/pages/Reimbursements/Income/Income.module.scss @@ -25,6 +25,29 @@ } } + &-by-venue { + max-width: rem.torem(300px); + + // Selected tags must be displayed to occupy available width. + // Width must be calculated based on the viewport width minus + // various padding values since they belong to a component that + // is not a direct child of the main content container and + // has a restricted width. + &-selected-tags { + width: calc(100vw - size.$main-content-padding-xs * 2); + + @media (min-width: size.$mobile) { + width: calc(100vw - (size.$main-content-padding) * 2) + } + + @media (min-width: size.$laptop) { + // On very large screens the layout does not exceed 90rem. + width: calc(100vw - (size.$main-content-padding * 2) - size.$side-nav-width); + max-width: calc(size.$desktop - (size.$main-content-padding * 2) - size.$side-nav-width); + } + } + } + &-by-year { display: flex; flex-flow: row wrap; @@ -37,6 +60,7 @@ align-items: end; &-is-only-filter { + height: auto; margin-left: 0; } } diff --git a/pro/src/pages/Reimbursements/Income/Income.spec.tsx b/pro/src/pages/Reimbursements/Income/Income.spec.tsx index f197e55cd2d..30ee6c7083b 100644 --- a/pro/src/pages/Reimbursements/Income/Income.spec.tsx +++ b/pro/src/pages/Reimbursements/Income/Income.spec.tsx @@ -1,23 +1,34 @@ import { screen, waitFor, - waitForElementToBeRemoved, } from '@testing-library/react' import { userEvent } from '@testing-library/user-event' import { api } from 'apiClient/api' +import type { + StatisticsModel, + GetOffererResponseModel, + GetOffererVenueResponseModel, + AggregatedRevenue, +} from 'apiClient/v1' import * as useAnalytics from 'app/App/analytics/firebase' import { defaultGetOffererResponseModel, defaultGetOffererVenueResponseModel, } from 'commons/utils/factories/individualApiFactories' +import { statisticsFactory } from 'commons/utils/factories/statisticsFactories' import { sharedCurrentUserFactory } from 'commons/utils/factories/storeFactories' import { renderWithProviders } from 'commons/utils/renderWithProviders' -import { Income, MOCK_INCOME_BY_YEAR } from './Income' -import { IncomeResults, IncomeType } from './types' +import { Income } from './Income' +import { isCollectiveAndIndividualRevenue, isCollectiveRevenue } from './utils' -const MOCK_DATA = { +const MOCK_DATA: { + selectedOffererId: number + offerer: GetOffererResponseModel & { + managedVenues: Array + } +} & StatisticsModel = { selectedOffererId: 100, offerer: { ...defaultGetOffererResponseModel, @@ -39,11 +50,18 @@ const MOCK_DATA = { }, ], }, + ...statisticsFactory({ + emptyYear: '1994', + individualRevenueOnlyYear: '1995', + collectiveRevenueOnlyYear: '1996', + collectiveAndIndividualRevenueYear: '1997', + }), } const LABELS = { error: /Erreur dans le chargement des données./, venuesSelector: /Partenaire/, + venuesSelectorError: /Vous devez sélectionner au moins un partenaire/, incomeResultsLabel: /Chiffre d’affaires total/, emptyScreen: /Vous n’avez aucune réservation/, mandatoryHelper: /\* sont obligatoires/, @@ -64,6 +82,7 @@ const renderIncome = () => { vi.mock('apiClient/api', () => ({ api: { getOfferer: vi.fn(), + getStatistics: vi.fn(), }, })) @@ -77,19 +96,20 @@ describe('Income', () => { it('should attempt to fetch venues data and display a loading spinner meanwhile', async () => { vi.spyOn(api, 'getOfferer').mockResolvedValue(MOCK_DATA.offerer) + vi.spyOn(api, 'getStatistics').mockResolvedValue({ incomeByYear: MOCK_DATA.incomeByYear }) renderIncome() - await waitForElementToBeRemoved(() => screen.queryByTestId('spinner')) - expect(api.getOfferer).toHaveBeenCalledWith(MOCK_DATA.selectedOffererId) + await waitFor(() => expect(screen.getByTestId('venues-spinner')).toBeInTheDocument()) + expect(api.getOfferer).toHaveBeenNthCalledWith(1, MOCK_DATA.selectedOffererId) }) it('should display an error message if venues couldnt be fetched', async () => { vi.spyOn(api, 'getOfferer').mockRejectedValue(new Error('error')) + vi.spyOn(api, 'getStatistics').mockResolvedValue({ incomeByYear: MOCK_DATA.incomeByYear }) renderIncome() - await waitFor(() => { - expect(screen.getByText(LABELS.error)).toBeInTheDocument() - }) + await waitFor(() => expect(screen.getByText(LABELS.error)).toBeInTheDocument()) + expect(api.getOfferer).toHaveBeenNthCalledWith(1, MOCK_DATA.selectedOffererId) }) it('should display an empty screen if no venues were found', async () => { @@ -97,113 +117,209 @@ describe('Income', () => { ...MOCK_DATA.offerer, managedVenues: [], }) + vi.spyOn(api, 'getStatistics').mockResolvedValue({ incomeByYear: MOCK_DATA.incomeByYear }) renderIncome() - await waitFor(() => { - expect(screen.getByText(LABELS.emptyScreen)).toBeInTheDocument() - }) + await waitFor(() => expect(screen.getByText(LABELS.emptyScreen)).toBeInTheDocument()) + expect(api.getOfferer).toHaveBeenNthCalledWith(1, MOCK_DATA.selectedOffererId) + }) + + it('should attempt to fetch income data with all venues and display a loading spinner meanwhile', async () => { + vi.spyOn(api, 'getOfferer').mockResolvedValue(MOCK_DATA.offerer) + vi.spyOn(api, 'getStatistics').mockResolvedValue({ incomeByYear: MOCK_DATA.incomeByYear }) + renderIncome() + + await waitFor(() => expect(screen.getByTestId('income-spinner')).toBeInTheDocument()) + expect(api.getStatistics).toHaveBeenNthCalledWith(1, MOCK_DATA.offerer.managedVenues.map((venue) => venue.id)) + }) + + it('should display an error message if income couldnt be fetched', async () => { + vi.spyOn(api, 'getOfferer').mockResolvedValue(MOCK_DATA.offerer) + vi.spyOn(api, 'getStatistics').mockRejectedValue(new Error('error')) + renderIncome() + + await waitFor(() => expect(screen.getByText(LABELS.error)).toBeInTheDocument()) }) - // TODO : https://passculture.atlassian.net/browse/PC-32278 - it('should attempt to fetch income data with all venues and display a loading spinner meanwhile', () => {}) - it('should display an error message if income couldnt be fetched', () => {}) - it('should display an empty screen if no income data was found', () => {}) + it('should display an empty screen if no income data was found', async () => { + vi.spyOn(api, 'getOfferer').mockResolvedValue(MOCK_DATA.offerer) + vi.spyOn(api, 'getStatistics').mockResolvedValue({ incomeByYear: {} }) + renderIncome() + + await waitFor(() => expect(screen.getByText(LABELS.emptyScreen)).toBeInTheDocument()) + }) - it('should display a venue selector with all venues selected by default', async () => { + it('should display an auto-focused venue selector with all venues selected by default', async () => { vi.spyOn(api, 'getOfferer').mockResolvedValue(MOCK_DATA.offerer) + vi.spyOn(api, 'getStatistics').mockResolvedValue({ incomeByYear: MOCK_DATA.incomeByYear }) renderIncome() await waitFor(() => { - expect( - screen.getByRole('combobox', { - name: LABELS.venuesSelector, - }) - ).toBeInTheDocument() - - MOCK_DATA.offerer.managedVenues.forEach((venue) => { - expect( - screen.getByRole('button', { - name: `Supprimer ${venue.name}`, - }) - ).toBeInTheDocument() - }) + expect(screen.getByRole('combobox', { + name: LABELS.venuesSelector, + })).toBeInTheDocument() }) + + // When venues are selected, delete tag buttons are displayed. + await waitFor(() => { + expect(screen.getAllByRole('button', { + name: /Supprimer/, + }).length).toBe(MOCK_DATA.offerer.managedVenues.length) + }) + + expect(document.activeElement?.id).toBe('search-selectedVenues') }) it('should not display a venue selector, nor the mandatory input helper if there is only one venue', async () => { - vi.spyOn(api, 'getOfferer').mockResolvedValue(MOCK_DATA.offerer) + vi.spyOn(api, 'getOfferer').mockResolvedValue({ + ...MOCK_DATA.offerer, + managedVenues: [MOCK_DATA.offerer.managedVenues[0]], + }) + vi.spyOn(api, 'getStatistics').mockResolvedValue({ incomeByYear: MOCK_DATA.incomeByYear }) renderIncome() + // This is a check to avoid a false positive by testing existence + // of element to prove conjoined non-existence of another. await waitFor(() => { - expect( - screen.queryByRole('combobox', { - name: LABELS.venuesSelector, - }) - ).not.toBeInTheDocument() - - expect( - screen.queryByText(LABELS.mandatoryHelper) - ).not.toBeInTheDocument() + expect(screen.getAllByRole('button', { + name: /Afficher les revenus de l'année/, + }).length).toBeGreaterThan(0) }) + + expect( + screen.queryByRole('combobox', { + name: LABELS.venuesSelector, + }) + ).not.toBeInTheDocument() + + expect( + screen.queryByText(LABELS.mandatoryHelper) + ).not.toBeInTheDocument() }) it('should display a set of year filters with the last year selected by default', async () => { vi.spyOn(api, 'getOfferer').mockResolvedValue(MOCK_DATA.offerer) + vi.spyOn(api, 'getStatistics').mockResolvedValue({ incomeByYear: MOCK_DATA.incomeByYear }) renderIncome() + const years = [...Object.keys(MOCK_DATA.incomeByYear)] await waitFor(() => { - const years = Object.keys(MOCK_INCOME_BY_YEAR) - years.forEach((year) => { - expect( - screen.getByRole('button', { - name: `Afficher les revenus de l'année ${year}`, - }) - ).toBeInTheDocument() - }) + expect(screen.getAllByRole('button', { + name: /Afficher les revenus de l'année/, + }).length).toBe(years.length) }) + + // Years are sorted in descending order, so the last/most recent year + // is the first item of the list of filters. + const mostRecentYear = years.sort((a, b) => parseInt(b) - parseInt(a))[0] + expect(screen.getByRole('button', { + name: `Afficher les revenus de l'année ${mostRecentYear}`, + })).toHaveAttribute('aria-current', 'true') }) - it('should display the income results', async () => { - vi.spyOn(api, 'getOfferer').mockResolvedValue(MOCK_DATA.offerer) + it('should auto-focus the last year filter if there is only one venue', async () => { + vi.spyOn(api, 'getOfferer').mockResolvedValue({ + ...MOCK_DATA.offerer, + managedVenues: [MOCK_DATA.offerer.managedVenues[0]], + }) + vi.spyOn(api, 'getStatistics').mockResolvedValue({ incomeByYear: MOCK_DATA.incomeByYear }) renderIncome() await waitFor(() => { - const resultTitles = screen.getAllByText(LABELS.incomeResultsLabel) - expect(resultTitles.length).toBeGreaterThan(0) + expect(screen.getAllByRole('button', { + name: /Afficher les revenus de l'année/, + }).length).toBeGreaterThan(0) }) + + const years = [...Object.keys(MOCK_DATA.incomeByYear)] + // Years are sorted in descending order, so the last/most recent year + // is the first item of the list of filters. + const mostRecentYear = years.sort((a, b) => parseInt(b) - parseInt(a))[0] + expect(screen.getByRole('button', { + name: `Afficher les revenus de l'année ${mostRecentYear}`, + })).toHaveFocus() + }) + + it('should display the income results', async () => { + vi.spyOn(api, 'getOfferer').mockResolvedValue(MOCK_DATA.offerer) + vi.spyOn(api, 'getStatistics').mockResolvedValue({ incomeByYear: MOCK_DATA.incomeByYear }) + renderIncome() + + await waitFor(() => expect(screen.getAllByText(LABELS.incomeResultsLabel).length).toBeGreaterThan(0)) }) }) describe('when the user changes venue selection', () => { beforeEach(() => { - vi.spyOn(api, 'getOfferer').mockResolvedValue(MOCK_DATA.offerer) vi.spyOn(useAnalytics, 'useAnalytics').mockImplementation(() => ({ logEvent: vi.fn(), })) }) - // TODO : https://passculture.atlassian.net/browse/PC-32278 - it('should display an error if no venues are selected', () => {}) - it('should attempt to fetch income data with the selected venues and display a loading spinner meanwhile', () => {}) + it('should display an error if no venues are selected and avoid fetching income data', async () => { + vi.spyOn(api, 'getOfferer').mockResolvedValue(MOCK_DATA.offerer) + vi.spyOn(api, 'getStatistics').mockResolvedValue({ incomeByYear: MOCK_DATA.incomeByYear }) + renderIncome() + + await waitFor(() => { + expect(screen.getByRole('combobox', { + name: LABELS.venuesSelector, + })).toBeInTheDocument() + }) + + const deleteVenueButtons = screen.getAllByRole('button', { name: /Supprimer/ }) + for (const button of deleteVenueButtons) { + await userEvent.click(button) + } + + await waitFor(() => expect(screen.getByText(LABELS.venuesSelectorError)).toBeInTheDocument(), { + timeout: 3000, + }) + + // It should not attempt to fetch income data if no venues are selected. + expect(api.getStatistics).toHaveBeenCalledTimes(1) + }) + + it('should attempt to fetch income data with the selected venues and display a loading spinner meanwhile', async () => { + vi.spyOn(api, 'getOfferer').mockResolvedValue(MOCK_DATA.offerer) + vi.spyOn(api, 'getStatistics').mockResolvedValue({ incomeByYear: MOCK_DATA.incomeByYear }) + renderIncome() + + await waitFor(() => { + expect(screen.getByRole('combobox', { + name: LABELS.venuesSelector, + })).toBeInTheDocument() + }) + + const deleteVenueButtons = screen.getAllByRole('button', { name: /Supprimer/ }) + const unselectedVenue = deleteVenueButtons[0] + await userEvent.click(unselectedVenue) + + await waitFor(() => expect(screen.getByTestId('income-spinner')).toBeInTheDocument()) + const expectedLeftVenueIds = MOCK_DATA.offerer.managedVenues + .filter(v => v.name !== unselectedVenue.textContent) + .map(v => v.id) + expect(api.getStatistics).toHaveBeenNthCalledWith(2, expectedLeftVenueIds) + }) }) describe('when the user changes year selection', () => { beforeEach(() => { - vi.spyOn(api, 'getOfferer').mockResolvedValue(MOCK_DATA.offerer) vi.spyOn(useAnalytics, 'useAnalytics').mockImplementation(() => ({ logEvent: vi.fn(), })) }) it('should display the income results for the selected year', async () => { + vi.spyOn(api, 'getOfferer').mockResolvedValue(MOCK_DATA.offerer) + vi.spyOn(api, 'getStatistics').mockResolvedValue({ incomeByYear: MOCK_DATA.incomeByYear }) renderIncome() const toFloatStr = (number: number): string => number.toString().replace('.', ',') + '€' await waitFor(() => screen.getAllByText(LABELS.incomeResultsLabel)) - const yearsWithData = Object.keys(MOCK_INCOME_BY_YEAR).filter( - (year) => - Object.keys(MOCK_INCOME_BY_YEAR[year as unknown as number]).length > 0 + const yearsWithData = Object.keys(MOCK_DATA.incomeByYear).filter( + (year) => Object.keys(MOCK_DATA.incomeByYear[year]).length > 0 ) for (const y of yearsWithData) { @@ -213,20 +329,27 @@ describe('Income', () => { }) ) - const income = MOCK_INCOME_BY_YEAR[y as unknown as number] - const incomeTypes = Object.keys(income) as IncomeType[] + const income = MOCK_DATA.incomeByYear[y] + const incomeTypes = Object.keys(income) as (keyof AggregatedRevenue)[] for (const t of incomeTypes) { await waitFor(() => { - const incomeResults = income[t] as IncomeResults - const { total, individual, group } = incomeResults - expect(screen.getByText(toFloatStr(total))).toBeInTheDocument() - if (individual && group) { + const incomeResults = income[t] + const { total, individual, collective } = incomeResults + + if (isCollectiveAndIndividualRevenue(incomeResults)) { + expect(screen.getByText(toFloatStr(total))).toBeInTheDocument() + expect( + screen.getByText(toFloatStr(individual)) + ).toBeInTheDocument() expect( - screen.getAllByText(toFloatStr(individual)).length - ).toBeGreaterThan(0) + screen.getByText(toFloatStr(collective)) + ).toBeInTheDocument() + } else if (isCollectiveRevenue(incomeResults)) { expect( - screen.getAllByText(toFloatStr(group)).length - ).toBeGreaterThan(0) + screen.getByText(toFloatStr(collective)) + ).toBeInTheDocument() + } else { + expect(screen.getByText(toFloatStr(individual))).toBeInTheDocument() } }) } @@ -234,18 +357,22 @@ describe('Income', () => { }) it('should display en empty screen if no data is available for the selected year', async () => { + vi.spyOn(api, 'getOfferer').mockResolvedValue(MOCK_DATA.offerer) + vi.spyOn(api, 'getStatistics').mockResolvedValue({ incomeByYear: MOCK_DATA.incomeByYear }) renderIncome() await waitFor(() => screen.getAllByText(LABELS.incomeResultsLabel)) - const emptyYear = 2021 + + const emptyYear = Object.keys(MOCK_DATA.incomeByYear).find( + year => Object.keys(MOCK_DATA.incomeByYear[year]).length === 0 + ) await userEvent.click( screen.getByRole('button', { name: `Afficher les revenus de l'année ${emptyYear}`, }) ) - await waitFor(() => { - expect(screen.getByText(LABELS.emptyScreen)).toBeInTheDocument() - }) + + await waitFor(() => expect(screen.getByText(LABELS.emptyScreen)).toBeInTheDocument()) }) }) }) diff --git a/pro/src/pages/Reimbursements/Income/Income.tsx b/pro/src/pages/Reimbursements/Income/Income.tsx index c4309a67244..4b06c409f12 100644 --- a/pro/src/pages/Reimbursements/Income/Income.tsx +++ b/pro/src/pages/Reimbursements/Income/Income.tsx @@ -1,197 +1,210 @@ -import classnames from 'classnames' import isEqual from 'lodash.isequal' -import { useState, useEffect } from 'react' -import { useSelector } from 'react-redux' -import useSWR from 'swr' +import classnames from 'classnames' +import { useState, useEffect, useRef } from 'react' +import { FormikProvider, useFormik } from 'formik' +import * as yup from 'yup' -import { api } from 'apiClient/api' -import { GET_OFFERER_QUERY_KEY } from 'commons/config/swrQueryKeys' -import { selectCurrentOffererId } from 'commons/store/user/selectors' import { FormLayout } from 'components/FormLayout/FormLayout' -import { formatAndOrderVenues } from 'repository/venuesService' import { Spinner } from 'ui-kit/Spinner/Spinner' +import { SelectAutocomplete } from 'ui-kit/form/SelectAutoComplete/SelectAutocomplete' -import { - getPhysicalVenuesFromOfferer, - getVirtualVenueFromOfferer, -} from '../../Home/venueUtils' - +import { useVenues, useIncome } from './hooks' import styles from './Income.module.scss' import { IncomeError } from './IncomeError/IncomeError' import { IncomeNoData } from './IncomeNoData/IncomeNoData' import { IncomeResultsBox } from './IncomeResultsBox/IncomeResultsBox' -import { IncomeVenueSelector } from './IncomeVenueSelector/IncomeVenueSelector' -import { IncomeByYear } from './types' -// FIXME: remove this, use real data. -// Follow-up Jira ticket : https://passculture.atlassian.net/browse/PC-32278 -export const MOCK_INCOME_BY_YEAR: IncomeByYear = { - 2021: {}, - 2022: { - aggregatedRevenue: { - total: 2000.27, - individual: 2000, - }, - }, - 2023: { - aggregatedRevenue: { - total: 3000, - individual: 1500, - group: 1500, - }, - }, - 2024: { - aggregatedRevenue: { - total: 4000, - individual: 2000, - group: 2000, - }, - expectedRevenue: { - total: 5000, - individual: 2500, - group: 2500, - }, - }, +type VenueFormValues = { + selectedVenues: string[] + 'search-selectedVenues': string } export const Income = () => { - const selectedOffererId = useSelector(selectCurrentOffererId) - const { - data: selectedOfferer, - error: venuesApiError, - isLoading: areVenuesLoading, - } = useSWR( - selectedOffererId ? [GET_OFFERER_QUERY_KEY, selectedOffererId] : null, - ([, offererIdParam]) => api.getOfferer(offererIdParam) - ) - - const physicalVenues = getPhysicalVenuesFromOfferer(selectedOfferer) - const virtualVenue = getVirtualVenueFromOfferer(selectedOfferer) - - const rawVenues = [...physicalVenues, virtualVenue].filter((v) => !!v) - const venues = formatAndOrderVenues(rawVenues) - const venueValues = venues.map((v) => v.value) + const firstYearFilterRef = useRef(null) - const [isIncomeLoading, setIsIncomeLoading] = useState(true) - const [incomeApiError] = useState() - - const [selectedVenues, setSelectedVenues] = useState([]) - const [incomeByYear, setIncomeByYear] = useState() + const [previouslySelectedOffererId, setPreviouslySelectedOffererId] = useState(null) + const [debouncedSelectedVenues, setDebouncedSelectedVenues] = useState([]) const [activeYear, setActiveYear] = useState() - const years = Object.keys(incomeByYear || {}) - .map(Number) - .sort((a, b) => b - a) - const finalActiveYear = activeYear || years[0] - - const activeYearIncome = - incomeByYear && finalActiveYear ? incomeByYear[finalActiveYear] : {} - const activeYearHasData = - activeYearIncome.aggregatedRevenue || activeYearIncome.expectedRevenue + const { + areVenuesLoading, + venuesApiError, + venuesDataReady, + venues, + selectedOffererId + } = useVenues() + const hasSingleVenue = venuesDataReady && venues.length === 1 + const venueValues = venues.map((v) => v.value) - useEffect(() => { - if (venueValues.length > 0 && selectedVenues.length === 0) { - setSelectedVenues(venueValues) - } - }, [venueValues, selectedVenues]) + const formik = useFormik({ + // SelectAutocomplete has two fields: + // - selectedVenues: for the