Skip to content

Commit

Permalink
Merge branch 'main' into 1430-extend-card
Browse files Browse the repository at this point in the history
  • Loading branch information
seluianova authored Nov 19, 2024
2 parents f7ed055 + 9d279d8 commit 02e5557
Show file tree
Hide file tree
Showing 22 changed files with 431 additions and 109 deletions.
5 changes: 3 additions & 2 deletions administration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@mui/icons-material": "^5.11.16",
"@mui/material": "^5.13.6",
"@mui/system": "^5.13.6",
"@mui/x-date-pickers": "^7.22.2",
"@nivo/bar": "^0.85.1",
"@pdf-lib/fontkit": "^1.1.1",
"@types/geojson": "^7946.0.14",
Expand All @@ -29,7 +30,6 @@
"graphql": "^16.8.1",
"i18next": "^23.16.4",
"localforage": "^1.10.0",
"normalize-strings": "^1.1.1",
"normalize.css": "^8.0.1",
"notistack": "^3.0.1",
"pdf-lib": "^1.17.1",
Expand All @@ -38,7 +38,8 @@
"react-flip-move": "^3.0.5",
"react-i18next": "^15.1.0",
"react-router-dom": "^6.14.0",
"styled-components": "^5.3.11"
"styled-components": "^5.3.11",
"xregexp": "^5.1.1"
},
"devDependencies": {
"@babel/core": "^7.22.5",
Expand Down
7 changes: 5 additions & 2 deletions administration/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { LocalizationProvider } from '@mui/x-date-pickers'
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'
import React, { ReactElement } from 'react'

import AppApolloProvider from './AppApolloProvider'
Expand All @@ -14,13 +16,14 @@ if (!process.env.REACT_APP_API_BASE_URL) {

const App = (): ReactElement => {
useMetaTags()

return (
<ProjectConfigProvider>
<AppToasterProvider>
<AuthProvider>
<AppApolloProvider>
<Router />
<LocalizationProvider dateAdapter={AdapterDateFns}>
<Router />
</LocalizationProvider>
</AppApolloProvider>
</AuthProvider>
</AppToasterProvider>
Expand Down
2 changes: 1 addition & 1 deletion administration/src/bp-modules/cards/AddCardForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const AddCardForm = ({ card, onRemove, updateCard }: CreateCardsFormProps): Reac
/>
</FormGroup>
)}
<ExtensionForms card={card} updateCard={updateCard} />
<ExtensionForms card={card} updateCard={updateCard} showRequired />
</UiCard>
)
}
Expand Down
4 changes: 3 additions & 1 deletion administration/src/bp-modules/cards/ExtensionForms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import { Card, getExtensions } from '../../cards/Card'
type ExtensionFormsProps = {
card: Card
updateCard: (card: Partial<Card>) => void
showRequired: boolean
}

const ExtensionForms = ({ card, updateCard }: ExtensionFormsProps): ReactElement => (
const ExtensionForms = ({ card, updateCard, showRequired }: ExtensionFormsProps): ReactElement => (
<>
{getExtensions(card).map(({ extension: { Component, ...extension }, state }) => (
<Component
key={extension.name}
showRequired={showRequired}
value={state}
setValue={value => updateCard({ extensions: value })}
isValid={extension.isValid(state)}
Expand Down
62 changes: 62 additions & 0 deletions administration/src/bp-modules/components/CustomDatePicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { DesktopDatePicker } from '@mui/x-date-pickers'
import { deDE } from '@mui/x-date-pickers/locales'
import React, { ReactElement } from 'react'

import useWindowDimensions from '../../hooks/useWindowDimensions'

type CustomDatePickerProps = {
date: Date | null
onBlur: () => void
onChange: (date: Date | null) => void
onClear: () => void
isValid: boolean
minDate?: Date
maxDate?: Date
disableFuture: boolean
}

const CustomDatePicker = ({
date,
onBlur,
onChange,
onClear,
isValid,
minDate,
maxDate,
disableFuture,
}: CustomDatePickerProps): ReactElement => {
const { viewportSmall } = useWindowDimensions()
const formStyle = viewportSmall ? { fontSize: 16, padding: '9px 10px' } : { fontSize: 14, padding: '6px 10px' }
const textFieldBoxShadow =
'0 0 0 0 rgba(205, 66, 70, 0), 0 0 0 0 rgba(205, 66, 70, 0), inset 0 0 0 1px #cd4246, inset 0 0 0 1px rgba(17, 20, 24, 0.2), inset 0 1px 1px rgba(17, 20, 24, 0.3)'
return (
<DesktopDatePicker
views={['year', 'month', 'day']}
value={date}
format='dd.MM.yyyy'
slotProps={{
clearIcon: { fontSize: viewportSmall ? 'medium' : 'small' },
openPickerIcon: { fontSize: 'small' },
field: { clearable: true, onClear },
textField: {
placeholder: 'TT.MM.JJJJ',
error: !isValid,
spellCheck: false,
onBlur,
sx: {
width: '100%',
input: formStyle,
boxShadow: !isValid ? textFieldBoxShadow : undefined,
},
},
}}
localeText={deDE.components.MuiLocalizationProvider.defaultProps.localeText}
disableFuture={disableFuture}
minDate={minDate}
maxDate={maxDate}
onChange={onChange}
/>
)
}

export default CustomDatePicker
79 changes: 48 additions & 31 deletions administration/src/bp-modules/self-service/CardSelfServiceForm.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { Checkbox, FormGroup, InputGroup, Intent } from '@blueprintjs/core'
import InfoOutlined from '@mui/icons-material/InfoOutlined'
import { Alert, styled } from '@mui/material'
import { styled } from '@mui/material'
import React, { ReactElement, useContext, useState } from 'react'

import { Card, isFullNameValid, isValid } from '../../cards/Card'
import { Card, getFullNameValidationErrorMessage, isFullNameValid, isValid } from '../../cards/Card'
import ClearInputButton from '../../cards/extensions/components/ClearInputButton'
import useWindowDimensions from '../../hooks/useWindowDimensions'
import BasicDialog from '../../mui-modules/application/BasicDialog'
import { ProjectConfigContext } from '../../project-configs/ProjectConfigContext'
import { removeMultipleSpaces } from '../../util/helper'
import { useAppToaster } from '../AppToaster'
import ExtensionForms from '../cards/ExtensionForms'
import { DataPrivacyAcceptingStatus } from './CardSelfServiceView'
import { ActionButton } from './components/ActionButton'
import FormErrorMessage from './components/FormErrorMessage'
import { IconTextButton } from './components/IconTextButton'
import { UnderlineTextButton } from './components/UnderlineTextButton'

Expand All @@ -19,34 +23,18 @@ const StyledCheckbox = styled(Checkbox)`
margin-left: 4px;
`

const StyledAlert = styled(Alert)`
margin-bottom: 24px;
white-space: pre-line;
`

const Container = styled('div')`
margin-bottom: 24px;
`

type CardSelfServiceFormProps = {
card: Card
updateCard: (card: Partial<Card>) => void
dataPrivacyAccepted: boolean
setDataPrivacyAccepted: (value: boolean) => void
dataPrivacyAccepted: DataPrivacyAcceptingStatus
setDataPrivacyAccepted: (status: DataPrivacyAcceptingStatus) => void
generateCards: () => Promise<void>
}

const getTooltipMessage = (cardsValid: boolean, dataPrivacyAccepted: boolean): string => {
const tooltipMessages: string[] = []
if (!cardsValid) {
tooltipMessages.push('Mindestens eine Ihrer Angaben ist ungültig.')
}
if (!dataPrivacyAccepted) {
tooltipMessages.push('Bitte akzeptieren Sie die Datenschutzerklärung.')
}

return tooltipMessages.join('\n')
}
const CardSelfServiceForm = ({
card,
updateCard,
Expand All @@ -56,10 +44,31 @@ const CardSelfServiceForm = ({
}: CardSelfServiceFormProps): ReactElement => {
const { viewportSmall } = useWindowDimensions()
const projectConfig = useContext(ProjectConfigContext)
const [formSendAttempt, setFormSendAttempt] = useState(false)
const [touchedFullName, setTouchedFullName] = useState(false)
const [openDataPrivacy, setOpenDataPrivacy] = useState<boolean>(false)
const [openReferenceInformation, setOpenReferenceInformation] = useState<boolean>(false)
const cardValid = isValid(card, { expirationDateNullable: true })
const cardCreationDisabled = !cardValid || !dataPrivacyAccepted
const appToaster = useAppToaster()
const showErrorMessage = touchedFullName || formSendAttempt

const createKoblenzPass = async () => {
setFormSendAttempt(true)
if (dataPrivacyAccepted === DataPrivacyAcceptingStatus.untouched) {
setDataPrivacyAccepted(DataPrivacyAcceptingStatus.denied)
}
if (!cardValid || dataPrivacyAccepted !== DataPrivacyAcceptingStatus.accepted) {
appToaster?.show({
message: (
<FormErrorMessage style={{ color: 'white' }} errorMessage='Mindestens eine Ihrer Angaben ist ungültig.' />
),
timeout: 0,
intent: 'danger',
})
return
}
await generateCards()
}

return (
<>
Expand All @@ -76,27 +85,35 @@ const CardSelfServiceForm = ({
input={card.fullName}
/>
}
intent={isFullNameValid(card) ? undefined : Intent.DANGER}
intent={isFullNameValid(card) || !showErrorMessage ? undefined : Intent.DANGER}
value={card.fullName}
onChange={event => updateCard({ fullName: event.target.value })}
onBlur={() => setTouchedFullName(true)}
onChange={event => updateCard({ fullName: removeMultipleSpaces(event.target.value) })}
/>
{showErrorMessage && <FormErrorMessage errorMessage={getFullNameValidationErrorMessage(card.fullName)} />}
</FormGroup>
<ExtensionForms card={card} updateCard={updateCard} />
<ExtensionForms card={card} updateCard={updateCard} showRequired={formSendAttempt} />
<IconTextButton onClick={() => setOpenReferenceInformation(true)}>
<InfoOutlined />
Wo finde ich das Aktenzeichen?
</IconTextButton>
<StyledCheckbox checked={dataPrivacyAccepted} onChange={() => setDataPrivacyAccepted(!dataPrivacyAccepted)}>
<StyledCheckbox
checked={dataPrivacyAccepted === DataPrivacyAcceptingStatus.accepted}
onChange={() =>
setDataPrivacyAccepted(
dataPrivacyAccepted === DataPrivacyAcceptingStatus.accepted
? DataPrivacyAcceptingStatus.denied
: DataPrivacyAcceptingStatus.accepted
)
}>
Ich akzeptiere die{' '}
<UnderlineTextButton onClick={() => setOpenDataPrivacy(true)}>Datenschutzerklärung</UnderlineTextButton>.
</StyledCheckbox>
{dataPrivacyAccepted === DataPrivacyAcceptingStatus.denied && (
<FormErrorMessage errorMessage='Bitte akzeptieren sie die Datenschutzerklärung' />
)}
</Container>
{cardCreationDisabled && (
<StyledAlert variant='outlined' severity='warning'>
{getTooltipMessage(cardValid, dataPrivacyAccepted)}
</StyledAlert>
)}
<ActionButton onClick={generateCards} variant='contained' disabled={cardCreationDisabled} size='large'>
<ActionButton onClick={createKoblenzPass} variant='contained' size='large'>
KoblenzPass erstellen
</ActionButton>
<BasicDialog
Expand Down
14 changes: 11 additions & 3 deletions administration/src/bp-modules/self-service/CardSelfServiceView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,18 @@ const StyledInfoTextButton = styled(IconTextButton)`
margin: 0;
`

export enum DataPrivacyAcceptingStatus {
untouched,
accepted,
denied,
}

// TODO 1646 Add tests for CardSelfService
const CardSelfServiceView = (): ReactElement => {
const projectConfig = useContext(ProjectConfigContext)
const [dataPrivacyAccepted, setDataPrivacyAccepted] = useState<boolean>(false)
const [dataPrivacyCheckbox, setDataPrivacyCheckbox] = useState<DataPrivacyAcceptingStatus>(
DataPrivacyAcceptingStatus.untouched
)
const {
selfServiceState,
setSelfServiceState,
Expand Down Expand Up @@ -127,8 +135,8 @@ const CardSelfServiceView = (): ReactElement => {
{selfServiceState === CardSelfServiceStep.form && (
<CardSelfServiceForm
card={selfServiceCard}
dataPrivacyAccepted={dataPrivacyAccepted}
setDataPrivacyAccepted={setDataPrivacyAccepted}
dataPrivacyAccepted={dataPrivacyCheckbox}
setDataPrivacyAccepted={setDataPrivacyCheckbox}
updateCard={updatedCard => setSelfServiceCard(updateCard(selfServiceCard, updatedCard))}
generateCards={generateCards}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import InfoOutlined from '@mui/icons-material/InfoOutlined'
import { styled } from '@mui/material'
import React, { CSSProperties, ReactElement } from 'react'

const Container = styled('div')`
margin: 6px 0;
color: #ba1a1a;
display: flex;
gap: 8px;
align-items: center;
font-size: 14px;
`

type FormErrorMessageProps = {
errorMessage: string | null
style?: CSSProperties
}

const FormErrorMessage = ({ errorMessage, style }: FormErrorMessageProps): ReactElement | null =>
errorMessage ? (
<Container style={style}>
<InfoOutlined />
{errorMessage}
</Container>
) : null

export default FormErrorMessage
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ApolloError } from '@apollo/client'
import { useCallback, useContext, useState } from 'react'
import React, { useCallback, useContext, useState } from 'react'

import { Card, generateCardInfo, initializeCard } from '../../../cards/Card'
import { generatePdf } from '../../../cards/PdfFactory'
Expand All @@ -12,6 +12,7 @@ import { base64ToUint8Array, uint8ArrayToBase64 } from '../../../util/base64'
import downloadDataUri from '../../../util/downloadDataUri'
import getCustomDeepLinkFromQrCode from '../../../util/getCustomDeepLinkFromQrCode'
import { useAppToaster } from '../../AppToaster'
import FormErrorMessage from '../components/FormErrorMessage'

export enum CardSelfServiceStep {
form,
Expand Down Expand Up @@ -54,9 +55,9 @@ const useCardGeneratorSelfService = (): UseCardGeneratorSelfServiceReturn => {
if (error instanceof ApolloError) {
const { title } = getMessageFromApolloError(error)
appToaster?.show({
message: title,
intent: 'danger',
message: <FormErrorMessage style={{ color: 'white' }} errorMessage={title} />,
timeout: 0,
intent: 'danger',
})
} else {
appToaster?.show({
Expand Down
Loading

0 comments on commit 02e5557

Please sign in to comment.