diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index 4d2e9038..f2350072 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -16,6 +16,7 @@ import ArrivalPaymentPage from "../../containers/ArrivalPaymentPageContainer"; import AerodromeStatusPage from '../../containers/AerodromeStatusPageContainer'; import ProfilePage from '../../containers/ProfilePageContainer'; + const UNPROTECTED_ROUTES = [ '/aerodrome-status' ] diff --git a/src/components/wizards/ArrivalWizard/Finish/FinishActions.tsx b/src/components/wizards/ArrivalWizard/Finish/FinishActions.tsx index ae9753de..d2910377 100644 --- a/src/components/wizards/ArrivalWizard/Finish/FinishActions.tsx +++ b/src/components/wizards/ArrivalWizard/Finish/FinishActions.tsx @@ -2,6 +2,7 @@ import React from 'react' import PropTypes from 'prop-types' import ActionButton from './ActionButton' import ActionsWrapper from './ActionsWrapper' +import SaveProfilePrompt from '../../SaveProfilePrompt' import { useTranslation } from 'react-i18next' const FinishActions = ({itemKey, createMovementFromMovement, finish}) => { @@ -10,6 +11,8 @@ const FinishActions = ({itemKey, createMovementFromMovement, finish}) => { const departureImagePath = require('./ic_flight_takeoff_black_48dp_2x.png'); return ( + <> + { dataCy="finish-button" /> + ) } diff --git a/src/components/wizards/DepartureWizard/Finish/Finish.tsx b/src/components/wizards/DepartureWizard/Finish/Finish.tsx index 15c9fb6c..dab3f30f 100644 --- a/src/components/wizards/DepartureWizard/Finish/Finish.tsx +++ b/src/components/wizards/DepartureWizard/Finish/Finish.tsx @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import ImageButton from '../../../ImageButton'; +import SaveProfilePrompt from '../../SaveProfilePrompt'; import Wrapper from '../../ArrivalWizard/Finish/Wrapper'; import Heading from '../../ArrivalWizard/Finish/Heading'; import { useTranslation } from 'react-i18next'; @@ -11,6 +12,7 @@ const Finish = props => { return ( {props.isUpdate === true ? t('departure.updated') : t('departure.created')} + {!props.isUpdate && } ); diff --git a/src/components/wizards/SaveProfilePrompt.tsx b/src/components/wizards/SaveProfilePrompt.tsx new file mode 100644 index 00000000..29ec4513 --- /dev/null +++ b/src/components/wizards/SaveProfilePrompt.tsx @@ -0,0 +1,201 @@ +import React, { useState } from 'react'; +import { connect } from 'react-redux'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import MaterialIcon from '../MaterialIcon'; +import { addAircraft, updateAircraft, saveProfile } from '../../modules/profile'; +import { toAircraftsArray, Aircraft } from '../../modules/profile/migration'; + +const Bar = styled.div` + display: inline-flex; + align-items: center; + gap: 0.6em; + padding: 0.6em 1em; + margin: 0.5em auto; + background-color: #fff8e1; + border: 1px solid #ffe082; + border-radius: 6px; + color: #666; + font-size: 0.9em; +`; + +const StarIcon = styled.span` + color: #e8a735; + display: flex; + align-items: center; +`; + +const SaveLink = styled.button` + background: none; + border: none; + padding: 0; + font-family: inherit; + font-size: inherit; + font-weight: bold; + cursor: pointer; + color: ${props => props.theme.colors.main}; + text-decoration: none; + + &:hover { + text-decoration: underline; + } +`; + +const DismissButton = styled.button` + background: none; + border: none; + padding: 0.2em; + cursor: pointer; + color: #bbb; + font-size: 1.1em; + display: flex; + align-items: center; + + &:hover { + color: #666; + } +`; + +const SuccessMessage = styled.div` + display: inline-flex; + align-items: center; + gap: 0.4em; + padding: 0.6em 1em; + margin: 0.5em auto; + background-color: #e8f5e9; + border: 1px solid #a5d6a7; + border-radius: 6px; + color: #2e7d32; + font-size: 0.9em; + font-weight: bold; +`; + +interface SaveProfilePromptProps { + wizardValues: Record; + profileAircrafts: Aircraft[]; + profileHasPersonalData: boolean; + auth: any; + addAircraft: (aircraft: Aircraft) => void; + updateAircraft: (index: number, aircraft: Aircraft) => void; + saveProfile: (values: Record) => void; +} + +const SaveProfilePrompt: React.FC = ({ + wizardValues, + profileAircrafts, + profileHasPersonalData, + auth, + addAircraft, + updateAircraft, + saveProfile, +}) => { + const { t } = useTranslation(); + const [state, setState] = useState<'prompt' | 'confirmed' | 'dismissed'>('prompt'); + + + if (typeof __CONF__ !== 'undefined' && __CONF__.profileEnabled === false) { + return null; + } + + if (!auth || auth.guest || auth.kiosk || auth.uid === 'ipauth') { + return null; + } + + if (state === 'dismissed' || !wizardValues?.immatriculation) { + return null; + } + + if (state === 'confirmed') { + return ( + + + {t('common.saved')} + + ); + } + + const movementAircraft: Aircraft = { + immatriculation: wizardValues.immatriculation, + aircraftType: wizardValues.aircraftType || null, + mtow: wizardValues.mtow ?? null, + aircraftCategory: wizardValues.aircraftCategory || null, + }; + + const existingIndex = profileAircrafts.findIndex( + a => a.immatriculation === movementAircraft.immatriculation + ); + const existing = existingIndex >= 0 ? profileAircrafts[existingIndex] : null; + + const aircraftChanged = existing && ( + existing.aircraftType !== movementAircraft.aircraftType || + existing.mtow !== movementAircraft.mtow || + existing.aircraftCategory !== movementAircraft.aircraftCategory + ); + + // Determine prompt mode + let promptKey: string | null = null; + let onConfirm: () => void; + + if (!profileHasPersonalData) { + promptKey = 'profile.saveDetailsPrompt'; + onConfirm = () => { + saveProfile({ + firstname: wizardValues.firstname || null, + lastname: wizardValues.lastname || null, + email: wizardValues.email || null, + phone: wizardValues.phone || null, + memberNr: wizardValues.memberNr || null, + }); + if (!existing) { + addAircraft(movementAircraft); + } else if (aircraftChanged) { + updateAircraft(existingIndex, movementAircraft); + } + setState('confirmed'); + }; + } else if (!existing) { + promptKey = 'profile.saveAircraftPrompt'; + onConfirm = () => { + addAircraft(movementAircraft); + setState('confirmed'); + }; + } else if (aircraftChanged) { + promptKey = 'profile.updateAircraftPrompt'; + onConfirm = () => { + updateAircraft(existingIndex, movementAircraft); + setState('confirmed'); + }; + } else { + return null; + } + + return ( + + + {t(promptKey, { immatriculation: movementAircraft.immatriculation })} + {t('common.save')} + setState('dismissed')} aria-label="dismiss"> + + + + ); +}; + +const mapStateToProps = (state: any) => { + const profile = state.profile.profile; + const aircrafts = toAircraftsArray(profile?.aircrafts) || []; + return { + wizardValues: state.ui.wizard.values, + profileAircrafts: aircrafts, + profileHasPersonalData: !!(profile?.firstname && profile?.lastname), + auth: state.auth.data, + }; +}; + +const mapDispatchToProps = { + addAircraft, + updateAircraft, + saveProfile, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(SaveProfilePrompt); diff --git a/src/locales/de.json b/src/locales/de.json index 76c59560..e2f6c57c 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -12,6 +12,9 @@ "login": "Anmelden", "confirm": "Bestätigen", "finish": "Beenden", + "yes": "Ja", + "no": "Nein", + "saved": "Gespeichert!", "add": "Hinzufügen", "download": "Herunterladen", "status": "Status", @@ -294,7 +297,10 @@ "save": "Speichern", "addAircraft": "Flugzeug hinzufügen", "removeAircraft": "Entfernen", - "noAircraft": "Noch keine Flugzeuge gespeichert." + "noAircraft": "Noch keine Flugzeuge gespeichert.", + "saveDetailsPrompt": "Pilotendaten und Flugzeug speichern für das nächste Mal?", + "saveAircraftPrompt": "{{immatriculation}} im Profil speichern?", + "updateAircraftPrompt": "{{immatriculation}} im Profil aktualisieren?" }, "lockMovements": { "heading": "Erfasste Bewegungen sperren", diff --git a/src/locales/en.json b/src/locales/en.json index bbf8a0b0..1e2318d0 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -12,6 +12,9 @@ "login": "Log in", "confirm": "Confirm", "finish": "Finish", + "yes": "Yes", + "no": "No", + "saved": "Saved!", "add": "Add", "download": "Download", "status": "Status", @@ -294,7 +297,10 @@ "save": "Save", "addAircraft": "Add aircraft", "removeAircraft": "Remove", - "noAircraft": "No aircraft saved." + "noAircraft": "No aircraft saved.", + "saveDetailsPrompt": "Save pilot info and aircraft so they're prefilled next time?", + "saveAircraftPrompt": "Save {{immatriculation}} to your profile?", + "updateAircraftPrompt": "Update {{immatriculation}} in your profile?" }, "lockMovements": { "heading": "Lock recorded movements",