From cfbec2dad035e95912a396530b32076e852834d1 Mon Sep 17 00:00:00 2001 From: Julian Buecher Date: Thu, 6 Jul 2023 00:30:35 +0200 Subject: [PATCH] OCPP 1.6 ReserveNow and Cancel Reservation + Custom Reservation Module --- .gitignore | 3 + .prettierrc | 4 +- package-lock.json | 15 + package.json | 1 + src/App.tsx | 412 ++++++----- src/I18n/languages/de.json | 159 +++- src/I18n/languages/en.json | 146 +++- .../ReservableChargingStationComponent.tsx | 128 ++++ ...servableChargingStationComponentStyles.tsx | 81 ++ .../date-time/DateTimePickerComponent.tsx | 175 +++++ .../DateTimePickerComponentStyles.tsx | 80 ++ src/components/fab/FabComponentStyles.tsx | 4 +- src/components/map/ClusterMap.tsx | 17 +- .../reservation/ReservationComponent.tsx | 92 +++ .../ReservationComponentStyles.tsx | 119 +++ .../header/ReservationHeaderComponent.tsx | 63 ++ .../ReservationHeaderComponentStyles.tsx | 46 ++ .../date/DateFilterControlComponent.tsx | 25 +- .../DateTimeFilterControlComponent.tsx | 0 src/config/Configuration.tsx | 5 + src/custom-theme/customCommonColor.tsx | 3 +- src/deeplinking/DeepLinkingManager.tsx | 1 + src/notification/Notifications.tsx | 30 +- src/provider/CentralServerProvider.tsx | 590 +++++++++------ src/provider/SecurityProvider.tsx | 60 +- src/screens/cars/AddCar.tsx | 68 +- .../ChargingStationConnectorDetails.tsx | 477 ++++++++---- .../ChargingStationConnectorDetailsStyles.tsx | 8 +- .../ChargingStationConnectorReserveNow.tsx | 447 +++++++++++ ...argingStationConnectorReserveNowStyles.tsx | 230 ++++++ .../list/ChargingStations.tsx | 158 ++-- .../list/ReservableChargingStations.tsx | 240 ++++++ .../list/ReservableChargingStationsStyles.tsx | 64 ++ src/screens/reservations/AddReservation.tsx | 695 +++++++++++++++++ .../reservations/AddReservationStyles.tsx | 93 +++ src/screens/reservations/EditReservation.tsx | 698 ++++++++++++++++++ .../reservations/EditReservationStyles.tsx | 89 +++ src/screens/reservations/Reservations.tsx | 312 ++++++++ .../reservations/ReservationsFilters.tsx | 121 +++ .../ReservationsFiltersStyles.tsx | 28 + .../reservations/ReservationsStyles.tsx | 64 ++ .../details/ReservationDetails.tsx | 554 ++++++++++++++ .../details/ReservationDetailsStyles.tsx | 237 ++++++ src/screens/site-areas/SiteAreas.tsx | 116 +-- src/screens/sites/Sites.tsx | 80 +- src/screens/users/list/Users.tsx | 54 +- src/types/Authorization.tsx | 5 +- src/types/ChargingStation.tsx | 10 +- src/types/Filter.tsx | 6 +- src/types/HTTPError.tsx | 12 +- src/types/Reservation.tsx | 52 ++ src/types/Server.tsx | 15 +- src/types/Tenant.tsx | 4 +- src/types/UserNotifications.tsx | 7 +- .../requests/HTTPChargingStationRequests.tsx | 2 +- src/types/requests/HTTPReservationRequest.tsx | 77 ++ src/utils/Constants.tsx | 3 + src/utils/Utils.tsx | 241 ++++-- 58 files changed, 6715 insertions(+), 811 deletions(-) create mode 100644 src/components/charging-station/ReservableChargingStationComponent.tsx create mode 100644 src/components/charging-station/ReservableChargingStationComponentStyles.tsx create mode 100644 src/components/date-time/DateTimePickerComponent.tsx create mode 100644 src/components/date-time/DateTimePickerComponentStyles.tsx create mode 100644 src/components/reservation/ReservationComponent.tsx create mode 100644 src/components/reservation/ReservationComponentStyles.tsx create mode 100644 src/components/reservation/header/ReservationHeaderComponent.tsx create mode 100644 src/components/reservation/header/ReservationHeaderComponentStyles.tsx create mode 100644 src/components/search/filter/controls/date/date-time/DateTimeFilterControlComponent.tsx create mode 100644 src/screens/charging-stations/connector-details/reserve-now/ChargingStationConnectorReserveNow.tsx create mode 100644 src/screens/charging-stations/connector-details/reserve-now/ChargingStationConnectorReserveNowStyles.tsx create mode 100644 src/screens/charging-stations/list/ReservableChargingStations.tsx create mode 100644 src/screens/charging-stations/list/ReservableChargingStationsStyles.tsx create mode 100644 src/screens/reservations/AddReservation.tsx create mode 100644 src/screens/reservations/AddReservationStyles.tsx create mode 100644 src/screens/reservations/EditReservation.tsx create mode 100644 src/screens/reservations/EditReservationStyles.tsx create mode 100644 src/screens/reservations/Reservations.tsx create mode 100644 src/screens/reservations/ReservationsFilters.tsx create mode 100644 src/screens/reservations/ReservationsFiltersStyles.tsx create mode 100644 src/screens/reservations/ReservationsStyles.tsx create mode 100644 src/screens/reservations/details/ReservationDetails.tsx create mode 100644 src/screens/reservations/details/ReservationDetailsStyles.tsx create mode 100644 src/types/Reservation.tsx create mode 100644 src/types/requests/HTTPReservationRequest.tsx diff --git a/.gitignore b/.gitignore index 5d0ff3cbf..f882def97 100644 --- a/.gitignore +++ b/.gitignore @@ -115,3 +115,6 @@ assets/config.json coverage/ __tests__/__snapshots__/ test-results/ + +# ESLint Cache +.eslintcache diff --git a/.prettierrc b/.prettierrc index ab29b45cf..202cbbaf6 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,6 +3,6 @@ printWidth: 140, trailingComma: "none", jsxBracketSameLine: true, - requirePragma: false + requirePragma: false, + singleQuote: true } - diff --git a/package-lock.json b/package-lock.json index db99ddc0f..29bf8e383 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@formatjs/intl-numberformat": "^8.2.0", "@formatjs/intl-pluralrules": "^5.1.4", "@formatjs/intl-relativetimeformat": "^11.1.4", + "@notifee/react-native": "^7.8.0", "@ptomasroos/react-native-multi-slider": "github:0hio-creator/react-native-multi-slider", "@react-native-community/datetimepicker": "^6.7.0", "@react-native-firebase/app": "^16.4.5", @@ -5012,6 +5013,14 @@ "node": ">= 8" } }, + "node_modules/@notifee/react-native": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@notifee/react-native/-/react-native-7.8.0.tgz", + "integrity": "sha512-sx8h62U4FrR4pqlbN1rkgPsdamDt9Tad0zgfO6VtP6rNJq/78k8nxUnh0xIX3WPDcCV8KAzdYCE7+UNvhF1CpQ==", + "peerDependencies": { + "react-native": "*" + } + }, "node_modules/@opentelemetry/api": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.3.0.tgz", @@ -33612,6 +33621,12 @@ "fastq": "^1.6.0" } }, + "@notifee/react-native": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@notifee/react-native/-/react-native-7.8.0.tgz", + "integrity": "sha512-sx8h62U4FrR4pqlbN1rkgPsdamDt9Tad0zgfO6VtP6rNJq/78k8nxUnh0xIX3WPDcCV8KAzdYCE7+UNvhF1CpQ==", + "requires": {} + }, "@opentelemetry/api": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.3.0.tgz", diff --git a/package.json b/package.json index eaeae18d6..cf684f006 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "@formatjs/intl-numberformat": "^8.2.0", "@formatjs/intl-pluralrules": "^5.1.4", "@formatjs/intl-relativetimeformat": "^11.1.4", + "@notifee/react-native": "^7.8.0", "@ptomasroos/react-native-multi-slider": "github:0hio-creator/react-native-multi-slider", "@react-native-community/datetimepicker": "^6.7.0", "@react-native-firebase/app": "^16.4.5", diff --git a/src/App.tsx b/src/App.tsx index b578241d9..964b4f49a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,18 +1,14 @@ import { createDrawerNavigator } from '@react-navigation/drawer'; import { createMaterialBottomTabNavigator } from '@react-navigation/material-bottom-tabs'; -import { - getStateFromPath, - InitialState, LinkingOptions, - NavigationContainer, - NavigationContainerRef -} from '@react-navigation/native'; +import { getStateFromPath, InitialState, LinkingOptions, NavigationContainer, NavigationContainerRef } from '@react-navigation/native'; import { createStackNavigator } from '@react-navigation/stack'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import I18n from 'i18n-js'; -import {Icon, NativeBaseProvider} from 'native-base'; -import React, {useEffect, useState} from 'react'; -import {Appearance, ColorSchemeName, NativeEventSubscription, StatusBar, Text} from 'react-native'; +import { Icon, NativeBaseProvider } from 'native-base'; +import React, { useEffect, useState } from 'react'; +import { Appearance, ColorSchemeName, NativeEventSubscription, StatusBar, Text } from 'react-native'; import { scale } from 'react-native-size-matters'; +import notifee, { AndroidImportance, EventType } from '@notifee/react-native'; import DeepLinkingManager from './deeplinking/DeepLinkingManager'; import I18nManager from './I18n/I18nManager'; @@ -61,15 +57,20 @@ import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; import FontAwesome from 'react-native-vector-icons/FontAwesome'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import Settings from './screens/settings/Settings'; -import {hide} from 'react-native-bootsplash'; -import {ThemeType} from './types/Theme'; -import {GestureHandlerRootView} from 'react-native-gesture-handler'; +import { hide } from 'react-native-bootsplash'; +import { ThemeType } from './types/Theme'; +import { GestureHandlerRootView } from 'react-native-gesture-handler'; import messaging from '@react-native-firebase/messaging'; -import {AuthContext} from './context/AuthContext'; +import { AuthContext } from './context/AuthContext'; import Loading from './screens/loading/Loading'; -import {Notification} from './types/UserNotifications'; +import { Notification } from './types/UserNotifications'; import Configuration from './config/Configuration'; -import {RootSiblingParent} from 'react-native-root-siblings'; +import { RootSiblingParent } from 'react-native-root-siblings'; +import Reservations from './screens/reservations/Reservations'; +import ReservationDetails from './screens/reservations/details/ReservationDetails'; +import ReserveNow from './screens/charging-stations/connector-details/reserve-now/ChargingStationConnectorReserveNow'; +import AddReservation from './screens/reservations/AddReservation'; +import EditReservation from './screens/reservations/EditReservation'; // Init i18n I18nManager.initialize(); @@ -89,11 +90,13 @@ const CarsStack = createStackNavigator(); const InvoicesStack = createStackNavigator(); const PaymentMethodsStack = createStackNavigator(); const SettingsStack = createStackNavigator(); +const ReservationsStack = createStackNavigator(); // Navigation Tab variable const ChargingStationDetailsTabs = createMaterialBottomTabNavigator(); const ChargingStationConnectorDetailsTabs = createMaterialBottomTabNavigator(); const TransactionDetailsTabs = createMaterialBottomTabNavigator(); +const ReservationDetailsTabs = createMaterialBottomTabNavigator(); // Navigation Drawer variable const AppDrawer = createDrawerNavigator(); @@ -107,7 +110,7 @@ const createTabBarIcon = ( return ( - + ); } @@ -156,11 +155,7 @@ function StatsNavigator(props: BaseProps) { function ReportErrorNavigator(props: BaseProps) { return ( - + ); } @@ -176,12 +171,11 @@ function ChargingStationDetailsTabsNavigator(props: BaseProps) { inactiveColor={commonColor.disabledDark} barStyle={barStyle} labeled - backBehavior={'initialRoute'} - > + backBehavior={'initialRoute'}> {I18n.t('chargers.actions')}, tabBarIcon: (iconProps) => createTabBarIcon(iconProps, MaterialIcons, 'build') @@ -190,7 +184,7 @@ function ChargingStationDetailsTabsNavigator(props: BaseProps) { {I18n.t('chargers.ocpp')}, tabBarIcon: (iconProps) => createTabBarIcon(iconProps, MaterialIcons, 'format-list-bulleted') @@ -199,7 +193,7 @@ function ChargingStationDetailsTabsNavigator(props: BaseProps) { {I18n.t('chargers.properties')}, tabBarIcon: (iconProps) => createTabBarIcon(iconProps, MaterialIcons, 'info') @@ -220,12 +214,11 @@ function ChargingStationConnectorDetailsTabsNavigator(props: BaseProps) { inactiveColor={commonColor.disabledDark} barStyle={barStyle} labeled - backBehavior={'initialRoute'} - > + backBehavior={'initialRoute'}> {I18n.t('sites.chargePoint')}, tabBarIcon: (iconProps) => createTabBarIcon(iconProps, FontAwesome, 'bolt') @@ -234,7 +227,7 @@ function ChargingStationConnectorDetailsTabsNavigator(props: BaseProps) { {I18n.t('details.graph')}, tabBarIcon: (iconProps) => createTabBarIcon(iconProps, MaterialCommunityIcons, 'chart-areaspline-variant') @@ -255,12 +248,11 @@ function TransactionDetailsTabsNavigator(props: BaseProps) { inactiveColor={commonColor.disabledDark} barStyle={barStyle} labeled - backBehavior={'initialRoute'} - > + backBehavior={'initialRoute'}> {I18n.t('transactions.transaction')}, tabBarIcon: (iconProps) => createTabBarIcon(iconProps, FontAwesome, 'bolt') @@ -269,7 +261,7 @@ function TransactionDetailsTabsNavigator(props: BaseProps) { {I18n.t('details.graph')}, tabBarIcon: (iconProps) => createTabBarIcon(iconProps, MaterialCommunityIcons, 'chart-areaspline-variant') @@ -281,33 +273,30 @@ function TransactionDetailsTabsNavigator(props: BaseProps) { function SitesNavigator(props: BaseProps) { return ( - - + + - @@ -324,7 +313,7 @@ function SitesNavigator(props: BaseProps) { ); @@ -333,11 +322,7 @@ function SitesNavigator(props: BaseProps) { function ChargingStationsNavigator(props: BaseProps) { return ( - + + + ); @@ -414,11 +409,7 @@ function TransactionInProgressNavigator(props: BaseProps) { function UsersNavigator(props: BaseProps) { return ( - + ); } @@ -426,11 +417,7 @@ function UsersNavigator(props: BaseProps) { function TagsNavigator(props: BaseProps) { return ( - + ); } @@ -438,15 +425,11 @@ function TagsNavigator(props: BaseProps) { function CarsNavigator(props: BaseProps) { return ( - + ); @@ -455,11 +438,7 @@ function CarsNavigator(props: BaseProps) { function InvoicesNavigator(props: BaseProps) { return ( - + ); } @@ -467,15 +446,11 @@ function InvoicesNavigator(props: BaseProps) { function PaymentMethodsNavigator(props: BaseProps) { return ( - + ); @@ -484,11 +459,7 @@ function PaymentMethodsNavigator(props: BaseProps) { function SettingsNavigator(props: BaseProps) { return ( - + ); } @@ -525,20 +496,19 @@ function AppDrawerNavigator(props: BaseProps) { }} backBehavior={'initialRoute'} drawerContent={(drawerProps) => }> - {/*// Hack, drawerSection property does not exist but we can still pass it*/} - + {/*/ / Hack, drawerSection property does not exist but we can still pass it*/} + - }} - initialParams={{...(props?.route?.params?.params || {}), canOpenDrawer: false}} + initialParams={{ ...(props?.route?.params?.params || {}), canOpenDrawer: false }} /> - + )} + {securityProvider?.isComponentReservationActive() && ( + + }} + initialParams={props?.route?.params?.params} + /> + )} - + - + {securityProvider?.canListUsers() && ( + drawerIcon: () => ( + + ) }} initialParams={props?.route?.params?.params} /> @@ -624,14 +607,16 @@ function AppDrawerNavigator(props: BaseProps) { /> )} - + {securityProvider?.canListPaymentMethods() && ( + drawerIcon: () => ( + + ) }} initialParams={props?.route?.params?.params} /> @@ -661,13 +646,53 @@ function AppDrawerNavigator(props: BaseProps) { ); } +function ReservationsNavigator(props: BaseProps) { + return ( + + + + + + + ); +} + +function ReservationDetailsTabNavigator(props: BaseProps) { + const commonColor = Utils.getCurrentCommonColor(); + const barStyle = getTabStyle(); + const style = computeStyleSheet(); + return ( + + {I18n.t('reservations.title')}, + tabBarIcon: (iconProps) => createTabBarIcon(iconProps, FontAwesome, 'bolt') + }} + /> + + ); +} + export interface Props {} interface State { navigationState?: InitialState; @@ -692,8 +717,8 @@ export default class App extends React.Component { super(props); this.navigationRef = React.createRef(); this.appContext = { - handleSignIn: () => this.setState({isSignedIn: true}), - handleSignOut: () => this.setState({isSignedIn: false}) + handleSignIn: () => this.setState({ isSignedIn: true }), + handleSignOut: () => this.setState({ isSignedIn: false }) }; this.state = { navigationState: null, @@ -709,7 +734,6 @@ export default class App extends React.Component { super.setState(state, callback); }; - public async componentDidMount() { // Set up theme const themeManager = ThemeManager.getInstance(); @@ -717,7 +741,7 @@ export default class App extends React.Component { // Listen for theme changes this.themeSubscription = Appearance.addChangeListener(({ colorScheme }) => { themeManager.setThemeType(Appearance.getColorScheme() as ThemeType); - this.setState({theme: colorScheme}); + this.setState({ theme: colorScheme }); }); // Get the central server @@ -735,7 +759,7 @@ export default class App extends React.Component { await Notifications.initialize(); // Store initial url through which app was launched (if any) - const initialNotification = await messaging().getInitialNotification() as Notification; + const initialNotification = (await messaging().getInitialNotification()) as Notification; const canHandleNotification = await Notifications.canHandleNotification(initialNotification); let tenantSubdomain: string; if (canHandleNotification) { @@ -784,12 +808,12 @@ export default class App extends React.Component { {showAppUpdateDialog && ( this.setState({ showAppUpdateDialog: false })} /> )} - - {isSignedIn == null ? - - : - this.createRootNavigator() - } + + {isSignedIn == null ? : this.createRootNavigator()} @@ -797,70 +821,105 @@ export default class App extends React.Component { } private buildLinking(): LinkingOptions { - return ( - { - prefixes: DeepLinkingManager.getAuthorizedURLs(), - getInitialURL: () => this.initialUrl, - subscribe: (listener) => { - // Listen for background notifications when the app is running, - const removeBackgroundNotificationListener = messaging().onNotificationOpenedApp(async (remoteMessage: Notification) => { - const canHandleNotification = await Notifications.canHandleNotificationOpenedApp(remoteMessage); - if (canHandleNotification) { - this.setState({isSignedIn: true}, () => listener(remoteMessage.data.deepLink)); - } - }); - // Listen for FCM token refresh event - const removeTokenRefreshEventListener = messaging().onTokenRefresh((token) => { - void Notifications.onTokenRefresh(token); + return { + prefixes: DeepLinkingManager.getAuthorizedURLs(), + getInitialURL: () => this.initialUrl, + subscribe: (listener) => { + const removeOnMessageListener = messaging().onMessage(async (remoteMessage: Notification) => { + const canHandleNotification = await Notifications.canHandleNotificationOpenedApp(remoteMessage); + await notifee.displayNotification({ + id: remoteMessage.messageId, + title: remoteMessage.notification.title, + body: remoteMessage.notification.body, + android: { + ...remoteMessage.notification.android, + channelId: 'fcm_notifications', + pressAction: { id: 'default' }, + importance: AndroidImportance.HIGH + }, + ios: { badgeCount: 0 } }); - return () => { - removeBackgroundNotificationListener(); - removeTokenRefreshEventListener(); - }; - }, - config: { - screens: { - AuthNavigator: { - screens: { - Login: DeepLinkingManager.PATH_LOGIN + notifee.onForegroundEvent(({ type }) => { + if (type === EventType.PRESS) { + if (canHandleNotification) { + this.setState({ isSignedIn: true }, () => listener(remoteMessage.data.deepLink)); } + } + }); + }); + // Listen for background notifications when the app is running, + const removeBackgroundNotificationListener = messaging().onNotificationOpenedApp(async (remoteMessage: Notification) => { + const canHandleNotification = await Notifications.canHandleNotificationOpenedApp(remoteMessage); + if (canHandleNotification) { + this.setState({ isSignedIn: true }, () => listener(remoteMessage.data.deepLink)); + } + const notificationId = await notifee.displayNotification({ + id: remoteMessage.messageId, + title: remoteMessage.notification.title, + body: remoteMessage.notification.body, + android: { + ...remoteMessage.notification.android, + channelId: 'fcm_notifications', + pressAction: { id: 'default' }, + importance: AndroidImportance.HIGH }, - AppDrawerNavigator: { - initialRouteName: 'ChargingStationsNavigator', - screens: { - ChargingStationsNavigator: { - initialRouteName: 'ChargingStations', - screens: { - ChargingStations: `${DeepLinkingManager.PATH_CHARGING_STATIONS}/${DeepLinkingManager.FRAGMENT_ALL}` - } - }, - InvoicesNavigator: DeepLinkingManager.PATH_INVOICES, - TransactionInProgressNavigator: { - screens: { - TransactionsInProgress: `${DeepLinkingManager.PATH_TRANSACTIONS}/${DeepLinkingManager.FRAGMENT_IN_PROGRESS}` - } - }, - TransactionHistoryNavigator: { - screens: { - TransactionsHistory: `${DeepLinkingManager.PATH_TRANSACTIONS}/${DeepLinkingManager.FRAGMENT_HISTORY}` - } + ios: { badgeCount: 0 } + }); + notifee.onBackgroundEvent(async () => {}); + }); + // Listen for FCM token refresh event + const removeTokenRefreshEventListener = messaging().onTokenRefresh((token) => { + void Notifications.onTokenRefresh(token); + }); + return () => { + removeBackgroundNotificationListener(); + removeTokenRefreshEventListener(); + removeOnMessageListener(); + }; + }, + config: { + screens: { + AuthNavigator: { + screens: { + Login: DeepLinkingManager.PATH_LOGIN + } + }, + AppDrawerNavigator: { + initialRouteName: 'ChargingStationsNavigator', + screens: { + ChargingStationsNavigator: { + initialRouteName: 'ChargingStations', + screens: { + ChargingStations: `${DeepLinkingManager.PATH_CHARGING_STATIONS}/${DeepLinkingManager.FRAGMENT_ALL}` } - } + }, + InvoicesNavigator: DeepLinkingManager.PATH_INVOICES, + TransactionInProgressNavigator: { + screens: { + TransactionsInProgress: `${DeepLinkingManager.PATH_TRANSACTIONS}/${DeepLinkingManager.FRAGMENT_IN_PROGRESS}` + } + }, + TransactionHistoryNavigator: { + screens: { + TransactionsHistory: `${DeepLinkingManager.PATH_TRANSACTIONS}/${DeepLinkingManager.FRAGMENT_HISTORY}` + } + }, + ReservationsNavigator: DeepLinkingManager.PATH_RESERVATIONS } } - }, - getStateFromPath: (url, options) => { - const path = url.split('/')?.[1].split('#')?.[0].split('?')?.[0]; - const query = url.split('?')?.[1]?.split('#')?.[0]; - let fragment = url.split('#')?.[1]; - if (path === DeepLinkingManager.PATH_CHARGING_STATIONS && fragment === DeepLinkingManager.FRAGMENT_IN_ERROR) { - fragment = DeepLinkingManager.FRAGMENT_ALL; - } - const newURL = path + (fragment ? '/' + fragment : '') + (query ? '?' + query : ''); - return getStateFromPath(newURL, options); } + }, + getStateFromPath: (url, options) => { + const path = url.split('/')?.[1].split('#')?.[0].split('?')?.[0]; + const query = url.split('?')?.[1]?.split('#')?.[0]; + let fragment = url.split('#')?.[1]; + if (path === DeepLinkingManager.PATH_CHARGING_STATIONS && fragment === DeepLinkingManager.FRAGMENT_IN_ERROR) { + fragment = DeepLinkingManager.FRAGMENT_ALL; + } + const newURL = path + (fragment ? '/' + fragment : '') + (query ? '?' + query : ''); + return getStateFromPath(newURL, options); } - ); + }; } private createRootNavigator() { @@ -872,15 +931,14 @@ export default class App extends React.Component { onReady={() => this.onReady()} linking={this.buildLinking()} ref={this.navigationRef} - onStateChange={(newState) => this.setState({navigationState: newState})} - initialState={this.state.navigationState} - > + onStateChange={(newState) => this.setState({ navigationState: newState })} + initialState={this.state.navigationState}> - {isSignedIn ? + {isSignedIn ? ( - : - - } + ) : ( + + )} diff --git a/src/I18n/languages/de.json b/src/I18n/languages/de.json index 487acf144..a847ceaa6 100644 --- a/src/I18n/languages/de.json +++ b/src/I18n/languages/de.json @@ -89,7 +89,8 @@ "userOrTenantUpdated": "Sie wurden ausgeloggt, weil entweder Ihre Einstellungen oder die Ihrer Organisation aktualisiert wurden", "tenantRedirectionInvalidDomain": "Ungültige Tenant Redirection Domain", "from": "Von", - "to": "Bis" + "to": "Bis", + "notification": "Benachrichtigung" }, "sidebar": { "home": "Home", @@ -105,11 +106,12 @@ "invoices": "Rechnungen", "paymentMethods": "Zahlungsmethoden", "qrCodeScanner": "QR-Code Scanner", - "settings": "Einstellungen" + "settings": "Einstellungen", + "reservations": "Reservierungen" }, "authentication": { - "noTenantFoundTitle" : "Keine Organisation gefunden", - "noTenantFoundMessage" : "Möchten Sie einen neue Organisation mittels QR-Code registrieren?", + "noTenantFoundTitle": "Keine Organisation gefunden", + "noTenantFoundMessage": "Möchten Sie einen neue Organisation mittels QR-Code registrieren?", "tenantTitle": "Organisation", "mandatoryTenantSubDomain": "Die Subdomain der Organisation ist notwendig", "mandatoryTenantName": "Der Name der Organisation ist notwendig", @@ -219,8 +221,8 @@ "noUsers": "Keine Nutzer", "users": "Nutzer", "user": "Nutzer", - "selectUser" : "Nutzer auswählen", - "selectUsers" : "Nutzer auswählen", + "selectUser": "Nutzer auswählen", + "selectUsers": "Nutzer auswählen", "selectOneOrSeveralUsers": "Einen oder mehrere Nutzer auswählen" }, "paymentMethods": { @@ -229,7 +231,7 @@ "paymentMethods": "Zahlungsmethoden", "paymentMethod": "Zahlungsmethode", "addPaymentMethodError": "Hinzufügen der Zahlungsmethode ist fehlgeschlagen", - "addPaymentMethodSuccess" : "Neue Zahlungsmethode erfolgreich hinzugefügt", + "addPaymentMethodSuccess": "Neue Zahlungsmethode erfolgreich hinzugefügt", "deletePaymentMethodSuccess": "Zahlungsmethode erfolgreich entfernt", "deletePaymentMethodTitle": "Zahlungsmethode löschen", "deletePaymentMethodSubtitle": "Möchten Sie wirklich die {{cardBrand}} Karte mit der Endung {{cardLast4}} entfernen", @@ -263,7 +265,7 @@ "cars": { "noCars": "Keine Fahrzeuge", "carUnexpectedError": "Ein Fehler ist aufgetreten", - "cars": "Fehrzeuge", + "cars": "Fahrzeuge", "car": "Fahrzeug", "selectCar": "Fahrzeug auswählen", "selectCars": "Fahrzeuge auswählen", @@ -367,7 +369,7 @@ "totalInactivityNote": "Gesamte Inkativitätsdauer", "totalPriceNote": "Gesamtkosten", "paymentMethods": "Zahlungsmethoden", - "paymentMethodsNote" : "Zahlungsmethoden durchsuchen und erstellen", + "paymentMethodsNote": "Zahlungsmethoden durchsuchen und erstellen", "invoices": "Rechunngen", "invoicesNote": "Rechnungen durchsuchen und herunterladen" }, @@ -392,6 +394,7 @@ "title": "Ladestationen", "heartBeat": "Herzschlag", "chargers": "Ladestation(en)", + "charger": "Ladestation", "noOCPPParameters": "Kein OCPP Parameter", "noChargerParameters": "Kein Ladestationen Parameter", "chargerConfigurationUnexpectedError": "Einstellungen der Ladestation wurden nicht erkannt", @@ -417,7 +420,10 @@ "ocpp": "OCPP", "actions": "Aktionen", "noSession": "Kein Ladevorgang", - "noSessionMessage": "An diesem Ladepunkt wurde kein Ladevorgang durchgeführt" + "noSessionMessage": "An diesem Ladepunkt wurde kein Ladevorgang durchgeführt", + "noChargerMessageTitle": "Es wurde keine Ladestation ausgewählt", + "selectCharger": "Ladestation auswählen", + "selectChargers": "Ladestationen auswählen" }, "connector": { "occupied": "Besetzt", @@ -537,6 +543,41 @@ "message": "Ein unbekannter Benutzer hat auf '{{chargeBoxID}}' gebadgt.", "subMessage": "Unbekannter Benutzer", "longMessage": "Ein unbekanner Benutzer hat auf '{{chargeBoxID}}' gebadgt." + }, + "reservationCreatedNotification": { + "title": "Reservierung erstellt", + "body": "Ihre Reservierung für die Ladestation '{{chargingStationID}}' an Anschluss '{{connectorID}}' wurde erfolgreich erstellt." + }, + "reservationCancelledNotification": { + "title": "Reservierung storniert", + "body": "Sie haben Ihre Reservierung für die Ladestation '{{chargingStationID}}' an Anschluss '{{connectorID}}' erfolgreich storniert." + }, + "reservationUpcomingNotification": { + "title": "Anstehende Reservierung", + "body": "Ihre Reservierung der Ladestation '{{chargingStationID}}' an Anschliss '{{connectorID}}' steht unmittelbar bevor." + }, + "reservationUpcomingWarning": { + "title": "Anstehende Reservierung", + "body": "An Ihrer derzeitigen Ladestation '{{chargingStationID}}' an Anschluss '{{connectorID}}' beginnt ab '{{fromDate}}' eine Reservierung für einen anderen Nutzer." + }, + "reservedChargingStationBlockedNotification": { + "title": "Reservierte Ladestation blockiert", + "body": "An Ihrer derzeitig reservierten Ladestation '{{chargingStationID}}' an Anschluss '{{connectorID}}' wurde eine Ladevorgang gestartet." + }, + "reservationStatusChangedNotification": { + "title": "Status der Reservierung geändert", + "body": "Der Status Ihrer Reservierung für die Ladestation '{{chargingStationID}}' an Anschluss '{{connectorID}}' hat sich geändert.", + "status": { + "reservation_done": "Reservierung abgeschlossen", + "reservation_scheduled": "Reservierung eingeplant", + "reservation_in_progress": "Reservierung laufend", + "reservation_cancelled": "Reservierung storniert", + "reservation_expired": "Reservierung abgelaufen" + } + }, + "reservationUnmetNotification": { + "title": "Reservierung nicht wahrgenommen", + "body": "Die Reservierung an der Ladestation '{{chargingStationID}}' an Anschluss '{{connectorID}}' wurde aufgrund Nichterscheinens innerhalb von 15 Minuten nach Start der Reservierung abgebrochen." } }, "appUpdate": { @@ -551,12 +592,106 @@ "sitesRoamingFilterLabel": "Roaming-Standorte zeigen", "siteAreasRoamingFilterLabel": "Roaming-Standortbereiche zeigen", "transactionsRoamingFilterLabel": "Roaming-Ladevorgänge zeigen", - "usersRoamingFilterLabel": "Roaming-Nutzer zeigen" + "usersRoamingFilterLabel": "Roaming-Nutzer zeigen", + "activeReservationsFilterLabel": "Aktive Reservierungen anzeigen" }, "settingsDistanceUnit": { "distanceUnit": "Distance unit", "kilometers": "Kilometer", "miles": "Meilen", "automatic": "Automatik" + }, + "reservations": { + "titles": "Reservierungen", + "description": "Das Reservierungs-Interface ermöglicht Erstellen und Bearbeiten von geplanten und unmittelbaren Reservierungen für den Ladestationbetreiber.", + "title": "Reservierung", + "id": "ID", + "chargingstationId": "Ladestation", + "connectorId": "Anschluss", + "connectorStatus": "Anschluss Verfügbarkeit", + "expiryDate": "Ablaufdatum", + "fromDate": "Von", + "toDate": "Bis", + "arrivalTime": "Ankunftszeit", + "departureTime": "Abfahrtszeit", + "tagId": "Tag ID", + "status": "Status", + "type": "Reservierungs-Typ", + "user": "Nutzer", + "car": "Fahrzeug", + "reservationsUnexpectedError": "Reservierungen können nicht gelesen werden", + "reservationDoesNotExist": "Reservierungen konnte nicht gefunden werden", + "noReservations": "Keine Reservierungen", + "invalidDate": "Ungültiges Datum", + "dateBeforeMinimum": "Ungültiges Datum. Wähle ein Datum in der Zukunft.", + "dateAfterMaximum": "Ungültiges Datum. Datum liegt außerhalb des zulässigen Bereichs von {{duration}} Stunden.", + "invalidDateRange": "Ungültige Zeitspanne", + "statuses": { + "reservation_done": "Abgeschlossen", + "reservation_scheduled": "Eingeplant", + "reservation_in_progress": "Laufend", + "reservation_cancelled": "Storniert", + "reservation_expired": "Abgelaufen", + "reservation_unmet": "Unerfüllt", + "unknown": "Unbekannt" + }, + "types": { + "reserve_now": "Reserve Now", + "planned_reservation": "Geplant", + "unknown": "Unbekannt" + }, + "reserve_now": { + "title": "Reservierung", + "tooltips": "Jetzt Reservieren", + "confirm": "Wollen Sie für den Nutzer '{{userName}}' an der Ladestation '{{chargingStationID}}' an Anschluss '{{connectorID}}' eine Reservierung tätigen?", + "details": "Anschluss '{{connectorID}}' an Ladestation '{{chargingStationID}}' reservieren", + "success": "Die Reservierung an Ladestation '{{chargingStationID}}' für Anschluss '{{connectorID}}' wurde erfolgreich durchgeführt", + "error": "Beim Versuch eine Reservierung zu tätigen ist ein Fehler aufgetreten" + }, + "cancel_reservation": { + "title": "Reservierung stornieren", + "tooltips": "Reservierung stornieren", + "confirm": "Wollen Sie an der Ladestation '{{chargingStationID}}' die Reservierung stornieren?'", + "details": "", + "success": "Die Reservierung an der Ladestation '{{chargingStationID}}' wurde erfolgreich storniert", + "error": "Beim Versuch die Reservierung an der Ladestation '{{chargingStationID}}' zu stornieren ist ein Fehler aufgetreten" + }, + "create": { + "title": "Reservierung erstellen", + "tooltips": "Reservierung erstellen", + "confirm": "", + "details": "", + "success": "Die Reservierung an Ladestation '{{chargingStationID}}' für Anschluss '{{connectorID}}' wurde erfolgreich erstellt", + "error": "Beim Erstellen der Reservierung ist ein Fehler aufgetreten" + }, + "update": { + "title": "Reservierung aktualisieren", + "tooltips": "Reservierung aktualisieren", + "confirm": "", + "details": "", + "success": "Die Reservierung an Ladestation '{{chargingStationID}}' für Anschluss '{{connectorID}}' wurde erfolgreich aktualisiert", + "error": "Die Reservierung kann nicht verändert werden, siehe Log-Nachrichten für Details" + }, + "delete": { + "title": "Reservierung löschen", + "tooltips": "Reservierung löschen", + "confirm": "Sind Sie sich sicher, dass Sie die Reservierung löschen wollen?", + "details": "", + "success": "Die Reservierung an Ladestation '{{chargingStationID}}' für Anschluss '{{connectorID}}' wurde erfolgreich gelöscht", + "error": "Beim Löschen der Reservierung ist ein Fehler aufgetreten" + }, + "action_error": { + "general": { + "not_found": "Die Reservierung wurde nicht gefunden", + "already_exists": "Eine Reservierung mit der gleichen ID existiert bereits für einen anderen Nutzer", + "not_supported": "Das Feature 'Reservierungen' ist für diese Ladestation nicht aktiviert", + "rejected": "The reservation was rejected", + "collision": "Die Reservierung kollidiert mit anderen Reservierung(en) für den gewünschten Zeitraum. Wählen Sie ein anderes Zeitfenster aus", + "faulted": "Die gewünschte Ladesäule oder der gewünschte Anschluss änderten ihren Status in 'FEHLERHAFT' während der Reservierung", + "occupied": "Die gewünschte Ladesäule oder der gewünschte Anschluss änderten ihren Status in 'BESETZT' während der Reservierung", + "unavailable": "Die gewünschte Ladesäule oder der gewünschte Anschluss änderten ihren Status in 'NICHT VERFÜGBAR' während der Reservierung", + "multiple_reserve_now": "Es ist nur eine 'RESERVE NOW' Reservierung pro Nutzer zeitgleich möglich" + } + } } -} +} \ No newline at end of file diff --git a/src/I18n/languages/en.json b/src/I18n/languages/en.json index f8370748b..225fcbabe 100644 --- a/src/I18n/languages/en.json +++ b/src/I18n/languages/en.json @@ -89,7 +89,8 @@ "userOrTenantUpdated": "You have been logged off because either your profile or your organization's settings have been updated", "tenantRedirectionInvalidDomain": "Invalid tenant redirection domain", "from": "From", - "to": "To" + "to": "To", + "notification": "Notification" }, "sidebar": { "home": "Home", @@ -105,7 +106,8 @@ "invoices": "Invoices", "paymentMethods": "Payment methods", "qrCodeScanner": "QR-Code scanner", - "settings": "Settings" + "settings": "Settings", + "reservations": "Reservations" }, "authentication": { "noTenantFoundTitle": "No organization found", @@ -392,6 +394,7 @@ "title": "Charging Stations", "heartBeat": "Heartbeat", "chargers": "Charging Station(s)", + "charger": "Charging Station", "noOCPPParameters": "No OCPP parameter", "noChargerParameters": "No Charging Station parameter", "chargerConfigurationUnexpectedError": "Cannot read the configuration of the charging station", @@ -417,7 +420,10 @@ "ocpp": "OCPP", "actions": "Actions", "noSession": "No Session", - "noSessionMessage": "No session has been done on that connector" + "noSessionMessage": "No session has been done on that connector", + "noChargerMessageTitle": "No Charging Station has been selected", + "selectCharger": "Select Charging Station", + "selectChargers": "Select Charging Station(s)" }, "connector": { "occupied": "Occupied", @@ -537,6 +543,42 @@ "message": "An unknown user has just badged on '{{chargeBoxID}}'.", "subMessage": "Unknown User", "longMessage": "An unknown user has just badged on '{{chargeBoxID}}'." + }, + "reservationCreatedNotification": { + "title": "Reservation Created", + "body": "Your reservation for charging station '{{chargingStationID}}' on connector '{{connectorID}}' has been created successfully." + }, + "reservationCancelledNotification": { + "title": "Reservation Cancelled", + "body": "Your reservation for the charging station '{{chargingStationID}}' on connector '{{connectorID}}' has been cancelled successfully." + }, + "reservationUpcomingNotification": { + "title": "Reservation Upcoming", + "body": "Your reservation on the charging station '{{chargingStationID}}' at connector '{{connectorID}}' is upcoming." + }, + "reservationUpcomingWarning": { + "title": "Reservation Upcoming", + "body": "There is a reservation upcoming from '{{fromDate}}' to '{{toDate}}' on this charging station '{{chargingStationID}}' at connector '{{connectorID}}'." + }, + "reservedChargingStationBlockedNotification": { + "title": "Reserved Charging Station Blocked", + "body": "At your currently reserved charging station '{{chargingStationID}}' on connector '{{connectorID}}' a charging session started." + }, + "reservationStatusChangedNotification": { + "title": "Reservation Status Changed", + "body": "Your reservation for charging station '{{chargingStationID}}' on connector '{{connectorID}}' changed its status.", + "status": { + "reservation_done": "Reservation Done", + "reservation_scheduled": "Reservation Scheduled", + "reservation_in_progress": "Reservation In Progress", + "reservation_cancelled": "Reservation Cancelled", + "reservation_expired": "Reservation Expired", + "reservation_unmet": "Reservation Unmet" + } + }, + "reservationUnmetNotification": { + "title": "Reservation Unmet", + "body": "The reservation for charging station '{{chargingStationID}}' on connector '{{connectorID}}' is cancelled after absence of 15 minutes after reservation start." } }, "appUpdate": { @@ -551,12 +593,106 @@ "sitesRoamingFilterLabel": "Show roaming sites", "siteAreasRoamingFilterLabel": "Show roaming site areas", "transactionsRoamingFilterLabel": "Show roaming sessions", - "usersRoamingFilterLabel": "Show roaming users" + "usersRoamingFilterLabel": "Show roaming users", + "activeReservationsFilterLabel": "Show active reservations" }, "settingsDistanceUnit": { "distanceUnit": "Distance unit", "kilometers": "Kilometers", "miles": "Miles", "automatic": "Automatic" + }, + "reservations": { + "titles": "Reservations", + "description": "Manage reservations on charging stations and connectors", + "title": "Reservation", + "id": "ID", + "chargingstationId": "Charging Station", + "connectorId": "Connector", + "connectorStatus": "Connector Status", + "expiryDate": "Expiration Date", + "fromDate": "From", + "toDate": "To", + "arrivalTime": "Arrival Time", + "departureTime": "Departure Time", + "tagId": "Tag", + "status": "Status", + "type": "Reservation Type", + "user": "User", + "car": "Car", + "reservationsUnexpectedError": "Cannot read reservations", + "reservationDoesNotExist": "Reservation does not exist", + "noReservations": "No reservations found", + "invalidDate": "Invalid date.", + "dateBeforeMinimum": "Invalid date. Date must be in the future.", + "dateAfterMaximum": "Invalid date. Date exceeds maximum duration of {{duration}} hours.", + "invalidDateRange": "Invalid date range. End date is before start date", + "statuses": { + "reservation_done": "Done", + "reservation_scheduled": "Scheduled", + "reservation_in_progress": "In Progress", + "reservation_cancelled": "Cancelled", + "reservation_expired": "Expired", + "reservation_unmet": "Unmet", + "unknown": "Unknown" + }, + "types": { + "reserve_now": "Reserve Now", + "planned_reservation": "Planned", + "unknown": "Unknown" + }, + "reserve_now": { + "title": "Reserve Now", + "tooltips": "Reserve Now", + "confirm": "Do you really want to reserve the connector '{{connectorID}}' for user '{{userName}}' on the charging station '{{chargingStationID}}'?", + "details": "Reserve connector '{{connectorID}}' at chargepoint '{{chargingStationID}}'", + "success": "The reservation on the charging station '{{chargingStationID}}' at connector '{{connectorID}}' was successfully", + "error": "Error occurred while trying to create a reservation, check the logs" + }, + "cancel_reservation": { + "title": "Cancel Reservation", + "tooltips": "Cancel Reservation", + "confirm": "Do you really want to cancel the reservation at charging station '{{chargingStationID}}'?", + "details": "", + "success": "The reservation at charging station '{{chargingStationID}}' is successfully canceled", + "error": "Error occured while trying to cancel the reservation at charging station '{{chargingStationID}}'" + }, + "create": { + "title": "Create Reservation", + "tooltips": "Create Reservation", + "confirm": "", + "details": "", + "success": "Reservation on '{{chargingStationID}}' at connector '{{connectorID}}' has been created successfully", + "error": "Error occurred while adding the reservation, check the logs" + }, + "update": { + "title": "Update Reservation", + "tooltips": "Update Reservation", + "confirm": "", + "details": "", + "success": "The reservation has been saved successfully", + "error": "Cannot change the reservation details, check the logs" + }, + "delete": { + "title": "Delete Reservation", + "tooltips": "Delete Reservation", + "confirm": "Do you really want to delete the reservation?", + "details": "", + "success": "The reservation on '{{chargingStationID}}' at connector '{{connectorID}}' is successfully deleted", + "error": "An error occurred during the deletion of the reservation, check the logs" + }, + "action_error": { + "general": { + "not_found": "The reservation has not been found", + "already_exists": "The reservation with the same ID already exists for another user", + "not_supported": "The feature 'Reservations' is not enabled for this charging station", + "rejected": "The reservation was rejected", + "collision": "This reservation collides with another reservation for this time slot. Choose another time", + "faulted": "The specified charging station or connector changed to 'FAULTED' state during reservation", + "occupied": "The specified charging station or connector changed to 'OCCUPIED' state during reservation", + "unavailable": "The specified charging station or connector changed to 'UNAVAILABLE' state during reservation", + "multiple_reserve_now": "Only one 'RESERVE NOW' reservation per user is possible" + } + } } -} +} \ No newline at end of file diff --git a/src/components/charging-station/ReservableChargingStationComponent.tsx b/src/components/charging-station/ReservableChargingStationComponent.tsx new file mode 100644 index 000000000..81b8b95ee --- /dev/null +++ b/src/components/charging-station/ReservableChargingStationComponent.tsx @@ -0,0 +1,128 @@ +import I18n from 'i18n-js'; +import moment from 'moment'; +import { Icon, Text, View } from 'native-base'; +import React from 'react'; +import { TouchableOpacity, ViewStyle } from 'react-native'; +import * as Animatable from 'react-native-animatable'; +import { scale } from 'react-native-size-matters'; +import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; +import DialogModal from '../../components/modal/DialogModal'; +import BaseProps from '../../types/BaseProps'; +import ChargingStation from '../../types/ChargingStation'; +import Utils from '../../utils/Utils'; +import computeModalCommonStyle from '../modal/ModalCommonStyle'; +import computeStyleSheet from './ReservableChargingStationComponentStyles'; +import computeListItemCommonStyle from '../list/ListItemCommonStyle'; + +export interface Props extends BaseProps { + chargingStation: ChargingStation; + selected?: boolean; + containerStyle?: ViewStyle[]; +} + +interface State { + showHeartbeatDialog: boolean; +} + +export default class ReservableChargingStationComponent extends React.Component { + public state: State; + public props: Props; + + public constructor(props: Props) { + super(props); + this.state = { + showHeartbeatDialog: false + }; + } + + public setState = ( + state: State | ((prevState: Readonly, props: Readonly) => State | Pick) | Pick, + callback?: () => void + ) => { + super.setState(state, callback); + }; + + public render() { + const style = computeStyleSheet(); + const listItemCommonStyle = computeListItemCommonStyle(); + const { chargingStation, containerStyle } = this.props; + const { showHeartbeatDialog } = this.state; + return ( + + {showHeartbeatDialog && this.renderHeartbeatStatusDialog()} + + { + this.setState({ showHeartbeatDialog: true }); + }}> + {chargingStation.inactive ? ( + + + + ) : ( + + + + )} + + + + + {chargingStation.id} + + {chargingStation.siteArea && ( + + {Utils.formatAddress(chargingStation.siteArea?.address)} + + )} + + {chargingStation.connectors.map((connector) => ( + + {Utils.translateConnectorType(connector.type)} + + ))} + + + + ); + } + + private renderHeartbeatStatusDialog() { + const { chargingStation } = this.props; + const modalCommonStyle = computeModalCommonStyle(); + let message = I18n.t('chargers.heartBeatOkMessage', { chargeBoxID: chargingStation.id }); + if (chargingStation.inactive) { + message = I18n.t('chargers.heartBeatKoMessage', { + chargeBoxID: chargingStation.id, + lastSeen: moment(new Date(chargingStation.lastSeen), null, true).fromNow(true) + }); + } + return ( + this.setState({ showHeartbeatDialog: false })} + onBackButtonPressed={() => this.setState({ showHeartbeatDialog: false })} + description={message} + buttons={[ + { + text: I18n.t('general.ok'), + buttonStyle: modalCommonStyle.primaryButton, + buttonTextStyle: modalCommonStyle.primaryButtonText, + action: () => this.setState({ showHeartbeatDialog: false }) + } + ]} + /> + ); + } + private renderChargingStationConnectors(style: any, chargingStation: ChargingStation) { + return chargingStation.connectors.map((connector) => ( + + + {Utils.translateConnectorType(connector.type)} + + + )); + } +} diff --git a/src/components/charging-station/ReservableChargingStationComponentStyles.tsx b/src/components/charging-station/ReservableChargingStationComponentStyles.tsx new file mode 100644 index 000000000..cf7329649 --- /dev/null +++ b/src/components/charging-station/ReservableChargingStationComponentStyles.tsx @@ -0,0 +1,81 @@ +import deepmerge from 'deepmerge'; +import { StyleSheet } from 'react-native'; +import ResponsiveStylesSheet from 'react-native-responsive-stylesheet'; +import { ScaledSheet } from 'react-native-size-matters'; + +import Utils from '../../utils/Utils'; + +export default function computeStyleSheet(): StyleSheet.NamedStyles { + const commonColor = Utils.getCurrentCommonColor(); + const commonStyles = ScaledSheet.create({ + selected: { + backgroundColor: commonColor.listItemSelected + }, + unselected: { + backgroundColor: commonColor.listHeaderBgColor + }, + chargingStationContainer: { + height: '90@s', + justifyContent: 'space-between', + alignItems: 'center', + flexDirection: 'row', + margin: 0, + padding: '5@s' + }, + contentContainer: { + height: '100%', + justifyContent: 'center', + flexDirection: 'column', + alignItems: 'flex-start', + paddingVertical: '5@s', + flex: 1 + }, + evIconContainer: { + marginLeft: '20@s', + height: '100%', + paddingRight: '20@s', + alignItems: 'center', + justifyContent: 'center' + }, + text: { + color: commonColor.textColor, + fontSize: '13@s' + }, + title: { + fontSize: '14@s', + width: '100%', + fontWeight: 'bold' + }, + bottomLine: { + flexDirection: 'row', + alignItems: 'flex-start' + }, + evStationIcon: { + color: commonColor.success, + paddingLeft: '20@s', + fontSize: '25@s' + }, + deadEvStationIcon: { + color: commonColor.danger, + paddingLeft: '20@s', + fontSize: '25@s' + }, + defaultContainer: { + borderRadius: '2@s', + justifyContent: 'center', + marginRight: '5@s', + marginTop: '5@s' + }, + defaultText: { + fontSize: '10@s', + color: commonColor.light, + paddingHorizontal: '3@s' + } + }); + const portraitStyles = {}; + const landscapeStyles = {}; + return ResponsiveStylesSheet.createOriented({ + landscape: deepmerge(commonStyles, landscapeStyles) as StyleSheet.NamedStyles, + portrait: deepmerge(commonStyles, portraitStyles) as StyleSheet.NamedStyles + }); +} diff --git a/src/components/date-time/DateTimePickerComponent.tsx b/src/components/date-time/DateTimePickerComponent.tsx new file mode 100644 index 000000000..737b7780d --- /dev/null +++ b/src/components/date-time/DateTimePickerComponent.tsx @@ -0,0 +1,175 @@ +import React from 'react'; +import { Icon } from 'native-base'; + +import I18n from 'i18n-js'; +import { Text, TouchableOpacity, View, ViewStyle } from 'react-native'; +import I18nManager from '../../I18n/I18nManager'; +import computeStyleSheet from './DateTimePickerComponentStyles'; +import Utils from '../../utils/Utils'; +import { scale } from 'react-native-size-matters'; +import DateTimePicker from 'react-native-modal-datetime-picker'; +import Message from '../../utils/Message'; +import moment from 'moment'; +import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; + +export interface Props { + title: string; + initialValue?: Date; + lowerBound?: Date; + upperBound?: Date; + mode?: 'date' | 'time' | 'datetime'; + locale?: string; + is24Hour?: boolean; + containerStyle?: ViewStyle[]; + onDateTimeChanged: (newDateTime: Date) => Promise | void; +} + +interface State { + value: Date; + openDateTimePicker: boolean; + isValid: boolean; +} + +export default class DateTimePickerComponent extends React.Component { + public props: Props; + public state: State; + + public constructor(props: Props) { + super(props); + this.onConfirm.bind(this); + this.state = { + value: null, + openDateTimePicker: false, + isValid: false + }; + } + + public setState = ( + state: State | ((prevState: Readonly, props: Readonly) => State | Pick) | Pick, + callback?: () => void + ) => { + super.setState(state, callback); + }; + + public canBeSaved() { + return true; + } + + public render() { + const style = computeStyleSheet(); + const { title, mode, initialValue, lowerBound, upperBound, locale, is24Hour, containerStyle } = this.props; + const commonColors = Utils.getCurrentCommonColor(); + return ( + + this.setState({ openDateTimePicker: true })}> + + + + + + {I18n.t(title)} + + {['date', 'datetime'].includes(mode) && ( + + {I18nManager.formatDateTime(initialValue, { dateStyle: 'medium' })} + + )} + {['time', 'datetime'].includes(mode) && ( + + {I18nManager.formatDateTime(initialValue, { timeStyle: 'medium' })} + + )} + + + this.onConfirm(newDateTime)} + onCancel={() => this.setState({ openDateTimePicker: false })} + /> + + ); + } + + private onConfirm(newValue: Date) { + const { onDateTimeChanged } = this.props; + // Workaround to fix the bug from react-native-modal-datetime-picker + newValue = this.validateValue(newValue); + this.setState({ openDateTimePicker: false, value: newValue }, () => onDateTimeChanged?.(newValue)); + } + + private fitDateBetweenMinAndMax(date: Date, granularity: moment.unitOfTime.StartOf = 'date'): Date { + const { upperBound, lowerBound } = this.props; + const parsedInput = moment(date); + const parsedMinimum = moment(lowerBound); + const parsedMaximum = moment(upperBound); + if (date && parsedInput.isValid()) { + if (parsedInput.isBefore(parsedMinimum, granularity)) { + Message.showError(I18n.t('reservations.dateBeforeMinimum')); + this.setState({ isValid: false }); + return lowerBound; + } else if (parsedInput.isAfter(parsedMaximum, granularity)) { + Message.showError(I18n.t('reservations.dateAfterMaximum', { duration: parsedInput.diff(parsedMaximum, 'h') })); + this.setState({ isValid: false }); + return upperBound; + } + this.setState({ isValid: true }); + } else { + Message.showError(I18n.t('reservations.invalidDate')); + this.setState({ isValid: false }); + } + return date; + } + + private selectIconByMode(mode: 'date' | 'datetime' | 'time') { + switch (mode) { + case 'date': + return 'calendar'; + case 'datetime': + return 'calendar-clock'; + case 'time': + return 'clock'; + } + } + + private determineIconStyle(style: any) { + if (!this.state.value) { + return style.defaultDateIcon; + } + return this.state.isValid ? style.validDateIcon : style.invalidDateIcon; + } + + private determineRequiredHeight(mode: 'date' | 'datetime' | 'time', style: any) { + switch (mode) { + case 'date': + return style.dateContainer; + case 'time': + return style.timeContainer; + case 'datetime': + return style.dateTimeContainer; + } + } + + private fitTimeBetweenMinAndMax(time: Date) { + return this.fitDateBetweenMinAndMax(this.fitDateBetweenMinAndMax(time,'h'),'m'); + } + + private validateValue(value: Date) { + const { mode } = this.props; + return mode === 'time' ? this.fitTimeBetweenMinAndMax(value) : this.fitDateBetweenMinAndMax(value); + } +} diff --git a/src/components/date-time/DateTimePickerComponentStyles.tsx b/src/components/date-time/DateTimePickerComponentStyles.tsx new file mode 100644 index 000000000..51992326c --- /dev/null +++ b/src/components/date-time/DateTimePickerComponentStyles.tsx @@ -0,0 +1,80 @@ +import deepmerge from 'deepmerge'; +import { StyleSheet } from 'react-native'; +import ResponsiveStylesSheet from 'react-native-responsive-stylesheet'; +import { ScaledSheet } from 'react-native-size-matters'; + +import Utils from '../../utils/Utils'; + +export default function computeStyleSheet(): StyleSheet.NamedStyles { + const commonColor = Utils.getCurrentCommonColor(); + const commonStyles = ScaledSheet.create({ + dateTimeContainer: { + height: '90@s', + justifyContent: 'space-between', + alignItems: 'center', + flexDirection: 'row', + margin: 0, + padding: '5@s' + }, + dateContainer: { + height: '70@s', + justifyContent: 'space-between', + alignItems: 'center', + flexDirection: 'row', + margin: 0, + padding: '5@s' + }, + timeContainer: { + height: '60@s', + justifyContent: 'space-between', + alignItems: 'center', + flexDirection: 'row', + margin: 0, + padding: '5@s' + }, + contentContainer: { + height: '100%', + justifyContent: 'center', + flexDirection: 'column', + alignItems: 'flex-start', + paddingVertical: '5@s', + width: '100%' + }, + calendarIconContainer: { + marginLeft: '20@s', + height: '100%', + paddingRight: '20@s', + alignItems: 'center', + justifyContent: 'center' + }, + text: { + color: commonColor.textColor, + fontSize: '13@s' + }, + title: { + fontSize: '14@s', + width: '100%', + fontWeight: 'bold' + }, + bottomLine: { + flexDirection: 'row', + alignItems: 'center', + width: '100%' + }, + defaultDateIcon: { + color: commonColor.disabled + }, + validDateIcon: { + color: commonColor.success + }, + invalidDateIcon: { + color: commonColor.danger + } + }); + const portraitStyles = {}; + const landscapeStyles = {}; + return ResponsiveStylesSheet.createOriented({ + landscape: deepmerge(commonStyles, landscapeStyles) as StyleSheet.NamedStyles, + portrait: deepmerge(commonStyles, portraitStyles) as StyleSheet.NamedStyles + }); +} diff --git a/src/components/fab/FabComponentStyles.tsx b/src/components/fab/FabComponentStyles.tsx index 9c06a77f6..b60b3a68a 100644 --- a/src/components/fab/FabComponentStyles.tsx +++ b/src/components/fab/FabComponentStyles.tsx @@ -5,7 +5,7 @@ import { ScaledSheet } from 'react-native-size-matters'; import Utils from '../../utils/Utils'; -export default function computeStyleSheet(): StyleSheet.NamedStyles { +export default function computeStyleSheet(color?: string): StyleSheet.NamedStyles { const commonColor = Utils.getCurrentCommonColor(); const commonStyles = ScaledSheet.create({ fab: { @@ -18,7 +18,7 @@ export default function computeStyleSheet(): StyleSheet.NamedStyles { right: 0, zIndex: 1, elevation: 4, - backgroundColor: commonColor.primary, + backgroundColor: color ?? commonColor.primary, shadowOffset: { width: 0, height: 1 diff --git a/src/components/map/ClusterMap.tsx b/src/components/map/ClusterMap.tsx index 46245f741..b179491af 100644 --- a/src/components/map/ClusterMap.tsx +++ b/src/components/map/ClusterMap.tsx @@ -26,12 +26,18 @@ export default class ClusterMap extends React.Component

; public state: State; private darkMapTheme = require('../../utils/map/google-maps-night-style.json'); + private mapRef: React.RefObject; public constructor(props: Props) { super(props); this.state = { satelliteMap: false - } + }; + this.mapRef = React.createRef(); + } + + public animateToRegion(region: Region, duration?: number) { + this.mapRef.current?.animateToRegion(region, duration); } public render() { @@ -44,6 +50,7 @@ export default class ClusterMap extends React.Component

{initialRegion && ( extends React.Component

onMapRegionChangeComplete(region)} - > - {items.map((item, index) => ( - renderMarker?.(item, index) - ))} + showsUserLocation={true} + onRegionChangeComplete={(region) => onMapRegionChangeComplete(region)}> + {items.map((item, index) => renderMarker?.(item, index))} )} diff --git a/src/components/reservation/ReservationComponent.tsx b/src/components/reservation/ReservationComponent.tsx new file mode 100644 index 000000000..88067dd0b --- /dev/null +++ b/src/components/reservation/ReservationComponent.tsx @@ -0,0 +1,92 @@ +import { View, ViewStyle, Text, TouchableOpacity } from 'react-native'; +import { Icon } from 'native-base'; +import BaseProps from '../../types/BaseProps'; +import Reservation from '../../types/Reservation'; +import React from 'react'; +import computeStyleSheet from './ReservationComponentStyles'; +import computeListItemCommonStyle from '../list/ListItemCommonStyle'; +import ReservationHeaderComponent from './header/ReservationHeaderComponent'; +import Utils from '../../utils/Utils'; +import { scale } from 'react-native-size-matters'; +import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; + +export interface Props extends BaseProps { + reservation: Reservation; + isSmartChargingActive: boolean; + isCarActive: boolean; + containerStyle?: ViewStyle[]; + isAdmin: boolean; + isSiteAdmin: boolean; + visible?: boolean; +} + +interface State {} + +export default class ReservationComponent extends React.Component { + public state: State; + public props: Props; + + constructor(props: Props) { + super(props); + this.state = { + isVisible: this.props?.visible + }; + } + + public setState = ( + state: State | ((prevState: Readonly, props: Readonly) => State | Pick) | Pick, + callback?: () => void + ) => { + super.setState(state, callback); + }; + + public render() { + const style = computeStyleSheet(); + const listItemCommonStyle = computeListItemCommonStyle(); + const { navigation, containerStyle } = this.props; + const { reservation, isAdmin, isSiteAdmin, isSmartChargingActive, isCarActive } = this.props; + const connector = Utils.getConnectorFromID(reservation.chargingStation, reservation.connectorID); + const car = !reservation.car ? '-' : reservation?.car.carCatalog.vehicleModel; + return ( + + + { + navigation.navigate('ReservationDetailsTabs', { + params: { reservationID: reservation.id }, + key: `${Utils.randomNumber()}` + }); + }}> + + + + {Utils.buildConnectorTypeSVG(connector?.type, null, 25)} + {Utils.translateConnectorType(connector.type)} + + + {Utils.buildReservationStatusIcon(reservation.status, style)} + + {Utils.translateReservationStatus(reservation.status)} + + + + {Utils.buildReservationTypeIcon(reservation.type, style)} + + {Utils.translateReservationType(reservation.type)} + + + {isCarActive && ( + + + + {car} + + + )} + + + + ); + } +} diff --git a/src/components/reservation/ReservationComponentStyles.tsx b/src/components/reservation/ReservationComponentStyles.tsx new file mode 100644 index 000000000..bbd7f9d15 --- /dev/null +++ b/src/components/reservation/ReservationComponentStyles.tsx @@ -0,0 +1,119 @@ +import deepmerge from 'deepmerge'; +import { StyleSheet } from 'react-native'; +import ResponsiveStylesSheet from 'react-native-responsive-stylesheet'; +import { ScaledSheet } from 'react-native-size-matters'; + +import Utils from '../../utils/Utils'; + +export default function computeStyleSheet(): StyleSheet.NamedStyles { + const commonColor = Utils.getCurrentCommonColor(); + const commonStyles = ScaledSheet.create({ + reservationContainer: { + flexDirection: 'row', + justifyContent: 'space-between' + }, + reservationContent: { + flex: 1, + justifyContent: 'space-between', + padding: '10@s', + alignItems: 'center' + }, + leftContainer: { + flexDirection: 'column', + alignItems: 'flex-start', + margin: '5@s', + flex: 1 + }, + statusIndicator: { + height: '100%', + width: '5@s', + borderTopLeftRadius: '8@s', + borderBottomLeftRadius: '8@s' + }, + reservationStatusCancelled: { + backgroundColor: commonColor.danger + }, + reservationStatusUnmet: { + backgroundColor: commonColor.disabledDark + }, + reservationStatusScheduled: { + backgroundColor: commonColor.info + }, + reservationStatusInProgress: { + backgroundColor: commonColor.warning + }, + reservationStatusDone: { + backgroundColor: commonColor.success + }, + label: { + color: commonColor.textColor, + fontSize: '10@s', + marginTop: '-3@s' + }, + info: { + color: commonColor.textColor + }, + success: { + color: commonColor.success + }, + warning: { + color: commonColor.warning + }, + danger: { + color: commonColor.danger + }, + reservationDetailsContainer: { + flex: 1, + flexDirection: 'row', + width: '100%', + justifyContent: 'space-between', + height: '100%' + }, + reservationDetailContainer: { + marginTop: '3@s', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + width: '25%' + }, + connectorDetail: { + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + height: '60@s', + flexGrow: 1, + color: commonColor.textColor + }, + icon: { + color: commonColor.textColor, + justifyContent: 'flex-end' + }, + arrowIcon: { + fontSize: '18@s', + color: commonColor.disabledDark + }, + labelValue: { + fontSize: '13@s', + color: commonColor.textColor + }, + subLabelValue: { + fontSize: '10@s', + color: commonColor.textColor + }, + connectorSVG: { + width: '40@s', + height: '40@s' + }, + labelImage: { + color: commonColor.textColor, + paddingTop: '2@s', + fontSize: '10@s' + } + }); + const portraitStyles = {}; + const landscapeStyles = {}; + return ResponsiveStylesSheet.createOriented({ + landscape: deepmerge(commonStyles, landscapeStyles) as StyleSheet.NamedStyles, + portrait: deepmerge(commonStyles, portraitStyles) as StyleSheet.NamedStyles + }); +} diff --git a/src/components/reservation/header/ReservationHeaderComponent.tsx b/src/components/reservation/header/ReservationHeaderComponent.tsx new file mode 100644 index 000000000..883d45b6e --- /dev/null +++ b/src/components/reservation/header/ReservationHeaderComponent.tsx @@ -0,0 +1,63 @@ +import { Icon } from 'native-base'; +import React from 'react'; +import { Text, View } from 'react-native'; +import { scale } from 'react-native-size-matters'; + +import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; +import I18nManager from '../../../I18n/I18nManager'; +import BaseProps from '../../../types/BaseProps'; +import Reservation from '../../../types/Reservation'; +import Utils from '../../../utils/Utils'; +import computeStyleSheet from './ReservationHeaderComponentStyles'; + +export interface Props extends BaseProps { + reservation: Reservation; + isAdmin: boolean; + isSiteAdmin: boolean; + visible?: boolean; +} + +interface State {} + +export default class ReservationHeaderComponent extends React.Component { + public state: State; + public props: Props; + + public constructor(props: Props) { + super(props); + this.state = { + isVisible: this.props.visible + }; + } + + public setState = ( + state: State | ((prevState: Readonly, props: Readonly) => State | Pick) | Pick, + callback?: () => void + ) => { + super.setState(state, callback); + }; + + public render() { + const style = computeStyleSheet(); + const { reservation, isAdmin, isSiteAdmin } = this.props; + return ( + + + + {I18nManager.formatDateTime(reservation.fromDate, { dateStyle: 'short' })} -{' '} + {I18nManager.formatDateTime(reservation.toDate, { dateStyle: 'short' })} + + + + + {reservation.chargingStationID} - {Utils.getConnectorLetterFromConnectorID(reservation.connectorID)} + + {(isAdmin || isSiteAdmin) && reservation.tag.user && ( + + {Utils.buildUserName(reservation.tag.user)} ({reservation.tag.user.email}) + + )} + + ); + } +} diff --git a/src/components/reservation/header/ReservationHeaderComponentStyles.tsx b/src/components/reservation/header/ReservationHeaderComponentStyles.tsx new file mode 100644 index 000000000..5a858e60b --- /dev/null +++ b/src/components/reservation/header/ReservationHeaderComponentStyles.tsx @@ -0,0 +1,46 @@ +import deepmerge from 'deepmerge'; +import { StyleSheet } from 'react-native'; +import ResponsiveStylesSheet from 'react-native-responsive-stylesheet'; +import { ScaledSheet } from 'react-native-size-matters'; + +import Utils from '../../../utils/Utils'; + +export default function computeStyleSheet(): StyleSheet.NamedStyles { + const commonColor = Utils.getCurrentCommonColor(); + const commonStyles = ScaledSheet.create({ + container: { + width: '100%', + flexDirection: 'column' + }, + firstLine: { + flexDirection: 'row', + alignItems: 'center' + }, + reservationTimeRange: { + color: commonColor.headerTextColor, + fontSize: '16@s', + fontWeight: 'bold', + flex: 1 + }, + subHeaderName: { + paddingTop: '2@s', + color: commonColor.headerTextColor + }, + chargingStationName: { + fontSize: '14@s' + }, + userFullName: { + fontSize: '13@s' + }, + arrowIcon: { + color: commonColor.disabledDark, + marginLeft: '20@s' + } + }); + const portraitStyles = {}; + const landscapeStyles = {}; + return ResponsiveStylesSheet.createOriented({ + landscape: deepmerge(commonStyles, landscapeStyles) as StyleSheet.NamedStyles, + portrait: deepmerge(commonStyles, portraitStyles) as StyleSheet.NamedStyles + }); +} diff --git a/src/components/search/filter/controls/date/DateFilterControlComponent.tsx b/src/components/search/filter/controls/date/DateFilterControlComponent.tsx index 6fecc4897..ab3f83883 100644 --- a/src/components/search/filter/controls/date/DateFilterControlComponent.tsx +++ b/src/components/search/filter/controls/date/DateFilterControlComponent.tsx @@ -1,4 +1,4 @@ -import { Icon } from 'native-base'; +import { Icon } from 'native-base'; import React from 'react'; import { TouchableOpacity, Text, View } from 'react-native'; import DateTimePickerModal from 'react-native-modal-datetime-picker'; @@ -15,6 +15,7 @@ export interface Props extends FilterControlComponentProps { minimumDate: Date; maximumDate: Date; defaultValue: Date; + dateMode?: 'date' | 'time' | 'datetime'; } interface State extends FilterControlComponentState { @@ -30,14 +31,16 @@ export default class DateFilterControlComponent extends FilterControlComponent, prevState: Readonly, snapshot?: any) { const { initialValue } = this.props; // If filter is not aware of initialValue change, put new initialValue to state - if ( (initialValue?.getTime() !== prevProps.initialValue?.getTime()) && (this.state.value?.getTime() !== initialValue?.getTime()) ) { - this.setState({value: initialValue }); + if (initialValue?.getTime() !== prevProps.initialValue?.getTime() && this.state.value?.getTime() !== initialValue?.getTime()) { + this.setState({ value: initialValue }); } } @@ -45,7 +48,7 @@ export default class DateFilterControlComponent extends FilterControlComponent { const internalStyle = computeStyleSheet(); - const { label, minimumDate, maximumDate, locale, style, defaultValue } = this.props; + const { label, minimumDate, maximumDate, locale, style, defaultValue, dateMode } = this.props; let { value } = this.state; - value = value ?? defaultValue ; + value = value ?? defaultValue; return ( {label} {value ? ( - this.setState({openDatePicker: true})}> - {I18nManager.formatDateTime(value, {dateStyle: 'medium'})} + this.setState({ openDatePicker: true })}> + + {I18nManager.formatDateTime(value, { dateStyle: 'medium' })} + { - this.setState({openDatePicker: false}); + this.setState({ openDatePicker: false }); }} onConfirm={(date) => { this.onConfirm(date); diff --git a/src/components/search/filter/controls/date/date-time/DateTimeFilterControlComponent.tsx b/src/components/search/filter/controls/date/date-time/DateTimeFilterControlComponent.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/src/config/Configuration.tsx b/src/config/Configuration.tsx index bbab3ce4b..10e3beb2f 100644 --- a/src/config/Configuration.tsx +++ b/src/config/Configuration.tsx @@ -25,6 +25,11 @@ export default class Configuration { name: 'android-local:8080', endpoint: 'http://10.0.2.2:8080' }, + { + id: '10.0.2.2:81', + name: 'android-local:81', + endpoint: 'http://10.0.2.2:81' + }, { id: 'kubernetes', name: 'QA', diff --git a/src/custom-theme/customCommonColor.tsx b/src/custom-theme/customCommonColor.tsx index 9288ad921..3dd77f0b7 100644 --- a/src/custom-theme/customCommonColor.tsx +++ b/src/custom-theme/customCommonColor.tsx @@ -16,7 +16,8 @@ export const buildCommonColor = (currentTheme: ThemeDefinition) => ({ primaryLight: currentTheme.primaryLight, yellow: '#FFBE59', - purple: 'purple', + purple: '#7B1FA2', + amber: '#FFA000', mapClusterBorder: currentTheme.mapClusterBorder, diff --git a/src/deeplinking/DeepLinkingManager.tsx b/src/deeplinking/DeepLinkingManager.tsx index 42f8d8c36..ccd284ac0 100644 --- a/src/deeplinking/DeepLinkingManager.tsx +++ b/src/deeplinking/DeepLinkingManager.tsx @@ -15,6 +15,7 @@ export default class DeepLinkingManager { public static readonly PATH_TRANSACTIONS = 'transactions'; public static readonly PATH_LOGIN = 'login'; public static readonly PATH_INVOICES = 'invoices'; + public static readonly PATH_RESERVATIONS = 'reservations'; public static readonly FRAGMENT_IN_ERROR = 'inerror'; public static readonly FRAGMENT_ALL = 'all'; public static readonly FRAGMENT_IN_PROGRESS = 'inprogress'; diff --git a/src/notification/Notifications.tsx b/src/notification/Notifications.tsx index cd70fe23a..8473c9b59 100644 --- a/src/notification/Notifications.tsx +++ b/src/notification/Notifications.tsx @@ -1,14 +1,15 @@ -import {Platform} from 'react-native'; +import { Platform } from 'react-native'; import messaging from '@react-native-firebase/messaging'; +import notifee, { AuthorizationStatus, AndroidImportance } from '@notifee/react-native'; import CentralServerProvider from '../provider/CentralServerProvider'; -import {Notification} from '../types/UserNotifications'; -import {getApplicationName, getBundleId, getVersion} from 'react-native-device-info'; +import { Notification } from '../types/UserNotifications'; +import { getApplicationName, getBundleId, getVersion } from 'react-native-device-info'; import Message from '../utils/Message'; import I18n from 'i18n-js'; import SecuredStorage from '../utils/SecuredStorage'; import ProviderFactory from '../provider/ProviderFactory'; -import {requestNotifications} from 'react-native-permissions'; +import { requestNotifications } from 'react-native-permissions'; export default class Notifications { private static centralServerProvider: CentralServerProvider; @@ -18,14 +19,29 @@ export default class Notifications { // Setup central provider this.centralServerProvider = await ProviderFactory.getProvider(); try { + if (Platform.OS === 'ios') { + // For iOS User + const settings = await notifee.requestPermission(); + if (settings.authorizationStatus >= AuthorizationStatus.AUTHORIZED) { + console.log('Permission settings:', settings); + } else { + console.log('User declined permissions'); + } + } const response = await requestNotifications(['alert', 'sound']); - if (response?.status === 'granted' ) { + if (response?.status === 'granted') { const fcmToken = await messaging().getToken(); if (fcmToken) { this.token = fcmToken; } } - } catch ( error ) { + await notifee.createChannel({ + id: 'fcm_notifications', + name: 'Push Notifications from FCM', + description: 'Push Notifications from FCM', + importance: AndroidImportance.HIGH + }); + } catch (error) { console.error(error); } } @@ -95,7 +111,7 @@ export default class Notifications { // Check tenant exist const tenant = await this.centralServerProvider.getTenant(tenantSubdomain); if (!tenant) { - Message.showError(I18n.t('general.tenantUnknown', {tenantSubdomain})); + Message.showError(I18n.t('general.tenantUnknown', { tenantSubdomain })); return false; } return true; diff --git a/src/provider/CentralServerProvider.tsx b/src/provider/CentralServerProvider.tsx index 9d567ec11..32c4e0286 100644 --- a/src/provider/CentralServerProvider.tsx +++ b/src/provider/CentralServerProvider.tsx @@ -1,41 +1,42 @@ -import {Buffer} from 'buffer'; +import { Buffer } from 'buffer'; /* eslint-disable @typescript-eslint/no-unsafe-return */ -import {NavigationContainerRef, StackActions} from '@react-navigation/native'; -import {AxiosInstance} from 'axios'; +import { NavigationContainerRef, StackActions } from '@react-navigation/native'; +import { AxiosInstance } from 'axios'; import I18n from 'i18n-js'; import jwtDecode from 'jwt-decode'; -import {Platform} from 'react-native'; +import { Platform } from 'react-native'; import ReactNativeBlobUtil from 'react-native-blob-util'; import Configuration from '../config/Configuration'; import I18nManager from '../I18n/I18nManager'; import Notifications from '../notification/Notifications'; -import {PLATFORM} from '../theme/variables/commonColor'; -import {ActionResponse, BillingOperationResult} from '../types/ActionResponse'; -import {BillingInvoice, BillingPaymentMethod} from '../types/Billing'; -import Car, {CarCatalog} from '../types/Car'; +import { PLATFORM } from '../theme/variables/commonColor'; +import { ActionResponse, BillingOperationResult } from '../types/ActionResponse'; +import { BillingInvoice, BillingPaymentMethod } from '../types/Billing'; +import Car, { CarCatalog } from '../types/Car'; import ChargingStation from '../types/ChargingStation'; -import {DataResult, TransactionDataResult} from '../types/DataResult'; -import Eula, {EulaAccepted} from '../types/Eula'; -import {KeyValue} from '../types/Global'; -import QueryParams, {PagingParams} from '../types/QueryParams'; -import {HttpChargingStationRequest} from '../types/requests/HTTPChargingStationRequests'; -import {RESTServerRoute, ServerAction} from '../types/Server'; -import {BillingSettings} from '../types/Setting'; +import { DataResult, TransactionDataResult } from '../types/DataResult'; +import Eula, { EulaAccepted } from '../types/Eula'; +import { KeyValue } from '../types/Global'; +import QueryParams, { PagingParams } from '../types/QueryParams'; +import { HttpChargingStationRequest } from '../types/requests/HTTPChargingStationRequests'; +import { RESTServerRoute, ServerAction } from '../types/Server'; +import { BillingSettings } from '../types/Setting'; import Site from '../types/Site'; import SiteArea from '../types/SiteArea'; import Tag from '../types/Tag'; -import {TenantConnection} from '../types/Tenant'; -import Transaction, {UserSessionContext} from '../types/Transaction'; -import User, {UserDefaultTagCar, UserMobileData} from '../types/User'; +import { TenantConnection } from '../types/Tenant'; +import Transaction, { UserSessionContext } from '../types/Transaction'; +import User, { UserDefaultTagCar, UserMobileData } from '../types/User'; import UserToken from '../types/UserToken'; import AxiosFactory from '../utils/AxiosFactory'; import Constants from '../utils/Constants'; import SecuredStorage from '../utils/SecuredStorage'; import Utils from '../utils/Utils'; import SecurityProvider from './SecurityProvider'; -import {getApplicationName, getBundleId, getVersion} from 'react-native-device-info'; +import { getApplicationName, getBundleId, getVersion } from 'react-native-device-info'; +import Reservation, { ReservationType } from '../types/Reservation'; export default class CentralServerProvider { private axiosInstance: AxiosInstance; @@ -52,6 +53,7 @@ export default class CentralServerProvider { private tenant: TenantConnection = null; private currency: string = null; private siteImagesCache: Map = new Map(); + private siteAreaImagesCache: Map = new Map(); private tenantLogosCache: Map = new Map(); private autoLoginDisabled = false; @@ -148,16 +150,13 @@ export default class CentralServerProvider { this.debugMethod('getTenantLogoBySubdomain'); let tenantLogo = null; // Call backend - const result = await this.axiosInstance.get( - this.buildUtilRestEndpointUrl(RESTServerRoute.REST_TENANT_LOGO, null, tenant), - { - headers: this.buildHeaders(), - responseType: 'arraybuffer', - params: { - Subdomain: tenant.subdomain - } + const result = await this.axiosInstance.get(this.buildUtilRestEndpointUrl(RESTServerRoute.REST_TENANT_LOGO, null, tenant), { + headers: this.buildHeaders(), + responseType: 'arraybuffer', + params: { + Subdomain: tenant.subdomain } - ); + }); if (result.data) { const base64Image = Buffer.from(result.data).toString('base64'); if (base64Image) { @@ -172,7 +171,7 @@ export default class CentralServerProvider { try { const currentTenantLogo = await this.getTenantLogoBySubdomain(this.tenant); return currentTenantLogo; - } catch ( error ) { + } catch (error) { return null; } } @@ -460,34 +459,32 @@ export default class CentralServerProvider { // Get the Tenant const tenant = await this.getTenant(tenantSubDomain); // Call - const result = await this.axiosInstance.get( - `${this.buildRestServerAuthURL(tenant)}/${RESTServerRoute.REST_MAIL_CHECK}`, - { - headers: this.buildHeaders(), - params: { - Tenant: tenantSubDomain, - Email: email, - VerificationToken: token - } + const result = await this.axiosInstance.get(`${this.buildRestServerAuthURL(tenant)}/${RESTServerRoute.REST_MAIL_CHECK}`, { + headers: this.buildHeaders(), + params: { + Tenant: tenantSubDomain, + Email: email, + VerificationToken: token } - ); + }); return result.data; } - public async getChargingStations(params = {}, paging: PagingParams = Constants.DEFAULT_PAGING, sorting: string[] = []): Promise> { + public async getChargingStations( + params = {}, + paging: PagingParams = Constants.DEFAULT_PAGING, + sorting: string[] = [] + ): Promise> { this.debugMethod('getChargingStations'); // Build Paging this.buildPaging(paging, params); // Build Sorting this.buildSorting(sorting, params); // Call - const result = await this.axiosInstance.get( - `${this.buildRestServerURL()}/${RESTServerRoute.REST_CHARGING_STATIONS}`, - { - headers: this.buildSecuredHeaders(), - params - } - ); + const result = await this.axiosInstance.get(`${this.buildRestServerURL()}/${RESTServerRoute.REST_CHARGING_STATIONS}`, { + headers: this.buildSecuredHeaders(), + params + }); return result?.data; } @@ -507,16 +504,13 @@ export default class CentralServerProvider { public async getChargingStation(id: string, extraParams: HttpChargingStationRequest = {}): Promise { this.debugMethod('getChargingStation'); // Call - const result = await this.axiosInstance.get( - this.buildRestEndpointUrl(RESTServerRoute.REST_CHARGING_STATION, { id }), - { - headers: this.buildSecuredHeaders(), - params: { - ID: id, - ...extraParams - } + const result = await this.axiosInstance.get(this.buildRestEndpointUrl(RESTServerRoute.REST_CHARGING_STATION, { id }), { + headers: this.buildSecuredHeaders(), + params: { + ID: id, + ...extraParams } - ); + }); return result.data; } @@ -539,13 +533,10 @@ export default class CentralServerProvider { // Build Sorting this.buildSorting(sorting, params); // Call - const result = await this.axiosInstance.get( - this.buildRestEndpointUrl(RESTServerRoute.REST_SITES), - { - headers: this.buildSecuredHeaders(), - params - } - ); + const result = await this.axiosInstance.get(this.buildRestEndpointUrl(RESTServerRoute.REST_SITES), { + headers: this.buildSecuredHeaders(), + params + }); return result.data; } @@ -560,18 +551,24 @@ export default class CentralServerProvider { // Build Sorting this.buildSorting(sorting, params); // Call - const result = await this.axiosInstance.get( - this.buildRestEndpointUrl(RESTServerRoute.REST_SITE_AREAS), - { - headers: this.buildSecuredHeaders(), - params - } - ); + const result = await this.axiosInstance.get(this.buildRestEndpointUrl(RESTServerRoute.REST_SITE_AREAS), { + headers: this.buildSecuredHeaders(), + params + }); return result.data; } // eslint-disable-next-line max-len - public async startTransaction(chargingStationID: string, connectorId: number, visualTagID: string, carID: string, userID: string, carStateOfCharge: number, targetStateOfCharge: number, departureTime: string): Promise { + public async startTransaction( + chargingStationID: string, + connectorId: number, + visualTagID: string, + carID: string, + userID: string, + carStateOfCharge: number, + targetStateOfCharge: number, + departureTime: string + ): Promise { this.debugMethod('startTransaction'); // Call const result = await this.axiosInstance.put( @@ -586,7 +583,7 @@ export default class CentralServerProvider { args: { visualTagID, connectorId - }, + } }, { headers: this.buildSecuredHeaders() @@ -600,7 +597,7 @@ export default class CentralServerProvider { // Call const result = await this.axiosInstance.put( this.buildRestEndpointUrl(RESTServerRoute.REST_TRANSACTION_STOP, { id: transactionId }), - { }, + {}, { headers: this.buildSecuredHeaders() } @@ -667,15 +664,12 @@ export default class CentralServerProvider { public async getTransaction(id: number): Promise { this.debugMethod('getTransaction'); // Call - const result = await this.axiosInstance.get( - this.buildRestEndpointUrl(RESTServerRoute.REST_TRANSACTION, { id }), - { - headers: this.buildSecuredHeaders(), - params: { - WithUser: true - } + const result = await this.axiosInstance.get(this.buildRestEndpointUrl(RESTServerRoute.REST_TRANSACTION, { id }), { + headers: this.buildSecuredHeaders(), + params: { + WithUser: true } - ); + }); return result.data; } @@ -712,13 +706,10 @@ export default class CentralServerProvider { // Build Sorting this.buildSorting(sorting, params); // Call - const result = await this.axiosInstance.get( - this.buildRestEndpointUrl(RESTServerRoute.REST_TRANSACTIONS_COMPLETED), - { - headers: this.buildSecuredHeaders(), - params - } - ); + const result = await this.axiosInstance.get(this.buildRestEndpointUrl(RESTServerRoute.REST_TRANSACTIONS_COMPLETED), { + headers: this.buildSecuredHeaders(), + params + }); return result.data; } @@ -742,30 +733,28 @@ export default class CentralServerProvider { // Build Sorting this.buildSorting(sorting, params); // Call - const result = await this.axiosInstance.get( - this.buildRestEndpointUrl(RESTServerRoute.REST_CARS), - { - headers: this.buildSecuredHeaders(), - params - } - ); + const result = await this.axiosInstance.get(this.buildRestEndpointUrl(RESTServerRoute.REST_CARS), { + headers: this.buildSecuredHeaders(), + params + }); return result.data; } - public async getCarCatalog(params = {}, paging: PagingParams = Constants.DEFAULT_PAGING, sorting: string[] = []): Promise> { + public async getCarCatalog( + params = {}, + paging: PagingParams = Constants.DEFAULT_PAGING, + sorting: string[] = [] + ): Promise> { this.debugMethod('getCarCatalog'); // Build Paging this.buildPaging(paging, params); // Build Sorting this.buildSorting(sorting, params); // Call - const result = await this.axiosInstance.get( - `${this.buildCentralRestServerServiceSecuredURL()}/${ServerAction.CAR_CATALOGS}`, - { - headers: this.buildSecuredHeaders(), - params - } - ); + const result = await this.axiosInstance.get(`${this.buildCentralRestServerServiceSecuredURL()}/${ServerAction.CAR_CATALOGS}`, { + headers: this.buildSecuredHeaders(), + params + }); return result.data; } @@ -776,13 +765,10 @@ export default class CentralServerProvider { // Build Sorting this.buildSorting(sorting, params); // Call - const result = await this.axiosInstance.get( - `${this.buildCentralRestServerServiceSecuredURL()}/${ServerAction.CAR}`, - { - headers: this.buildSecuredHeaders(), - params - } - ); + const result = await this.axiosInstance.get(`${this.buildCentralRestServerServiceSecuredURL()}/${ServerAction.CAR}`, { + headers: this.buildSecuredHeaders(), + params + }); return result.data; } @@ -793,32 +779,30 @@ export default class CentralServerProvider { // Build Sorting this.buildSorting(sorting, params); // Call - const result = await this.axiosInstance.get( - this.buildRestEndpointUrl(RESTServerRoute.REST_USERS), - { - headers: this.buildSecuredHeaders(), - params - } - ); + const result = await this.axiosInstance.get(this.buildRestEndpointUrl(RESTServerRoute.REST_USERS), { + headers: this.buildSecuredHeaders(), + params + }); return result.data; } public async getUserDefaultTagCar(userID: string, chargingStationID: string): Promise { this.debugMethod('getUserDefaultTagCar'); - const res = await this.axiosInstance.get( - this.buildRestEndpointUrl(RESTServerRoute.REST_USER_DEFAULT_TAG_CAR, { id: userID }), - { - headers: this.buildSecuredHeaders(), - params: { - UserID: userID, // Should be removed - already part of the URL path - ChargingStationID: chargingStationID // This information will soon be mandatory server-side - } + const res = await this.axiosInstance.get(this.buildRestEndpointUrl(RESTServerRoute.REST_USER_DEFAULT_TAG_CAR, { id: userID }), { + headers: this.buildSecuredHeaders(), + params: { + UserID: userID, // Should be removed - already part of the URL path + ChargingStationID: chargingStationID // This information will soon be mandatory server-side } - ); + }); return res?.data as UserDefaultTagCar; } - public async getTags(params: any = {}, paging: PagingParams = Constants.DEFAULT_PAGING, sorting: string[] = []): Promise> { + public async getTags( + params: any = {}, + paging: PagingParams = Constants.DEFAULT_PAGING, + sorting: string[] = [] + ): Promise> { this.debugMethod('getTags'); // Build Paging this.buildPaging(paging, params); @@ -827,13 +811,10 @@ export default class CentralServerProvider { // Force only local tags params.Issuer = true; // Call - const result = await this.axiosInstance.get( - this.buildRestEndpointUrl(RESTServerRoute.REST_TAGS), - { - headers: this.buildSecuredHeaders(), - params - } - ); + const result = await this.axiosInstance.get(this.buildRestEndpointUrl(RESTServerRoute.REST_TAGS), { + headers: this.buildSecuredHeaders(), + params + }); return result.data as DataResult; } @@ -848,13 +829,10 @@ export default class CentralServerProvider { // Build Sorting this.buildSorting(sorting, params); // Call - const result = await this.axiosInstance.get( - `${this.buildRestServerURL()}/${RESTServerRoute.REST_BILLING_INVOICES}`, - { - headers: this.buildSecuredHeaders(), - params - } - ); + const result = await this.axiosInstance.get(`${this.buildRestServerURL()}/${RESTServerRoute.REST_BILLING_INVOICES}`, { + headers: this.buildSecuredHeaders(), + params + }); return result.data as DataResult; } @@ -874,7 +852,11 @@ export default class CentralServerProvider { return result.data; } - public async getTransactionsActive(params: any = {}, paging: PagingParams = Constants.DEFAULT_PAGING, sorting: string[] = []): Promise> { + public async getTransactionsActive( + params: any = {}, + paging: PagingParams = Constants.DEFAULT_PAGING, + sorting: string[] = [] + ): Promise> { this.debugMethod('getTransactionsActive'); // Build Paging this.buildPaging(paging, params); @@ -882,38 +864,29 @@ export default class CentralServerProvider { this.buildSorting(sorting, params); params.WithUser = 'true'; // Call - const result = await this.axiosInstance.get( - this.buildRestEndpointUrl(RESTServerRoute.REST_TRANSACTIONS_ACTIVE), - { - headers: this.buildSecuredHeaders(), - params - } - ); + const result = await this.axiosInstance.get(this.buildRestEndpointUrl(RESTServerRoute.REST_TRANSACTIONS_ACTIVE), { + headers: this.buildSecuredHeaders(), + params + }); return result.data; } public async getUserImage(id: string): Promise { this.debugMethod('getUserImage'); // Call - const result = await this.axiosInstance.get( - this.buildRestEndpointUrl(RESTServerRoute.REST_USER_IMAGE, { id }), - { - headers: this.buildSecuredHeaders(), - params: { ID: id } - } - ); + const result = await this.axiosInstance.get(this.buildRestEndpointUrl(RESTServerRoute.REST_USER_IMAGE, { id }), { + headers: this.buildSecuredHeaders(), + params: { ID: id } + }); return result.data.image as string; } public async getUser(id: string): Promise { this.debugMethod('getUser'); // Call - const result = await this.axiosInstance.get( - this.buildRestEndpointUrl(RESTServerRoute.REST_USER, { id }), - { - headers: this.buildSecuredHeaders() - } - ); + const result = await this.axiosInstance.get(this.buildRestEndpointUrl(RESTServerRoute.REST_USER, { id }), { + headers: this.buildSecuredHeaders() + }); return result.data; } @@ -923,16 +896,13 @@ export default class CentralServerProvider { let foundSiteImage = this.siteImagesCache.get(id); if (!foundSiteImage) { // Call backend - const result = await this.axiosInstance.get( - this.buildUtilRestEndpointUrl(RESTServerRoute.REST_SITE_IMAGE, { id }), - { - headers: this.buildHeaders(), - responseType: 'arraybuffer', - params: { - TenantID: this.decodedToken?.tenantID - } + const result = await this.axiosInstance.get(this.buildUtilRestEndpointUrl(RESTServerRoute.REST_SITE_IMAGE, { id }), { + headers: this.buildHeaders(), + responseType: 'arraybuffer', + params: { + TenantID: this.decodedToken?.tenantID } - ); + }); if (result.data) { const base64Image = Buffer.from(result.data).toString('base64'); if (base64Image) { @@ -944,12 +914,35 @@ export default class CentralServerProvider { return foundSiteImage; } + public async getSiteAreaImage(id: string): Promise { + this.debugMethod('getSiteAreaImage'); + // Check cache + let foundSiteAreaImage = this.siteAreaImagesCache.get(id); + if (!foundSiteAreaImage) { + // Call backend + const result = await this.axiosInstance.get(this.buildUtilRestEndpointUrl(RESTServerRoute.REST_SITE_AREA_IMAGE, { id }), { + headers: this.buildHeaders(), + responseType: 'arraybuffer', + params: { + TenantID: this.decodedToken?.tenantID + } + }); + if (result.data) { + const base64Image = Buffer.from(result.data).toString('base64'); + if (base64Image) { + foundSiteAreaImage = 'data:' + result.headers['content-type'] + ';base64,' + base64Image; + this.siteAreaImagesCache.set(id, foundSiteAreaImage); + } + } + } + return foundSiteAreaImage; + } + public async getTransactionConsumption(transactionId: number): Promise { this.debugMethod('getTransactionConsumption'); // Call const result = await this.axiosInstance.get( - this.buildRestEndpointUrl(RESTServerRoute.REST_TRANSACTION_CONSUMPTIONS, - { id: transactionId }), + this.buildRestEndpointUrl(RESTServerRoute.REST_TRANSACTION_CONSUMPTIONS, { id: transactionId }), { headers: this.buildSecuredHeaders() } @@ -988,7 +981,10 @@ export default class CentralServerProvider { public async attachPaymentMethod(params: { userID: string; paymentMethodId: string }): Promise { this.debugMethod('attachPaymentMethod'); const result = await this.axiosInstance.post( - this.buildRestEndpointUrl(RESTServerRoute.REST_BILLING_PAYMENT_METHOD_ATTACH, { userID: params.userID, paymentMethodID: params.paymentMethodId }), + this.buildRestEndpointUrl(RESTServerRoute.REST_BILLING_PAYMENT_METHOD_ATTACH, { + userID: params.userID, + paymentMethodID: params.paymentMethodId + }), { params }, { headers: this.buildSecuredHeaders() @@ -1008,7 +1004,10 @@ export default class CentralServerProvider { return res?.data as BillingOperationResult; } - public async getPaymentMethods(params: { currentUserID: string }, paging: PagingParams = Constants.DEFAULT_PAGING): Promise> { + public async getPaymentMethods( + params: { currentUserID: string }, + paging: PagingParams = Constants.DEFAULT_PAGING + ): Promise> { this.debugMethod('getPaymentMethods'); // Build Paging this.buildPaging(paging, params); @@ -1031,12 +1030,9 @@ export default class CentralServerProvider { this.debugMethod('getBillingSettings'); // Execute the REST Service try { - const result = await this.axiosInstance.get( - `${this.buildRestServerURL()}/${RESTServerRoute.REST_BILLING_SETTING}`, - { - headers: this.buildSecuredHeaders() - } - ); + const result = await this.axiosInstance.get(`${this.buildRestServerURL()}/${RESTServerRoute.REST_BILLING_SETTING}`, { + headers: this.buildSecuredHeaders() + }); return result.data; } catch (error) { return null; @@ -1070,7 +1066,8 @@ export default class CentralServerProvider { 'GET', this.buildRestEndpointUrl(RESTServerRoute.REST_BILLING_DOWNLOAD_INVOICE, { invoiceID: invoice.id }), this.buildSecuredHeaders() - ).then(async (res) => { + ) + .then(async (res) => { // Open the downloaded invoice // On IOS, apps can only save files in their own internal filesystem // We need to open it to be able to save it to the phone custom directories @@ -1083,18 +1080,23 @@ export default class CentralServerProvider { } } - public async getUserSessionContext(userID: string, chargingStationID: string, connectorID: number, carID: string, tagID: string): Promise { + public async getUserSessionContext( + userID: string, + chargingStationID: string, + connectorID: number, + carID: string, + tagID: string + ): Promise { this.debugMethod('getUserSessionContext'); - const result = await this.axiosInstance.get(this.buildRestEndpointUrl(RESTServerRoute.REST_USER_SESSION_CONTEXT, {userID}), - { - headers: this.buildSecuredHeaders(), - params: { - ChargingStationID: chargingStationID, - ConnectorID: connectorID, - CarID: carID, - TagID: tagID - } - }); + const result = await this.axiosInstance.get(this.buildRestEndpointUrl(RESTServerRoute.REST_USER_SESSION_CONTEXT, { userID }), { + headers: this.buildSecuredHeaders(), + params: { + ChargingStationID: chargingStationID, + ConnectorID: connectorID, + CarID: carID, + TagID: tagID + } + }); return result.data; } @@ -1102,6 +1104,192 @@ export default class CentralServerProvider { return this.securityProvider; } + public async getReservations( + params = {}, + paging: PagingParams = Constants.DEFAULT_PAGING, + sorting: string[] = [] + ): Promise> { + this.debugMethod('getReservations'); + // Build Paging + this.buildPaging(paging, params); + // Build Sorting + this.buildSorting(sorting, params); + // Call + const result = await this.axiosInstance.get(this.buildRestEndpointUrl(RESTServerRoute.REST_RESERVATIONS), { + headers: this.buildSecuredHeaders(), + params + }); + return result.data; + } + + public async getReservation( + id: number, + params = {}, + paging: PagingParams = Constants.DEFAULT_PAGING, + sorting: string[] = [] + ): Promise { + this.debugMethod('getReservation'); + // Build Paging + this.buildPaging(paging, params); + // Build Sorting + this.buildSorting(sorting, params); + // Call + const result = await this.axiosInstance.get(this.buildRestEndpointUrl(RESTServerRoute.REST_RESERVATION, { id }), { + headers: this.buildSecuredHeaders(), + params + }); + return result.data; + } + + public async createReservation(reservation: Reservation): Promise { + this.debugMethod('createReservation'); + // Execute + const response = await this.axiosInstance.post(this.buildRestEndpointUrl(RESTServerRoute.REST_RESERVATIONS), reservation, { + headers: this.buildSecuredHeaders() + }); + return response?.data; + } + + public async updateReservation(reservation: Reservation): Promise { + this.debugMethod('updateReservation'); + // Execute + const response = await this.axiosInstance.put( + this.buildRestEndpointUrl(RESTServerRoute.REST_RESERVATION, { id: reservation.id }), + reservation, + { + headers: this.buildSecuredHeaders() + } + ); + return response?.data; + } + + public async deleteReservation(reservation: Reservation): Promise { + this.debugMethod('deleteReservation'); + // Execute + const response = await this.axiosInstance.delete( + this.buildRestEndpointUrl(RESTServerRoute.REST_RESERVATION, { id: reservation.id }), + { + headers: this.buildSecuredHeaders() + } + ); + return response?.data; + } + + public async cancelReservation(reservation: Reservation): Promise { + this.debugMethod('cancelReservation'); + const body = { + args: { + chargingStationID: reservation.chargingStationID, + connectorID: reservation.connectorID, + userID: reservation.userID, + } + }; + // Execute + const response = await this.axiosInstance.put( + this.buildRestEndpointUrl(RESTServerRoute.REST_RESERVATION_CANCEL, { id: reservation.id }), + body, + { + headers: this.buildSecuredHeaders() + } + ); + return response?.data; + } + + public async getReservableChargingStations( + params = {}, + paging: PagingParams = Constants.DEFAULT_PAGING, + sorting: string[] = [] + ): Promise> { + this.debugMethod('getReservableChargingStations'); + // Build Paging + this.buildPaging(paging, params); + // Build Sorting + this.buildSorting(sorting, params); + const response = await this.axiosInstance.get>( + this.buildRestEndpointUrl(RESTServerRoute.REST_CHARGING_STATIONS_RESERVATION_AVAILABILITY), + { + headers: this.buildSecuredHeaders(), + params + } + ); + return response?.data; + } + + public async reserveNow( + id: string, // ChargingStationID + connectorId: number, + expiryDate: Date, + visualTagID: string, + reservationId: number, + carID?: string, + parentIdTag?: string + ): Promise { + this.debugMethod('reserveNow'); + if (!id) { + return; + } + const body = { + args: { + reservationId: reservationId ?? null, + connectorId, + expiryDate, + visualTagID, + parentIdTag, + type: ReservationType.RESERVE_NOW, + carID + } + }; + // Execute + const response = await this.axiosInstance.put( + this.buildRestEndpointUrl(RESTServerRoute.REST_CHARGING_STATIONS_RESERVE_NOW, { id }), + body, + { + headers: this.buildSecuredHeaders() + } + ); + return response?.data; + } + + public async cancelReserveNowReservation(id: string, reservationId: number): Promise { + this.debugMethod('cancelReserveNowReservation'); + const body = { + args: { + reservationId + } + }; + // Execute + const response = await this.axiosInstance.put( + this.buildRestEndpointUrl(RESTServerRoute.REST_CHARGING_STATIONS_CANCEL_RESERVATION, { id }), + body, + { + headers: this.buildSecuredHeaders() + } + ); + return response?.data; + } + + public buildRestEndpointUrl( + urlPatternAsString: RESTServerRoute, + params: { [name: string]: string | number | null } = {}, + urlPrefix = this.buildRestServerURL() + ): string { + let resolvedUrlPattern = urlPatternAsString as string; + for (const key in params) { + if (Object.prototype.hasOwnProperty.call(params, key)) { + resolvedUrlPattern = resolvedUrlPattern.replace(`:${key}`, encodeURIComponent(params[key])); + } + } + return `${urlPrefix}/${resolvedUrlPattern}`; + } + + public buildUtilRestEndpointUrl( + urlPatternAsString: RESTServerRoute, + params: { [name: string]: string | number | null } = {}, + tenant?: TenantConnection + ): string { + return this.buildRestEndpointUrl(urlPatternAsString, params, this.buildUtilRestServerURL(tenant)); + } + private buildPaging(paging: PagingParams, queryParams: QueryParams): void { if (paging) { // Limit @@ -1146,7 +1334,7 @@ export default class CentralServerProvider { } private buildRestServerAuthURL(tenant: TenantConnection): string { - return (tenant?.endpoint?.endpoint) + '/v1/auth'; + return tenant?.endpoint?.endpoint + '/v1/auth'; } private buildRestServerURL(): string { @@ -1160,18 +1348,4 @@ export default class CentralServerProvider { private buildCentralRestServerServiceSecuredURL(): string { return this.tenant?.endpoint.endpoint + '/client/api'; } - - public buildRestEndpointUrl(urlPatternAsString: RESTServerRoute, params: { [name: string]: string | number | null } = {}, urlPrefix = this.buildRestServerURL()): string { - let resolvedUrlPattern = urlPatternAsString as string; - for (const key in params) { - if (Object.prototype.hasOwnProperty.call(params, key)) { - resolvedUrlPattern = resolvedUrlPattern.replace(`:${key}`, encodeURIComponent(params[key])); - } - } - return `${urlPrefix}/${resolvedUrlPattern}`; - } - - public buildUtilRestEndpointUrl(urlPatternAsString: RESTServerRoute, params: { [name: string]: string | number | null } = {}, tenant?: TenantConnection): string { - return this.buildRestEndpointUrl(urlPatternAsString, params, this.buildUtilRestServerURL(tenant)); - } } diff --git a/src/provider/SecurityProvider.tsx b/src/provider/SecurityProvider.tsx index d0730c7fb..d903f80cb 100644 --- a/src/provider/SecurityProvider.tsx +++ b/src/provider/SecurityProvider.tsx @@ -1,7 +1,9 @@ +import Reservation from '../types/Reservation'; import { Action, Entity, Role } from '../types/Authorization'; import SiteArea from '../types/SiteArea'; import { TenantComponents } from '../types/Tenant'; import UserToken from '../types/UserToken'; +import ChargingStation, { Connector } from 'types/ChargingStation'; export default class SecurityProvider { private loggedUser: UserToken; @@ -65,6 +67,14 @@ export default class SecurityProvider { return this.isComponentActive(TenantComponents.BILLING); } + public isComponentReservationActive(): boolean { + return this.isComponentActive(TenantComponents.RESERVATION); + } + + public isComponentSmartCharging(): boolean { + return this.isComponentActive(TenantComponents.SMART_CHARGING); + } + public isComponentActive(componentName: string): boolean { if (this.loggedUser && this.loggedUser.activeComponents) { return this.loggedUser.activeComponents.includes(componentName); @@ -108,9 +118,13 @@ export default class SecurityProvider { } public canAccess(resource: string, action: string): boolean { - return this.loggedUser && this.loggedUser.scopes && (this.loggedUser.scopes.includes(`${resource}:${action}`) - //TODO remove the plural (s) when backend new authorization deployed - || this.loggedUser.scopes.includes(`${resource}s:${action}`)); + return ( + this.loggedUser && + this.loggedUser.scopes && + (this.loggedUser.scopes.includes(`${resource}:${action}`) || + // TODO remove the plural (s) when backend new authorization deployed + this.loggedUser.scopes.includes(`${resource}s:${action}`)) + ); } public canListUsers(): boolean { @@ -132,4 +146,44 @@ export default class SecurityProvider { public canListPaymentMethods(): boolean { return this.canAccess(Entity.PAYMENT_METHOD, Action.LIST); } + + public canListReservations(): boolean { + return this.canAccess(Entity.RESERVATION, Action.LIST); + } + + public canReserveNow(connector: Connector, siteArea: SiteArea): boolean { + if (this.canAccess(Entity.CHARGING_STATION, Action.RESERVE_NOW)) { + if (this.isComponentActive(TenantComponents.ORGANIZATION)) { + if (!siteArea) { + return false; + } + return this.isSiteAdmin(siteArea.siteID) || this.loggedUser.sites.includes(siteArea.siteID) || connector.canReserveNow; + } + return this.isAdmin(); + } + return false; + } + + public canCancelReservation(connector: Connector, siteArea: SiteArea, reservation?: Reservation): boolean { + if ( + this.canAccess(Entity.CHARGING_STATION, Action.CANCEL_RESERVATION) || + this.canAccess(Entity.RESERVATION, Action.CANCEL_RESERVATION) + ) { + if (this.isComponentActive(TenantComponents.ORGANIZATION)) { + return this.isSiteAdmin(siteArea.siteID) || connector?.canCancelReservation || reservation?.canCancelReservation; + } + return this.isAdmin(); + } + return false; + } + + public canDeleteReservation(reservation: Reservation, siteArea: SiteArea): boolean { + if (this.canAccess(Entity.RESERVATION, Action.DELETE)) { + if (this.isComponentActive(TenantComponents.ORGANIZATION)) { + return this.isSiteAdmin(siteArea.siteID) || reservation.canDelete; + } + return this.isAdmin(); + } + return false; + } } diff --git a/src/screens/cars/AddCar.tsx b/src/screens/cars/AddCar.tsx index 965fec13b..72dbd9320 100644 --- a/src/screens/cars/AddCar.tsx +++ b/src/screens/cars/AddCar.tsx @@ -1,32 +1,32 @@ import React from 'react'; -import {Keyboard, Text, TextInput, View} from 'react-native'; +import { Keyboard, Text, TextInput, View } from 'react-native'; import HeaderComponent from '../../components/header/HeaderComponent'; import BaseProps from '../../types/BaseProps'; import BaseScreen from '../base-screen/BaseScreen'; import computeStyleSheet from './AddCarStyle'; import computeFormStyleSheet from '../../FormStyles'; import ModalSelect from '../../components/modal/ModalSelect'; -import Car, {CarCatalog, CarConverter, CarConverterType, CarType} from '../../types/Car'; -import {ItemSelectionMode} from '../../components/list/ItemsList'; -import {Icon} from 'native-base'; -import {Button, CheckBox, Input, Switch} from 'react-native-elements'; +import Car, { CarCatalog, CarConverter, CarConverterType, CarType } from '../../types/Car'; +import { ItemSelectionMode } from '../../components/list/ItemsList'; +import { Icon } from 'native-base'; +import { Button, CheckBox, Input, Switch } from 'react-native-elements'; import Utils from '../../utils/Utils'; import SelectDropdown from 'react-native-select-dropdown'; import CarCatalogComponent from '../../components/car/CarCatalogComponent'; import CarCatalogs from './CarCatalogs'; -import User, {UserStatus} from '../../types/User'; +import User, { UserStatus } from '../../types/User'; import UserComponent from '../../components/user/UserComponent'; import Users from '../users/list/Users'; import Orientation from 'react-native-orientation-locker'; import Message from '../../utils/Message'; -import {HTTPError} from '../../types/HTTPError'; +import { HTTPError } from '../../types/HTTPError'; import I18n from 'i18n-js'; import Constants from '../../utils/Constants'; -import {RestResponse} from '../../types/ActionResponse'; +import { RestResponse } from '../../types/ActionResponse'; import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; -import {scale} from 'react-native-size-matters'; -import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view'; -import {SafeAreaView} from 'react-native-safe-area-context'; +import { scale } from 'react-native-size-matters'; +import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; +import { SafeAreaView } from 'react-native-safe-area-context'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; interface State { @@ -111,11 +111,16 @@ export default class AddCar extends BaseScreen { backArrow={true} containerStyle={style.headerContainer} /> - + ( @@ -146,8 +151,8 @@ export default class AddCar extends BaseScreen { data={selectedCarCatalogConverters} buttonTextAfterSelection={(carConverter: CarConverter) => Utils.buildCarCatalogConverterName(carConverter)} rowTextForSelection={(carConverter: CarConverter) => Utils.buildCarCatalogConverterName(carConverter)} - buttonStyle={{...style.selectField, ...(!selectedCarCatalog ? style.selectFieldDisabled : {})}} - buttonTextStyle={{...style.selectFieldText, ...(!selectedConverter ? style.selectFieldTextPlaceholder : {})}} + buttonStyle={{ ...style.selectField, ...(!selectedCarCatalog ? style.selectFieldDisabled : {}) }} + buttonTextStyle={{ ...style.selectFieldText, ...(!selectedConverter ? style.selectFieldTextPlaceholder : {}) }} dropdownStyle={style.selectDropdown} rowStyle={style.selectDropdownRow} rowTextStyle={style.selectDropdownRowText} @@ -166,14 +171,14 @@ export default class AddCar extends BaseScreen { autoCapitalize={'characters'} autoCorrect={false} renderErrorMessage={!this.checkVIN()} - errorMessage={!this.checkVIN() ? vin && I18n.t('cars.invalidVIN'): null} + errorMessage={!this.checkVIN() ? vin && I18n.t('cars.invalidVIN') : null} errorStyle={formStyle.inputError} returnKeyType={'next'} onSubmitEditing={() => this.licensePlateInput?.focus()} onChangeText={(newVin: string) => this.setState({ vin: newVin })} /> this.licensePlateInput = ref} + ref={(ref) => (this.licensePlateInput = ref)} containerStyle={formStyle.inputContainer} inputStyle={formStyle.inputText} inputContainerStyle={[formStyle.inputTextContainer, !this.checkLicensePlate() && formStyle.inputTextContainerError]} @@ -193,7 +198,7 @@ export default class AddCar extends BaseScreen { ( @@ -225,7 +230,7 @@ export default class AddCar extends BaseScreen { containerStyle={formStyle.checkboxContainer} textStyle={formStyle.checkboxText} checked={type === CarType.COMPANY} - checkedIcon={} + checkedIcon={} uncheckedIcon={} onPress={() => this.setState({ type: CarType.COMPANY })} title={I18n.t('carTypes.companyCar')} @@ -234,7 +239,7 @@ export default class AddCar extends BaseScreen { containerStyle={formStyle.checkboxContainer} textStyle={formStyle.checkboxText} checked={type === CarType.POOL_CAR} - checkedIcon={} + checkedIcon={} uncheckedIcon={} onPress={() => this.setState({ type: CarType.POOL_CAR })} title={I18n.t('carTypes.poolCar')} @@ -243,7 +248,7 @@ export default class AddCar extends BaseScreen { containerStyle={formStyle.checkboxContainer} textStyle={formStyle.checkboxText} checked={type === CarType.PRIVATE} - checkedIcon={} + checkedIcon={} uncheckedIcon={} onPress={() => this.setState({ type: CarType.PRIVATE })} title={I18n.t('carTypes.privateCar')} @@ -258,7 +263,7 @@ export default class AddCar extends BaseScreen { containerStyle={formStyle.buttonContainer} buttonStyle={formStyle.button} loading={addCarPending} - loadingProps={{color: commonColors.light}} + loadingProps={{ color: commonColors.light }} onPress={() => void this.addCar()} /> @@ -280,7 +285,16 @@ export default class AddCar extends BaseScreen { private checkForm(): boolean { const { selectedCarCatalog, selectedConverter, selectedUser, type, vin, licensePlate } = this.state; - return this.checkVIN() && this.checkLicensePlate() && !!selectedCarCatalog && !!selectedConverter && !!selectedUser && !!type && !!vin && !!licensePlate; + return ( + this.checkVIN() && + this.checkLicensePlate() && + !!selectedCarCatalog && + !!selectedConverter && + !!selectedUser && + !!type && + !!vin && + !!licensePlate + ); } private onCarCatalogSelected(selectedCarCatalog: CarCatalog) { @@ -328,7 +342,7 @@ export default class AddCar extends BaseScreen { return selectedCarCatalogConverters; } - private renderCarCatalogPlaceholder(style: any) { + private renderCarCatalogPlaceholder(style: any) { return ( { defaultButtonText={I18n.t('cars.model')} defaultValue={null} buttonStyle={style.selectField} - buttonTextStyle={{...style.selectFieldText, ...(!this.state.selectedCarCatalog ? style.selectFieldTextPlaceholder : {})}} + buttonTextStyle={{ ...style.selectFieldText, ...(!this.state.selectedCarCatalog ? style.selectFieldTextPlaceholder : {}) }} renderDropdownIcon={() => } /> ); @@ -350,7 +364,7 @@ export default class AddCar extends BaseScreen { data={[]} statusBarTranslucent={true} defaultValue={null} - renderCustomizedButtonChild={() => } + renderCustomizedButtonChild={() => } buttonStyle={style.selectField} buttonTextStyle={style.selectFieldText} renderDropdownIcon={() => } @@ -391,7 +405,7 @@ export default class AddCar extends BaseScreen { if (response?.status === RestResponse.SUCCESS) { Message.showSuccess(I18n.t('cars.addCarSuccessfully')); const routes = this.props.navigation.getState().routes; - this.props.navigation.navigate(routes[Math.max(0, routes.length-2)].name, {refresh: true}); + this.props.navigation.navigate(routes[Math.max(0, routes.length - 2)].name, { refresh: true }); return; } else { Message.showError(I18n.t('cars.addError')); diff --git a/src/screens/charging-stations/connector-details/ChargingStationConnectorDetails.tsx b/src/screens/charging-stations/connector-details/ChargingStationConnectorDetails.tsx index 4d9d958cf..09a7cc603 100644 --- a/src/screens/charging-stations/connector-details/ChargingStationConnectorDetails.tsx +++ b/src/screens/charging-stations/connector-details/ChargingStationConnectorDetails.tsx @@ -1,7 +1,7 @@ -import {StatusCodes} from 'http-status-codes'; -import I18n from 'i18n-js'; -import {HStack, Icon, Spinner} from 'native-base'; import MultiSlider from '@ptomasroos/react-native-multi-slider'; +import { StatusCodes } from 'http-status-codes'; +import I18n from 'i18n-js'; +import { HStack, Icon, Spinner } from 'native-base'; import React from 'react'; import { ActivityIndicator, @@ -10,6 +10,7 @@ import { ImageBackground, ImageStyle, RefreshControl, + SafeAreaView, ScrollView, Text, TextInput, @@ -20,28 +21,37 @@ import { } from 'react-native'; import Orientation from 'react-native-orientation-locker'; +import DurationUnitFormat from 'intl-unofficial-duration-unit-format'; +import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; +import DateTimePicker from 'react-native-modal-datetime-picker'; +import { scale } from 'react-native-size-matters'; +import FontAwesome from 'react-native-vector-icons/FontAwesome'; +import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; +import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; import noSite from '../../../../assets/no-site.png'; +import I18nManager from '../../../I18n/I18nManager'; +import computeActivityIndicatorCommonStyles from '../../../components/activity-indicator/ActivityIndicatorCommonStyle'; import CarComponent from '../../../components/car/CarComponent'; -import ChargingStationConnectorComponent - from '../../../components/charging-station/connector/ChargingStationConnectorComponent'; +import ChargingStationConnectorComponent from '../../../components/charging-station/connector/ChargingStationConnectorComponent'; import ConnectorStatusComponent from '../../../components/connector-status/ConnectorStatusComponent'; +import computeFabStyles from '../../../components/fab/FabComponentStyles'; import HeaderComponent from '../../../components/header/HeaderComponent'; -import {ItemSelectionMode} from '../../../components/list/ItemsList'; +import { ItemSelectionMode } from '../../../components/list/ItemsList'; import computeListItemCommonStyle from '../../../components/list/ListItemCommonStyle'; import DialogModal from '../../../components/modal/DialogModal'; import computeModalCommonStyle from '../../../components/modal/ModalCommonStyle'; import ModalSelect from '../../../components/modal/ModalSelect'; import TagComponent from '../../../components/tag/TagComponent'; -import UserAvatar from '../../../components/user/avatar/UserAvatar'; import UserComponent from '../../../components/user/UserComponent'; -import I18nManager from '../../../I18n/I18nManager'; +import UserAvatar from '../../../components/user/avatar/UserAvatar'; +import { RestResponse } from '../../../types/ActionResponse'; import BaseProps from '../../../types/BaseProps'; import Car from '../../../types/Car'; -import ChargingStation, {ChargePointStatus, Connector, OCPPGeneralResponse} from '../../../types/ChargingStation'; -import {HTTPAuthError} from '../../../types/HTTPError'; +import ChargingStation, { ChargePointStatus, Connector, OCPPGeneralResponse } from '../../../types/ChargingStation'; +import { HTTPAuthError } from '../../../types/HTTPError'; import Tag from '../../../types/Tag'; -import Transaction, {StartTransactionErrorCode, UserSessionContext} from '../../../types/Transaction'; -import User, {UserStatus} from '../../../types/User'; +import Transaction, { StartTransactionErrorCode, UserSessionContext } from '../../../types/Transaction'; +import User, { UserStatus } from '../../../types/User'; import UserToken from '../../../types/UserToken'; import Message from '../../../utils/Message'; import Utils from '../../../utils/Utils'; @@ -50,20 +60,14 @@ import Cars from '../../cars/Cars'; import Tags from '../../tags/Tags'; import Users from '../../users/list/Users'; import computeStyleSheet from './ChargingStationConnectorDetailsStyles'; -import {scale} from 'react-native-size-matters'; -import computeActivityIndicatorCommonStyles from '../../../components/activity-indicator/ActivityIndicatorCommonStyle'; -import DurationUnitFormat from 'intl-unofficial-duration-unit-format'; -import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; -import FontAwesome from 'react-native-vector-icons/FontAwesome'; -import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; -import DateTimePicker from 'react-native-modal-datetime-picker'; -import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view'; function SocInput(props: { inputProps: TextInputProps; leftText: string; containerStyle?: ViewStyle }) { const style = computeStyleSheet(); return ( - {props?.leftText} + + {props?.leftText} + { @@ -172,7 +179,10 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre departureTime: null, departureSoC: null, currentSoC: null, - sessionContext: null + sessionContext: null, + canReserveNow: false, + canCancelReservation: false, + showCancelReservationDialog: false }; } @@ -196,10 +206,10 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre role: this.currentUser.role, email: this.currentUser.email } as User; - // await this.loadSelectedUserDefaultTagAndCar(selectedUser); + // await this.loadSelectedUserDefaultTagAndCar(selectedUser); const selectedUser = userFromNavigation ?? currentUser; const selectedTag = tagFromNavigation ?? this.state.selectedTag; - this.setState({ selectedUser, selectedTag },async () => this.refresh(false, () => void this.loadUserSessionContext())); + this.setState({ selectedUser, selectedTag }, async () => this.refresh(false, () => void this.loadUserSessionContext())); } public componentDidFocus(): void { @@ -240,7 +250,7 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre public getChargingStation = async (chargingStationID: string): Promise => { try { // Get Charger - const chargingStation = await this.centralServerProvider.getChargingStation(chargingStationID); + const chargingStation = await this.centralServerProvider.getChargingStation(chargingStationID, { WithReservation: true }); return chargingStation; } catch (error) { // Other common Error @@ -341,7 +351,7 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre // eslint-disable-next-line complexity public async refresh(showSpinner = false, callback: () => void = () => {}): Promise { - const newState = showSpinner ? {refreshing: true} : this.state; + const newState = showSpinner ? { refreshing: true } : this.state; this.setState(newState, async () => { let siteImage = this.state.siteImage; let transaction = null; @@ -379,25 +389,32 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre ) { showStartTransactionDialog = true; } - this.setState({ - showStartTransactionDialog: this.state.showStartTransactionDialog ?? showStartTransactionDialog, - chargingStation, - connector, - transaction, - siteImage, - ...(!transactionStillPending && {transactionPending: false, didPreparing: false}), - ...(!!this.state.departureTime && {departureTime: new Date(Math.max(this.getMinimumDateMillisecs(), this.state.departureTime?.getTime()))}), - refreshing: false, - isAdmin: this.securityProvider ? this.securityProvider.isAdmin() : false, - isSiteAdmin: this.securityProvider?.isSiteAdmin(chargingStation?.siteArea?.siteID) ?? false, - canStartTransaction: chargingStation ? this.canStartTransaction(chargingStation, connector) : false, - canStopTransaction: chargingStation ? this.canStopTransaction(chargingStation, connector) : false, - ...durationInfos, - isPricingActive: this.securityProvider?.isComponentPricingActive(), - loading: false - },() => callback?.()) ; + this.setState( + { + showStartTransactionDialog: this.state.showStartTransactionDialog ?? showStartTransactionDialog, + chargingStation, + connector, + transaction, + siteImage, + ...(!transactionStillPending && { transactionPending: false, didPreparing: false }), + ...(!!this.state.departureTime && { + departureTime: new Date(Math.max(this.getMinimumDateMillisecs(), this.state.departureTime?.getTime())) + }), + refreshing: false, + isAdmin: this.securityProvider ? this.securityProvider.isAdmin() : false, + isSiteAdmin: this.securityProvider?.isSiteAdmin(chargingStation?.siteArea?.siteID) ?? false, + canStartTransaction: chargingStation ? this.canStartTransaction(chargingStation, connector) : false, + canStopTransaction: chargingStation ? this.canStopTransaction(chargingStation, connector) : false, + ...durationInfos, + isPricingActive: this.securityProvider?.isComponentPricingActive(), + loading: false, + canReserveNow: chargingStation ? this.canReserveNow(chargingStation, connector) : false, + canCancelReservation: chargingStation ? this.canCancelReservation(chargingStation, connector) : false + }, + () => callback?.() + ); }); - }; + } public canStopTransaction = (chargingStation: ChargingStation, connector: Connector): boolean => { // Transaction? @@ -417,6 +434,20 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre return false; } + public canReserveNow = (chargingStation: ChargingStation, connector: Connector): boolean => { + if (connector && connector.status === ChargePointStatus.AVAILABLE) { + return this.securityProvider?.canReserveNow(connector, chargingStation?.siteArea); + } + return false; + }; + + public canCancelReservation = (chargingStation: ChargingStation, connector: Connector): boolean => { + if (connector && connector.status === ChargePointStatus.RESERVED) { + return this.securityProvider?.canCancelReservation(connector, chargingStation?.siteArea); + } + return false; + }; + public manualRefresh = async () => { // Display spinner this.setState({ refreshing: true }); @@ -432,14 +463,26 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre public async startTransaction(): Promise { await this.refresh(); - const { chargingStation, connector, selectedTag, selectedCar, selectedUser, canStartTransaction, currentSoC, departureSoC, departureTime } = this.state; + const { + chargingStation, + connector, + selectedTag, + selectedCar, + selectedUser, + canStartTransaction, + currentSoC, + departureSoC, + departureTime + } = this.state; try { if (this.isButtonDisabled() || !canStartTransaction) { Message.showError(I18n.t('general.notAuthorized')); return; } this.setState({ buttonDisabled: true }); - const departureTimeAsString = !departureTime ? departureTime : new Date(Math.max(this.getMinimumDateMillisecs(), departureTime.getTime())).toISOString(); + const departureTimeAsString = !departureTime + ? departureTime + : new Date(Math.max(this.getMinimumDateMillisecs(), departureTime.getTime())).toISOString(); // Start the Transaction const response = await this.centralServerProvider.startTransaction( chargingStation.id, @@ -542,7 +585,7 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre connector: Connector ): { totalInactivitySecs?: number; elapsedTimeFormatted?: string; inactivityFormatted?: string } => { // Transaction loaded? - const durationFormatOptions = {style: DurationUnitFormat.styles.NARROW, format: '{hour} {minutes}'}; + const durationFormatOptions = { style: DurationUnitFormat.styles.NARROW, format: '{hour} {minutes}' }; const defaultDuration = I18nManager.formatDuration(0, durationFormatOptions); if (transaction) { let elapsedTimeFormatted = defaultDuration; @@ -628,7 +671,9 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre return connector && connector.currentTransactionID && transaction ? ( - {price} + + {price} + ) : ( @@ -644,7 +689,10 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre - {connector.currentInstantWatts > 0 ? I18nManager.formatNumber(connector.currentInstantWatts / 1000, {maximumFractionDigits: 2} ) : 0} kW + {connector.currentInstantWatts > 0 + ? I18nManager.formatNumber(connector.currentInstantWatts / 1000, { maximumFractionDigits: 2 }) + : 0}{' '} + kW ) : ( @@ -660,7 +708,9 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre return connector && connector.currentTransactionID ? ( - {elapsedTimeFormatted} + + {elapsedTimeFormatted} + ) : ( @@ -676,7 +726,9 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre return connector && connector.currentTransactionID ? ( - {inactivityFormatted} + + {inactivityFormatted} + ) : ( @@ -692,7 +744,7 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre - {connector ? I18nManager.formatNumber(connector.currentTotalConsumptionWh/ 1000, {maximumFractionDigits: 2}) : ''} kW.h + {connector ? I18nManager.formatNumber(connector.currentTotalConsumptionWh / 1000, { maximumFractionDigits: 2 }) : ''} kW.h ) : ( @@ -708,14 +760,15 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre return connector && connector.currentStateOfCharge && !isNaN(connector.currentStateOfCharge) ? ( - {transaction ? - ( - {`${transaction.stateOfCharge}%`} - {'>'} {`${transaction.currentStateOfCharge}%`} - ) : - {connector.currentStateOfCharge} - } + {transaction ? ( + + {`${transaction.stateOfCharge}%`} + {'>'} + {`${transaction.currentStateOfCharge}%`}{' '} + + ) : ( + {connector.currentStateOfCharge} + )} ) : ( @@ -803,6 +856,38 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre ); }; + public cancelReservationConfirm = () => { + this.setState({ showCancelReservationDialog: true }); + }; + + public cancelReservation = async () => { + const { chargingStation, connector, canCancelReservation } = this.state; + if (!canCancelReservation) { + Message.showError(I18n.t('general.notAuthorized')); + } + try { + // Disable button + this.setState({ buttonDisabled: true }); + // Cancel Reservation + const response = await this.centralServerProvider.cancelReserveNowReservation(chargingStation.id, connector.reservationID); + if (response?.status === RestResponse.SUCCESS) { + Message.showSuccess(I18n.t('details.accepted')); + await this.refresh(); + } else { + Message.showError(I18n.t('details.denied')); + } + } catch (error) { + // Other common Error + await Utils.handleHttpUnexpectedError( + this.centralServerProvider, + error, + 'reservations.cancel_reservation.error', + this.props.navigation, + this.refresh.bind(this) + ); + } + }; + public render() { const style = computeStyleSheet(); const { @@ -811,7 +896,8 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre loading, isPricingActive, showStartTransactionDialog, - showStopTransactionDialog + showStopTransactionDialog, + showCancelReservationDialog } = this.state; const commonColors = Utils.getCurrentCommonColor(); const connectorLetter = Utils.getConnectorLetterFromConnectorID(connector?.connectorId); @@ -819,6 +905,7 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre {showStartTransactionDialog && this.renderStartTransactionDialog()} {showStopTransactionDialog && this.renderStopTransactionDialog()} + {showCancelReservationDialog && this.renderCancelReservationDialog()} {loading ? ( - ) : + ) : ( + {/* Reserve Now Button */} + {this.renderReservationButton()} {/* Site Image */} {this.renderBackGroundImage(style)} {/* Details */} - {connector?.status === ChargePointStatus.AVAILABLE || connector?.status === ChargePointStatus.PREPARING || connector?.status === ChargePointStatus.FINISHING ? ( + {connector?.status === ChargePointStatus.AVAILABLE || + connector?.status === ChargePointStatus.PREPARING || + connector?.status === ChargePointStatus.FINISHING ? ( this.renderChargingStationParameters(style) ) : ( } - > + contentContainerStyle={{ flexDirection: 'row', flexWrap: 'wrap' }} + refreshControl={ + + }> {this.renderConnectorStatus(style)} {this.renderUserInfo(style)} {this.renderInstantPower(style)} @@ -851,15 +948,18 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre )} - } + )} ); } private renderBackGroundImage(style: any) { - const { canStopTransaction, canStartTransaction, siteImage, connector} = this.state; + const { canStopTransaction, canStartTransaction, siteImage, connector } = this.state; return ( - + {/* Show Last Transaction */} {this.renderShowLastTransactionButton(style)} @@ -881,21 +981,22 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre private renderChargingStationParameters(style: any) { const activityIndicatorCommonStyles = computeActivityIndicatorCommonStyles(); const commonColors = Utils.getCurrentCommonColor(); - const {refreshing, sessionContext, sessionContextLoading} = this.state; + const { refreshing, sessionContext, sessionContextLoading } = this.state; return ( - - {refreshing && } + + {refreshing && ( + + )} + keyboardShouldPersistTaps={'always'}> {this.renderConnectorInfo(style)} {this.renderUserSelection(style)} {this.renderTagSelection(style)} @@ -906,7 +1007,6 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre {!sessionContextLoading && sessionContext?.smartChargingSessionParameters?.targetStateOfCharge && this.renderSoCInputs()} )} - ); @@ -914,28 +1014,40 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre private renderSmartChargingParameters(style: any) { const { transaction } = this.state; - const departureTimeFormatted = I18nManager.formatDateTime(transaction?.departureTime, {dateStyle: 'short', timeStyle: 'short'}); + const departureTimeFormatted = I18nManager.formatDateTime(transaction?.departureTime, { dateStyle: 'short', timeStyle: 'short' }); return ( <> {!Utils.isNullOrUndefined(transaction?.carStateOfCharge) && ( - {I18n.t('transactions.initialStateOfCharge')} - {transaction.carStateOfCharge}% + + {I18n.t('transactions.initialStateOfCharge')} + + + {transaction.carStateOfCharge}% + )} {!!transaction?.targetStateOfCharge && ( - {I18n.t('transactions.targetStateOfCharge')} - {transaction.targetStateOfCharge}% + + {I18n.t('transactions.targetStateOfCharge')} + + + {transaction.targetStateOfCharge}% + )} {!!transaction?.departureTime && ( - {I18n.t('transactions.departureTime')} - {departureTimeFormatted} + + {I18n.t('transactions.departureTime')} + + + {departureTimeFormatted} + )} @@ -948,17 +1060,24 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre const { departureTime, showTimePicker } = this.state; const minimumDate = new Date(); const maximumDate = new Date(new Date().getTime() + MAX_SESSION_DURATION_MILLISECS); - const departureTimeFormatted = I18nManager.formatDateTime(departureTime, {dateStyle: 'short', timeStyle: 'short'}); - const durationSeconds = Math.abs((departureTime.getTime() - new Date().getTime())/1000); - const durationFormatOptions = {style: DurationUnitFormat.styles.NARROW, format: `{hour} {minutes} ${durationSeconds < 60 ? '{seconds}': ''}`}; + const departureTimeFormatted = I18nManager.formatDateTime(departureTime, { dateStyle: 'short', timeStyle: 'short' }); + const durationSeconds = Math.abs((departureTime.getTime() - new Date().getTime()) / 1000); + const durationFormatOptions = { + style: DurationUnitFormat.styles.NARROW, + format: `{hour} {minutes} ${durationSeconds < 60 ? '{seconds}' : ''}` + }; const durationFormatted = I18nManager.formatDuration(durationSeconds, durationFormatOptions); const locale = this.centralServerProvider.getUserInfo()?.locale; const is24Hour = I18nManager?.isLocale24Hour(locale); return ( - {I18n.t('transactions.departureTime')} - this.setState({showTimePicker: true})}> - {departureTimeFormatted} ({durationFormatted}) + + {I18n.t('transactions.departureTime')} + + this.setState({ showTimePicker: true })}> + + {departureTimeFormatted} ({durationFormatted}) + this.setState({showTimePicker: false, departureTime: newDepartureTime})} - onCancel={() => this.setState({showTimePicker: false})} + onConfirm={(newDepartureTime) => this.setState({ showTimePicker: false, departureTime: newDepartureTime })} + onCancel={() => this.setState({ showTimePicker: false })} /> ); @@ -989,7 +1108,9 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre return ( - {I18n.t('transactions.stateOfCharge')} + + {I18n.t('transactions.stateOfCharge')} + {showCurrentSoCInput ? ( ) : ( - + )} this.onSoCChanged(this.state.currentSoC, this.computeNumericSoC(newDepartureSoC)), + onChangeText: (newDepartureSoC: string) => + this.onSoCChanged(this.state.currentSoC, this.computeNumericSoC(newDepartureSoC)), value: departureSoCText, editable: !tagCarLoading }} leftText={I18n.t('general.to')} - containerStyle={{...(settingsErrors?.departureSoCError && style.socInputContainerError)}} + containerStyle={{ ...(settingsErrors?.departureSoCError && style.socInputContainerError) }} /> } + customMarker={() => } minMarkerOverlapStepDistance={1} isMarkersSeparated={!showCurrentSoCInput} customMarkerLeft={() => <>} - customMarkerRight={() => } + customMarkerRight={() => } enabledOne={showCurrentSoCInput} enabledTwo={true} onValuesChange={([newCurrentSoC, newDepartureSoC]) => this.onSoCChanged(newCurrentSoC, newDepartureSoC)} @@ -1046,7 +1168,7 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre } this.setState({ departureSoC: newDepartureSoC, - currentSoC: newCurrentSoC, + currentSoC: newCurrentSoC }); } @@ -1065,9 +1187,7 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre this.setState({ showChargingSettings: !showChargingSettings })} style={style.accordion}> {I18n.t('transactions.chargingSettings')} - {Object.values(settingsErrors ?? {}).some(error => error) && ( - * - )} + {Object.values(settingsErrors ?? {}).some((error) => error) && *} {showChargingSettings ? ( @@ -1136,13 +1256,18 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre disabled={disabled} openable={isAdmin} - renderItem={(user) => - } + renderItem={(user) => ( + + )} defaultItems={[selectedUser]} onItemsSelected={this.onUserSelected.bind(this)} navigation={navigation} selectionMode={ItemSelectionMode.SINGLE}> - + )} {settingsErrors.billingError && this.renderBillingErrorMessages(style)} @@ -1165,7 +1290,9 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre defaultItems={[selectedCar]} renderItemPlaceholder={this.renderCarPlaceholder.bind(this)} defaultItemLoading={sessionContextLoading} - onItemsSelected={(selectedCars: Car[]) => this.setState({selectedCar: selectedCars?.[0]}, () => void this.loadUserSessionContext())} + onItemsSelected={(selectedCars: Car[]) => + this.setState({ selectedCar: selectedCars?.[0] }, () => void this.loadUserSessionContext()) + } navigation={navigation} selectionMode={ItemSelectionMode.SINGLE}> @@ -1218,7 +1345,13 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre return ( - renderItem={(tag) => } + renderItem={(tag) => ( + + )} disabled={disabled} openable={true} renderNoItem={this.renderNoTag.bind(this)} @@ -1226,7 +1359,9 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre ref={this.tagModalRef} defaultItems={[selectedTag]} defaultItemLoading={sessionContextLoading} - onItemsSelected={(selectedTags: Tag[]) => this.setState({ selectedTag: selectedTags?.[0] }, () => void this.loadUserSessionContext())} + onItemsSelected={(selectedTags: Tag[]) => + this.setState({ selectedTag: selectedTags?.[0] }, () => void this.loadUserSessionContext()) + } navigation={navigation} selectionMode={ItemSelectionMode.SINGLE}> @@ -1269,8 +1404,8 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre private async loadUserSessionContext(): Promise { const { selectedUser, chargingStation, connector } = this.state; - let { selectedCar, selectedTag } = this.state; - this.setState({sessionContextLoading: true}, async () => { + let { selectedCar, selectedTag } = this.state; + this.setState({ sessionContextLoading: true }, async () => { const userSessionContext = await this.getUserSessionContext( selectedUser?.id as string, chargingStation?.id, @@ -1278,7 +1413,7 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre selectedCar?.id, selectedTag?.id ); - selectedCar = userSessionContext?.car ? {...userSessionContext.car, user: selectedUser} : null; + selectedCar = userSessionContext?.car ? { ...userSessionContext.car, user: selectedUser } : null; selectedTag = userSessionContext?.tag; const defaultDepartureTime = userSessionContext?.smartChargingSessionParameters?.departureTime; this.setState({ @@ -1339,35 +1474,109 @@ export default class ChargingStationConnectorDetails extends BaseAutoRefreshScre private isButtonDisabled(): boolean { const { sessionContextLoading, transactionPending, connector } = this.state; - return sessionContextLoading - || Object.values(this.computeSettingsErrors()).some(error => error) - || transactionPending - || connector?.status === ChargePointStatus.FAULTED - || connector?.status === ChargePointStatus.UNAVAILABLE; + return ( + sessionContextLoading || + Object.values(this.computeSettingsErrors()).some((error) => error) || + transactionPending || + connector?.status === ChargePointStatus.FAULTED || + connector?.status === ChargePointStatus.UNAVAILABLE + ); } private getMinimumDateMillisecs(): number { return new Date().getTime() + MIN_SESSION_DURATION_MILLISECS; } - private async getUserSessionContext(userID: string, chargingStationID: string, connectorID: number, carID: string, tagID: string): Promise { + private async getUserSessionContext( + userID: string, + chargingStationID: string, + connectorID: number, + carID: string, + tagID: string + ): Promise { try { - return this.centralServerProvider.getUserSessionContext( - userID, - chargingStationID, - connectorID, - carID, - tagID - ); + return this.centralServerProvider.getUserSessionContext(userID, chargingStationID, connectorID, carID, tagID); } catch (error) { - await Utils.handleHttpUnexpectedError( - this.centralServerProvider, - error, - null, - this.props.navigation, - this.refresh.bind(this) - ); + await Utils.handleHttpUnexpectedError(this.centralServerProvider, error, null, this.props.navigation, this.refresh.bind(this)); return null; } } + + private renderReservationButton() { + const { chargingStation, connector, selectedUser, selectedTag, isAdmin, isSiteAdmin, canReserveNow, canCancelReservation } = this.state; + const { navigation } = this.props; + const commonColor = Utils.getCurrentCommonColor(); + const fabStyles = computeFabStyles(); + const style = computeStyleSheet(); + const connectorIsAvailble = connector?.status ? connector.status !== ChargePointStatus.UNAVAILABLE : false; + return ( + + {this.securityProvider.isComponentReservationActive() && connectorIsAvailble && ( + + navigation.navigate('AddReservation', { + chargingStation, + connector, + user: selectedUser, + tag: selectedTag + }) + } + style={fabStyles.fab}> + + + )} + {connector?.status === ChargePointStatus.RESERVED && canCancelReservation && ( + this.cancelReservationConfirm()} style={[computeFabStyles(commonColor.warning).fab, style.fab]}> + + + )} + {!this.securityProvider.isComponentReservationActive() && canReserveNow && ( + + navigation.navigate('ReserveNow', { + key: `${Utils.randomNumber()}`, + params: { chargingStation, connector, user: selectedUser, tag: selectedTag } + }) + } + style={[fabStyles.fab, style.fab]}> + + + )} + + ); + } + + private renderCancelReservationDialog() { + const { chargingStation, connector } = this.state; + const modalCommonStyle = computeModalCommonStyle(); + const connectorLetter = Utils.getConnectorLetterFromConnectorID(connector.connectorId); + return ( + this.setState({ showCancelReservationDialog: false })} + title={I18n.t('reservations.cancel_reservation.title')} + description={I18n.t('reservations.cancel_reservation.confirm', { + chargingStationID: chargingStation.id, + connectorID: connectorLetter + })} + buttons={[ + { + text: I18n.t('general.yes'), + action: () => { + this.cancelReservation(); + this.setState({ showCancelReservationDialog: false }); + }, + buttonStyle: modalCommonStyle.primaryButton, + buttonTextStyle: modalCommonStyle.primaryButtonText + }, + { + text: I18n.t('general.no'), + action: () => this.setState({ showCancelReservationDialog: false }), + buttonStyle: modalCommonStyle.primaryButton, + buttonTextStyle: modalCommonStyle.primaryButtonText + } + ]} + /> + ); + } } diff --git a/src/screens/charging-stations/connector-details/ChargingStationConnectorDetailsStyles.tsx b/src/screens/charging-stations/connector-details/ChargingStationConnectorDetailsStyles.tsx index 3d754be1c..dab688a3d 100644 --- a/src/screens/charging-stations/connector-details/ChargingStationConnectorDetailsStyles.tsx +++ b/src/screens/charging-stations/connector-details/ChargingStationConnectorDetailsStyles.tsx @@ -168,7 +168,7 @@ export default function computeStyleSheet(): StyleSheet.NamedStyles { marginRight: '5@s' }, currentSoCContainer: { - marginBottom: '13@s', + marginBottom: '13@s' }, socInputsContainer: { width: '100%', @@ -463,7 +463,7 @@ export default function computeStyleSheet(): StyleSheet.NamedStyles { shadowColor: '#000', shadowOffset: { width: 0, - height: 3, + height: 3 }, shadowOpacity: 0.27, shadowRadius: 4.65 @@ -485,8 +485,10 @@ export default function computeStyleSheet(): StyleSheet.NamedStyles { sliderRightTrack: { backgroundColor: commonColor.disabledDark, opacity: 0.2 + }, + fab: { + marginTop: '14@s' } - }); const portraitStyles = {}; const landscapeStyles = { diff --git a/src/screens/charging-stations/connector-details/reserve-now/ChargingStationConnectorReserveNow.tsx b/src/screens/charging-stations/connector-details/reserve-now/ChargingStationConnectorReserveNow.tsx new file mode 100644 index 000000000..f7bad6c00 --- /dev/null +++ b/src/screens/charging-stations/connector-details/reserve-now/ChargingStationConnectorReserveNow.tsx @@ -0,0 +1,447 @@ +import I18n from 'i18n-js'; +import { Icon, Text, View } from 'native-base'; +import React from 'react'; +import { TouchableOpacity } from 'react-native'; +import { Button } from 'react-native-elements'; +import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; +import Orientation from 'react-native-orientation-locker'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import SelectDropdown from 'react-native-select-dropdown'; +import { scale } from 'react-native-size-matters'; +import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; +import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; +import computeFormStyleSheet from '../../../../FormStyles'; +import I18nManager from '../../../../I18n/I18nManager'; +import CarComponent from '../../../../components/car/CarComponent'; +import DateTimePickerComponent from '../../../../components/date-time/DateTimePickerComponent'; +import HeaderComponent from '../../../../components/header/HeaderComponent'; +import { ItemSelectionMode } from '../../../../components/list/ItemsList'; +import computeListItemCommonStyle from '../../../../components/list/ListItemCommonStyle'; +import ModalSelect from '../../../../components/modal/ModalSelect'; +import TagComponent from '../../../../components/tag/TagComponent'; +import UserComponent from '../../../../components/user/UserComponent'; +import BaseScreen from '../../../../screens/base-screen/BaseScreen'; +import Cars from '../../../../screens/cars/Cars'; +import Tags from '../../../../screens/tags/Tags'; +import Users from '../../../../screens/users/list/Users'; +import { RestResponse } from '../../../../types/ActionResponse'; +import BaseProps from '../../../../types/BaseProps'; +import Car from '../../../../types/Car'; +import ChargingStation, { Connector } from '../../../../types/ChargingStation'; +import Tag from '../../../../types/Tag'; +import { UserSessionContext } from '../../../../types/Transaction'; +import User, { UserStatus } from '../../../../types/User'; +import Message from '../../../../utils/Message'; +import Utils from '../../../../utils/Utils'; +import computeStyleSheet from './ChargingStationConnectorReserveNowStyles'; +import moment from 'moment'; +import Constants from '../../../../utils/Constants'; + +interface State { + selectedChargingStation: ChargingStation; + selectedConnector: Connector; + selectedUser: User; + sessionContext: UserSessionContext; + selectedTag: Tag; + expiryDate: Date; + selectedParentTag?: Tag; + selectedCar?: Car; + sessionContextLoading?: boolean; + refreshing?: boolean; + isAdmin?: boolean; + isSiteAdmin?: boolean; +} + +export interface Props extends BaseProps {} + +export default class ReserveNow extends BaseScreen { + public state: State; + public props: Props; + private user: User; + private currentUser: User; + private tag: Tag; + private chargingStation: ChargingStation; + private connector: Connector; + private expiryDate: Date; + + public constructor(props: Props) { + super(props); + this.state = { + selectedChargingStation: null, + selectedConnector: null, + selectedUser: null, + selectedTag: null, + expiryDate: null, + selectedParentTag: null, + selectedCar: null, + refreshing: false, + isAdmin: false, + isSiteAdmin: false, + sessionContext: null, + sessionContextLoading: true + }; + } + + public async componentDidMount(): Promise { + await super.componentDidMount(); + Orientation.lockToPortrait(); + const currentUserToken = this.centralServerProvider.getUserInfo(); + this.currentUser = Utils.getParamFromNavigation(this.props.route, 'user', null) as unknown as User; + this.tag = Utils.getParamFromNavigation(this.props.route, 'tag', null) as unknown as Tag; + this.chargingStation = Utils.getParamFromNavigation(this.props.route, 'chargingStation', null) as unknown as ChargingStation; + this.connector = Utils.getParamFromNavigation(this.props.route, 'connector', null) as unknown as Connector; + this.expiryDate = moment().add(1, 'h').toDate(); + const currentUser = { + id: currentUserToken?.id, + firstName: currentUserToken?.firstName, + name: currentUserToken?.name, + status: UserStatus.ACTIVE, + role: currentUserToken.role, + email: currentUserToken.email + } as User; + this.setState( + { + selectedUser: this.user ?? currentUser, + selectedTag: this.tag ?? this.state.selectedTag, + selectedChargingStation: this.chargingStation, + selectedConnector: this.connector, + expiryDate: this.expiryDate + }, + async () => await this.loadUserSessionContext() + ); + } + + public componentWillUnmount(): void { + super.componentWillUnmount(); + Orientation.unlockAllOrientations(); + } + + public render() { + const { navigation } = this.props; + const { sessionContextLoading, selectedUser, selectedTag, selectedCar, expiryDate } = this.state; + const commonColors = Utils.getCurrentCommonColor(); + const style = computeStyleSheet(); + const formStyle = computeFormStyleSheet(); + return ( + + + + {this.state.selectedUser && ( + + + {this.renderExpiryDatePicker( + 'reservations.expiryDate', + 'datetime', + (newExpiryDate: Date) => this.setState({ expiryDate: newExpiryDate }), + expiryDate + )} + + + )} + {this.securityProvider?.canListUsers() && ( + + + + openable={true} + disabled={false} + defaultItems={[selectedUser]} + renderItem={(user: User) => this.renderUser(style, user)} + renderItemPlaceholder={() => this.renderUserPlaceholder(style)} + onItemsSelected={this.onUserSelected.bind(this)} + navigation={navigation} + selectionMode={ItemSelectionMode.SINGLE}> + + + + + )} + {this.securityProvider?.canListTags() && ( + + + + openable={true} + disabled={false} + defaultItems={[selectedTag]} + renderItem={(tag: Tag) => this.renderTag(style, tag)} + renderItemPlaceholder={() => this.renderTagPlaceholder(style)} + onItemsSelected={(tags: Tag[]) => this.setState({ selectedTag: tags?.[0] }, () => void this.loadUserSessionContext())} + navigation={navigation} + defaultItemLoading={sessionContextLoading} + selectionMode={ItemSelectionMode.SINGLE}> + + + + + )} + {this.securityProvider?.isComponentCarActive() && ( + + + + openable={true} + disabled={false} + defaultItems={[selectedCar]} + renderItemPlaceholder={() => this.renderCarPlaceholder(style)} + renderItem={(car) => } + onItemsSelected={(cars: Car[]) => this.setState({ selectedCar: cars?.[0] }, () => void this.loadUserSessionContext())} + defaultItemLoading={sessionContextLoading} + navigation={navigation} + selectionMode={ItemSelectionMode.SINGLE}> + + + + + )} +