Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

604: Withdraw application #865

Merged
merged 22 commits into from
Mar 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
741a934
604: add accessKey and withdrawal date to applications. create basic …
f1sh1918 Mar 14, 2023
a9ec892
604: add withdrawal function
f1sh1918 Mar 15, 2023
b92cff8
604: refactor ApplicationOverview to reuse ApplicationAction
f1sh1918 Mar 15, 2023
9b64774
604: show confirm message, add error handling, filter withdrawed appl…
f1sh1918 Mar 15, 2023
d3854a1
604: rename props due to a11y restrictions
f1sh1918 Mar 15, 2023
fad2893
604: rename props due to a11y restrictions, show withdraw label in ad…
f1sh1918 Mar 15, 2023
638f77d
604: rename controller, add alert
f1sh1918 Mar 15, 2023
e7aede0
604: Separate applicant application view from servant applicant view,…
f1sh1918 Mar 16, 2023
92991a0
Merge remote-tracking branch 'origin/main' into 604-withdraw-application
f1sh1918 Mar 16, 2023
c672ff6
604: update specs file, rename query
f1sh1918 Mar 16, 2023
9c827f0
604: add hint to delete the application
f1sh1918 Mar 16, 2023
3434f11
Update administration/src/components/applications/VerificationsView.tsx
f1sh1918 Mar 16, 2023
4f1eecc
Update administration/src/components/applications/ApplicationApplican…
f1sh1918 Mar 16, 2023
97b9534
Update administration/src/components/applications/ApplicationApplican…
f1sh1918 Mar 16, 2023
9af6ad4
Update administration/src/components/applications/ApplicationApplican…
f1sh1918 Mar 16, 2023
0c1a69d
Update administration/src/ErrorHandler.tsx
f1sh1918 Mar 16, 2023
ccc2c44
Update administration/src/components/applications/ApplicationApplican…
f1sh1918 Mar 16, 2023
4fee7cf
Update administration/src/components/applications/ApplicationApplican…
f1sh1918 Mar 16, 2023
3f51e7d
Update administration/src/components/applications/ApplicationApplican…
f1sh1918 Mar 16, 2023
2d2fffd
Update administration/src/components/applications/ApplicationApplican…
f1sh1918 Mar 16, 2023
b043df0
Update administration/src/components/applications/ApplicationApplican…
f1sh1918 Mar 16, 2023
f9bf48c
604: remove verification quick indicator, adjust getApplicationsByApp…
f1sh1918 Mar 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions administration/src/ErrorHandler.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import { Button, Card, H3 } from '@blueprintjs/core'
import { Button, Card, Typography } from '@mui/material'
import React, { ReactElement } from 'react'
import { styled } from '@mui/system'

type ErrorHandlerProps = {
title?: string
refetch: () => void
}

const ErrorContainer = styled(Card)`
padding: 20px;
`

const ErrorHandler = ({ refetch, title = 'Ein Fehler ist aufgetreten.' }: ErrorHandlerProps): ReactElement => {
return (
<Card>
<H3>{title}</H3>
<Button intent='primary' onClick={() => refetch()}>
<ErrorContainer>
<Typography variant='h6'>{title}</Typography>
<Button color='primary' variant='contained' onClick={() => refetch()}>
Erneut versuchen
</Button>
</Card>
</ErrorContainer>
)
}

Expand Down
2 changes: 2 additions & 0 deletions administration/src/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import ManageUsersController from './components/users/ManageUsersController'
import ApplyController from './application/components/ApplyController'
import DataPrivacyPolicy from './components/DataPrivacyPolicy'
import ApplicationVerificationController from './application-verification/VerificationController'
import ApplicationApplicantController from './components/applications/ApplicationApplicantController'

const Main = styled.div`
flex-grow: 1;
Expand All @@ -42,6 +43,7 @@ const Router = () => {
path: '/antrag-verifizieren/:applicationVerificationAccessKey',
element: <ApplicationVerificationController />,
},
{ path: '/antrag-einsehen/:accessKey', element: <ApplicationApplicantController /> },
]
: []),
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React, { useState } from 'react'
import { NonIdealState } from '@blueprintjs/core'

import ErrorHandler from '../../ErrorHandler'
import { useParams } from 'react-router-dom'
import styled from 'styled-components'
import { useGetApplicationByApplicantQuery } from '../../generated/graphql'
import ApplicationApplicantView from './ApplicationApplicantView'
import { CircularProgress } from '@mui/material'
import { SnackbarProvider, useSnackbar } from 'notistack'

const CenteredMessage = styled(NonIdealState)`
margin: auto;
`

const ApplicationApplicantController = (props: { providedKey: string }) => {
const [withdrawed, setWithdrawed] = useState<boolean>(false)
const { enqueueSnackbar } = useSnackbar()
const { loading, error, data, refetch } = useGetApplicationByApplicantQuery({
variables: { accessKey: props.providedKey },
onError: error => {
console.error(error)
enqueueSnackbar('Etwas ist schief gelaufen.', { variant: 'error' })
},
})

if (loading) return <CircularProgress style={{ margin: 'auto' }} />
else if (error || !data) return <ErrorHandler refetch={refetch} />
if (data.application.withdrawalDate) return <CenteredMessage title='Ihr Antrag wurde bereits zurückgezogen' />
if (withdrawed) return <CenteredMessage title='Ihr Antrag wurde zurückgezogen' />
else {
return (
<ApplicationApplicantView
application={data.application}
gotWithdrawed={() => setWithdrawed(true)}
providedKey={props.providedKey}
/>
)
}
}

const ControllerWithAccessKey = () => {
const { accessKey } = useParams()

if (!accessKey) {
return null
} else {
return (
<SnackbarProvider>
<ApplicationApplicantController providedKey={accessKey} />
</SnackbarProvider>
)
}
}

export default ControllerWithAccessKey
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React, { ReactElement, useContext, useState } from 'react'
import { Application } from './ApplicationsOverview'
import VerificationsView from './VerificationsView'
import JsonFieldView, { GeneralJsonField } from './JsonFieldView'
import { ProjectConfigContext } from '../../project-configs/ProjectConfigContext'
import { useWithdrawApplicationMutation } from '../../generated/graphql'

import { styled } from '@mui/system'
import { Button, Card, CircularProgress, Divider, Typography } from '@mui/material'
import { format } from 'date-fns'
import ConfirmDialog from '../../application/components/ConfirmDialog'
import { Delete } from '@mui/icons-material'
import { useSnackbar } from 'notistack'

const ApplicationViewCard = styled(Card)`
max-width: 800px;
margin: 10px;
align-self: center;
`

type ApplicationApplicantViewProps = {
application: Application
providedKey: string
gotWithdrawed: () => void
}

const ApplicationApplicantView = ({
application,
providedKey,
gotWithdrawed,
}: ApplicationApplicantViewProps): ReactElement => {
const [dialogOpen, setDialogOpen] = useState<boolean>(false)
const { createdDate: createdDateString, jsonValue, id } = application
const createdDate = new Date(createdDateString)
const jsonField: GeneralJsonField = JSON.parse(jsonValue)
const config = useContext(ProjectConfigContext)
const baseUrl = `${process.env.REACT_APP_API_BASE_URL}/application/${config.projectId}/${id}`
const { enqueueSnackbar } = useSnackbar()

const [withdrawApplication, { loading: withdrawalLoading }] = useWithdrawApplicationMutation({
onError: error => {
console.error(error)
enqueueSnackbar('Etwas ist schief gelaufen.', { variant: 'error' })
},
onCompleted: ({ withdrawed }: { withdrawed: boolean }) => {
if (withdrawed) gotWithdrawed()
else {
console.error('Withdraw operation returned false.')
enqueueSnackbar('Der Antrag wurde bereits zurückgezogen.', { variant: 'error' })
}
},
})

const submitWithdrawal = () => {
withdrawApplication({
variables: {
accessKey: providedKey,
},
})
}

if (withdrawalLoading) return <CircularProgress style={{ margin: 'auto' }} />

return (
<ApplicationViewCard elevation={2}>
<div style={{ overflow: 'visible', padding: '20px' }}>
<Typography mb='8px' variant='h6'>
Ihr Antrag auf die Ehrenamtskarte Bayern vom {format(createdDate, 'dd.MM.yyyy, HH:mm')}
</Typography>
<JsonFieldView jsonField={jsonField} baseUrl={baseUrl} key={0} hierarchyIndex={0} />
<Divider style={{ margin: '24px 0px' }} />
<VerificationsView verifications={application.verifications} />
{!application.withdrawalDate && (
<>
<Divider style={{ margin: '24px 0px' }} />
<Typography mt='8px' mb='16px' variant='body2'>
Hier können Sie Ihren Antrag zurückziehen und Ihre Eingaben unwiderruflich löschen.
</Typography>
<Button variant='contained' endIcon={<Delete />} onClick={() => setDialogOpen(true)}>
Antrag zurückziehen
</Button>
<ConfirmDialog
open={dialogOpen}
onUpdateOpen={setDialogOpen}
title='Antrag zurückziehen?'
content='Möchten Sie den Antrag zurückziehen?'
onConfirm={submitWithdrawal}
/>
</>
)}
</div>
</ApplicationViewCard>
)
}

export default ApplicationApplicantView
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Alert, Button, Card, Divider, H4, IResizeEntry, NonIdealState, ResizeSensor } from '@blueprintjs/core'
import { Alert, Button, Callout, Card, Divider, H4, IResizeEntry, NonIdealState, ResizeSensor } from '@blueprintjs/core'
import { format } from 'date-fns'
import React, { FunctionComponent, useContext, useState } from 'react'
import styled from 'styled-components'
Expand All @@ -9,7 +9,7 @@ import { GetApplicationsQuery, useDeleteApplicationMutation } from '../../genera
import { ProjectConfigContext } from '../../project-configs/ProjectConfigContext'
import VerificationsView, { VerificationsQuickIndicator } from './VerificationsView'

type Application = GetApplicationsQuery['applications'][number]
export type Application = GetApplicationsQuery['applications'][number]

export const CARD_PADDING = 20
const COLLAPSED_HEIGHT = 250
Expand Down Expand Up @@ -40,11 +40,15 @@ const ExpandContainer = styled.div<{ $collapsed: boolean }>`
pointer-events: ${props => (props.$collapsed ? 'all' : 'none')};
`

const WithdrawAlert = styled(Callout)`
margin-bottom: 16px;
`

const ApplicationView: FunctionComponent<{ application: Application; gotDeleted: () => void }> = ({
application,
gotDeleted,
}) => {
const { createdDate: createdDateString, jsonValue, id } = application
const { createdDate: createdDateString, jsonValue, id, withdrawalDate } = application
const createdDate = new Date(createdDateString)
const jsonField: GeneralJsonField = JSON.parse(jsonValue)
const config = useContext(ProjectConfigContext)
Expand Down Expand Up @@ -89,6 +93,13 @@ const ApplicationView: FunctionComponent<{ application: Application; gotDeleted:
<H4>Antrag vom {format(createdDate, 'dd.MM.yyyy, HH:mm')}</H4>
<VerificationsQuickIndicator verifications={application.verifications} />
</div>
{withdrawalDate && (
<WithdrawAlert intent='warning'>
Der Antrag wurde vom Antragssteller am {format(new Date(withdrawalDate), 'dd.MM.yyyy, HH:mm')}{' '}
zurückgezogen. <br />
Bitte löschen Sie den Antrag zeitnah.
</WithdrawAlert>
)}
<JsonFieldView jsonField={jsonField} baseUrl={baseUrl} key={0} hierarchyIndex={0} />
<Divider style={{ margin: '24px 0px' }} />
<VerificationsView verifications={application.verifications} />
Expand Down Expand Up @@ -126,7 +137,7 @@ const ApplicationView: FunctionComponent<{ application: Application; gotDeleted:
}

// Necessary for FlipMove, as it cannot handle functional components
class ApplicationViewComponent extends React.Component<{ application: Application; gotDeleted: () => void }> {
export class ApplicationViewComponent extends React.Component<{ application: Application; gotDeleted: () => void }> {
render() {
return <ApplicationView {...this.props} />
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export const VerificationsQuickIndicator = ({ verifications }: { verifications:
return (
<Tooltip2
content={
<div style={{ textAlign: 'center' }}>
<div>
<b>Bestätigung(en) durch Organisationen:</b>
<br />
Bestätigt/Ausstehend/Widersprochen
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
query getApplicationByApplicant($accessKey: String!) {
application: getApplicationByApplicant(accessKey: $accessKey) {
id
createdDate
jsonValue
withdrawalDate
verifications {
contactEmailAddress
organizationName
verifiedDate
rejectedDate
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ query getApplications($regionId: Int!) {
id
createdDate
jsonValue
withdrawalDate
verifications {
contactEmailAddress
organizationName
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mutation withdrawApplication($accessKey: String!) {
withdrawed: withdrawApplication(accessKey: $accessKey)
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ object Applications : IntIdTable() {
val regionId = reference("regionId", Regions)
val jsonValue = text("jsonValue")
val createdDate = datetime("createdDate").defaultExpression(CurrentDateTime)
val accessKey = varchar("accessKey", 100).uniqueIndex()
val withdrawalDate = datetime("withdrawalDate").nullable()
}

class ApplicationEntity(id: EntityID<Int>) : IntEntity(id) {
Expand All @@ -24,6 +26,8 @@ class ApplicationEntity(id: EntityID<Int>) : IntEntity(id) {
var regionId by Applications.regionId
var jsonValue by Applications.jsonValue
var createdDate by Applications.createdDate
var accessKey by Applications.accessKey
var withdrawalDate by Applications.withdrawalDate
}

object ApplicationVerifications : IntIdTable() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,23 @@ object ApplicationRepository {
files: List<Part>,
): Pair<ApplicationEntity, List<ApplicationVerificationEntity>> {
return transaction {
val random = SecureRandom.getInstanceStrong()
val byteArray = ByteArray(64)
random.nextBytes(byteArray)
val applicationKey = Base64.getUrlEncoder().encodeToString(byteArray)
val newApplication =
ApplicationEntity.new {
this.regionId = EntityID(regionId, Regions)
this.jsonValue = toString(applicationJson)
this.accessKey = applicationKey
}

val random = SecureRandom.getInstanceStrong()
val verificationEntities = applicationVerifications.map {
val byteArray = ByteArray(64)
random.nextBytes(byteArray)
val key = Base64.getUrlEncoder().encodeToString(byteArray)
val verificationKey = Base64.getUrlEncoder().encodeToString(byteArray)
ApplicationVerificationEntity.new {
this.applicationId = newApplication.id
this.accessKey = key
this.accessKey = verificationKey
this.contactName = it.contactName
this.organizationName = it.organizationName
this.contactEmailAddress = it.contactEmailAddress
Expand Down Expand Up @@ -93,6 +96,14 @@ object ApplicationRepository {
}
}

fun getApplicationByApplicant(accessKey: String): ApplicationView {
return transaction {
ApplicationEntity.find { Applications.accessKey eq accessKey }
.single()
.let { ApplicationView.fromDbEntity(it) }
}
}

fun getApplicationByApplicationVerificationAccessKey(applicationVerificationAccessKey: String): ApplicationView {
return transaction {
(Applications innerJoin ApplicationVerifications)
Expand Down Expand Up @@ -160,6 +171,18 @@ object ApplicationRepository {
}
}

fun withdrawApplication(accessKey: String): Boolean {
return transaction {
val application = ApplicationEntity.find { Applications.accessKey eq accessKey }.single()
if (application.withdrawalDate == null) {
application.withdrawalDate = LocalDateTime.now()
michael-markl marked this conversation as resolved.
Show resolved Hide resolved
true
} else {
false
}
}
}

fun findByIds(ids: List<Int>): List<ApplicationEntity?> {
return transaction {
ApplicationEntity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ class EakApplicationMutationService {
}
}

@GraphQLDescription("Withdraws the application")
fun withdrawApplication(accessKey: String): Boolean {
return transaction {
ApplicationRepository.withdrawApplication(accessKey)
}
}

@GraphQLDescription("Verifies or rejects an application verification")
fun verifyOrRejectApplicationVerification(
accessKey: String,
Expand Down
Loading