diff --git a/backend/datasources/dummy.py b/backend/datasources/dummy.py index 959c4a76..f1b39af7 100644 --- a/backend/datasources/dummy.py +++ b/backend/datasources/dummy.py @@ -209,7 +209,9 @@ def dummy_installation_details(username: str, contract_number: str) -> Installat code=contract_number, error=f"{contract_number} (Dummy error)", )) - return InstallationDetailsResult(**ns.load('frontend/src/data/dummyinstallationdetail.yaml')) + details = InstallationDetailsResult(**ns.load('frontend/src/data/dummyinstallationdetail.yaml')) + details.installation_details.contract_number = contract_number + return details invoice_pdf_exceptions = { e.__name__: e diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 66f184d0..7ad6a5d0 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -26,6 +26,7 @@ import InstallationsPage from './pages/InstallationsPage' import DetailInstallationPage from './pages/DetailInstallationPage' import InvoicesPage from './pages/InvoicesPage' import ProductionPage from './pages/ProductionPage' +import { InstallationContextProvider } from './components/InstallationProvider' const routes = [ { @@ -73,11 +74,21 @@ const routes = [ }, { path: '/installation', - element: , - }, - { - path: '/installation/:contract_number', - element: , + element: ( + + + + ), + children: [ + { + path: '', + element: , + }, + { + path: ':contract_number', + element: , + }, + ], }, { path: '/invoices', diff --git a/frontend/src/components/BreakPointIndicator.jsx b/frontend/src/components/BreakPointIndicator.jsx index 6bbd6051..0e3d118f 100644 --- a/frontend/src/components/BreakPointIndicator.jsx +++ b/frontend/src/components/BreakPointIndicator.jsx @@ -16,7 +16,7 @@ export default function BreakPointIndicator() { minWidth: '4rem', padding: '0.5rem 0.9rem', borderRadius: '5px', - alignText: 'center', + textAlign: 'center', color: 'black', backgroundColor: { xs: '#d99a', diff --git a/frontend/src/components/InstallationProvider.jsx b/frontend/src/components/InstallationProvider.jsx new file mode 100644 index 00000000..d6602319 --- /dev/null +++ b/frontend/src/components/InstallationProvider.jsx @@ -0,0 +1,48 @@ +import React, { useEffect, useState, useMemo } from 'react' +import PropTypes from 'prop-types' +import { useAuth } from './AuthProvider' +import ovapi from '../services/ovapi' + +const InstallationContext = React.createContext() + +const InstallationContextProvider = ({ children }) => { + const { currentUser } = useAuth() + const [installations, setInstallations] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const memoizedCurrentUser = useMemo(() => currentUser, [currentUser]) + + useEffect(() => { + const getInstallations = async () => { + try { + const installationsData = await ovapi.installations(memoizedCurrentUser) + setInstallations(installationsData) + } catch (error) { + setError(error) + } finally { + setLoading(false) + } + } + + getInstallations() + }, [memoizedCurrentUser]) + getInstallations() + }, [memoizedCurrentUser]) + + const contextValue = useMemo( + () => ({ installations, loading, error }), + [installations, loading, error], + ) + + return installations !== null ? ( + + {children} + + ) : null +} + +InstallationContextProvider.propTypes = { + children: PropTypes.node.isRequired, +} + +export { InstallationContext, InstallationContextProvider } diff --git a/frontend/src/components/NavigationButtons.jsx b/frontend/src/components/NavigationButtons.jsx index fc6e3e3e..30e2a428 100644 --- a/frontend/src/components/NavigationButtons.jsx +++ b/frontend/src/components/NavigationButtons.jsx @@ -19,10 +19,12 @@ const StyledButton = styled(Button)(({ theme }) => ({ const NavigationButtons = (props) => { const { toBefore, toNext, toReturn, returnIcon } = props + return ( { }} > - - - - - - + {toBefore && ( + + + + )} + {toNext && ( + + + + )} {returnIcon} diff --git a/frontend/src/pages/DetailInstallationPage/detailInstallationData.js b/frontend/src/pages/DetailInstallationPage/detailInstallationData.js index 6cb42549..809c948f 100644 --- a/frontend/src/pages/DetailInstallationPage/detailInstallationData.js +++ b/frontend/src/pages/DetailInstallationPage/detailInstallationData.js @@ -26,6 +26,7 @@ export const contractFields = [ 'discharge_date', 'status', ] + export function transformInstallationDetails(data) { const t = i18n.t const productionTecnologyOptions = { @@ -39,7 +40,7 @@ export function transformInstallationDetails(data) { technology: format.enumeration(data.technology, productionTecnologyOptions), } } -export default function transformContractDetails(contract) { +function transformContractDetails(contract) { const t = i18n.t const billingModeOptions = { index: t('CONTRACT_DETAIL.BILLING_MODE_INDEX'), @@ -88,3 +89,26 @@ export default function transformContractDetails(contract) { return contract } + +function computeNavigationInfo(installations, currentInstallationContractNumber) { + if (installations.length < 2) { + return {} + } + + // Find the index of the current installation + const currentIndex = installations.findIndex( + (installation) => installation.contract_number === currentInstallationContractNumber, + ) + + // Determine the index of the previous and next installations + const previousIndex = currentIndex > 0 ? currentIndex - 1 : installations.length - 1 + const nextIndex = currentIndex < installations.length - 1 ? currentIndex + 1 : 0 + + // Extract the contract numbers for the previous and next installations + const before = installations[previousIndex].contract_number + const next = installations[nextIndex].contract_number + + return { before, next } +} + +export { transformContractDetails, computeNavigationInfo } diff --git a/frontend/src/pages/DetailInstallationPage/detailInstallationData.test.js b/frontend/src/pages/DetailInstallationPage/detailInstallationData.test.js index d72d7080..69065d06 100644 --- a/frontend/src/pages/DetailInstallationPage/detailInstallationData.test.js +++ b/frontend/src/pages/DetailInstallationPage/detailInstallationData.test.js @@ -1,8 +1,10 @@ import { describe, expect, it } from 'vitest' import i18n from '../../i18n/i18n' -import transformContractDetails, { +import { + transformContractDetails, transformInstallationDetails, + computeNavigationInfo, } from './detailInstallationData' describe('transformContractDetails', () => { @@ -138,3 +140,77 @@ describe('transformInstallationDetails', () => { expect(result['rated_power']).toEqual('11 kW') }) }) + +describe('computeNavigationInfo', () => { + it('Returns empty when installations length is smaller than 2', () => { + const installations = [ + { + contract_number: 'a_contract_number', + installation_name: 'an_installation_name', + }, + ] + const currentInstallationContractNumber = 'a_contract_number' + + const result = computeNavigationInfo(installations, currentInstallationContractNumber) + + expect(result).toEqual({}) + }) + + describe('Having the installation 2 elements', () => { + it('Returns the not current element as before and next navigation values', () => { + const installations = [ + { + contract_number: 'a_contract_number', + installation_name: 'an_installation_name', + }, + { + contract_number: 'another_contract_number', + installation_name: 'another_installation_name', + }, + ] + const currentInstallationContractNumber = 'a_contract_number' + + const result = computeNavigationInfo( + installations, + currentInstallationContractNumber, + ) + + const expected_result = { + before: 'another_contract_number', + next: 'another_contract_number', + } + expect(result).toEqual(expected_result) + }) + }) + + describe('Having the installation more than 2 elements', () => { + it('Returns the not current element as before and next navigation values', () => { + const installations = [ + { + contract_number: 'a_contract_number', + installation_name: 'an_installation_name', + }, + { + contract_number: 'another_contract_number', + installation_name: 'another_installation_name', + }, + { + contract_number: 'yet_another_contract_number', + installation_name: 'yet_another_installation_name', + }, + ] + const currentInstallationContractNumber = 'another_contract_number' + + const result = computeNavigationInfo( + installations, + currentInstallationContractNumber, + ) + + const expected_result = { + before: 'a_contract_number', + next: 'yet_another_contract_number', + } + expect(result).toEqual(expected_result) + }) + }) +}) diff --git a/frontend/src/pages/DetailInstallationPage/index.jsx b/frontend/src/pages/DetailInstallationPage/index.jsx index c95806de..b4eacb82 100644 --- a/frontend/src/pages/DetailInstallationPage/index.jsx +++ b/frontend/src/pages/DetailInstallationPage/index.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useState, useContext, useMemo } from 'react' import { useParams } from 'react-router-dom' import { useTranslation } from 'react-i18next' import Container from '@mui/material/Container' @@ -12,36 +12,52 @@ import ErrorSplash from '../../components/ErrorSplash' import NavigationButtons from '../../components/NavigationButtons' import { contractFields } from './detailInstallationData' import { installationFields } from './detailInstallationData' -import transformContractDetails, { +import { + transformContractDetails, transformInstallationDetails, + computeNavigationInfo, } from './detailInstallationData' +import { InstallationContext } from '../../components/InstallationProvider' -export default function DetailInstallationPage(params) { +export default function DetailInstallationPage() { const { contract_number } = useParams() const { t } = useTranslation() const [installationDetail, setInstallationDetail] = useState(undefined) const [contractDetail, setContractDetail] = useState(undefined) const [error, setError] = useState(false) + const { installations } = useContext(InstallationContext) + const memoizedInstallations = useMemo(() => installations, [installations]) + const navigationInfo = computeNavigationInfo( + memoizedInstallations, + installationDetail?.contract_number, + ) + const navigationBeforeUrl = navigationInfo.before + ? `/installation/${navigationInfo.before}` + : undefined + const navigationNextUrl = navigationInfo.next + ? `/installation/${navigationInfo.next}` + : undefined useEffect(() => { getDetailInstallation() - }, [contract_number]) + }, [contract_number, memoizedInstallations]) async function getDetailInstallation() { setError(false) setInstallationDetail(undefined) setContractDetail(undefined) - var result try { - result = await ovapi.installationDetails(contract_number) - } catch (e) { - setError(e) - return + const result = await ovapi.installationDetails(contract_number) + if (!result) { + setError(true) + return + } + setInstallationDetail(result?.installation_details) + const contractData = transformContractDetails(result?.contract_details) + setContractDetail(contractData) + } catch (error) { + setError(true) } - const installationData = transformInstallationDetails(result?.installation_details) - setInstallationDetail(installationData) - const contractData = transformContractDetails(result?.contract_details) - setContractDetail(contractData) } return !error && (!installationDetail || !contractDetail) ? ( @@ -50,23 +66,21 @@ export default function DetailInstallationPage(params) { {t('INSTALLATION_DETAIL.DETAILS_TITLE')} + } + /> {error ? ( ) : ( <> - {/* TODO: get the before and after user installations */} - } - /> installations, [installations]) - React.useEffect(() => { - getInstallations() - }, [currentUser]) - - async function getInstallations() { - beLoading(true) - setRows([]) - setError(false) - try { - setRows(await ovapi.installations(currentUser)) - } catch (error) { - setError(error) + useEffect(() => { + setPageLoading(true) + if (installations) { + setPageLoading(false) } - beLoading(false) - } + }, [installations]) const columns = [ { @@ -54,27 +41,28 @@ export default function InstallationsPage(params) { numeric: false, }, ] + const actions = [] const selectionActions = [] + const itemActions = [ { title: t('INSTALLATIONS.TOOLTIP_DETAILS'), - view: (contract) => { - return ( - - ) - }, + view: (contract) => ( + + ), }, ] - return isLoading ? ( + + return pageLoading ? ( ) : ( @@ -85,21 +73,21 @@ export default function InstallationsPage(params) { getInstallations()} + backlink="/installation" backtext={t('INSTALLATIONS.RELOAD')} /> ) : ( @@ -111,7 +99,7 @@ export default function InstallationsPage(params) { } - > + /> )} ) diff --git a/frontend/src/services/messages.js b/frontend/src/services/messages.js index c9c7fd5e..340454cd 100644 --- a/frontend/src/services/messages.js +++ b/frontend/src/services/messages.js @@ -25,7 +25,7 @@ IMPORTANT: `subscribe()` returns a function to unsubscribe. Use it. const _subscribers = new Set() function _notify(message, level, extra) { - _subscribers.forEach((l) => l({ message, level, ...(extra || {}) })) + _subscribers.forEach((l) => l({ message, level, ...extra })) } function subscribe(subscriber) {