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 6 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
8 changes: 7 additions & 1 deletion administration/src/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import ForgotPasswordController from './components/auth/ForgotPasswordController
import ManageUsersController from './components/users/ManageUsersController'
import ApplyController from './application/components/ApplyController'
import DataPrivacyPolicy from './components/DataPrivacyPolicy'
import ApplicationUserOverviewController from './components/applications/ApplicationUserOverviewController'

const Main = styled.div`
flex-grow: 1;
Expand All @@ -34,7 +35,12 @@ const Router = () => {
{ path: '/forgot-password', element: <ForgotPasswordController /> },
{ path: '/reset-password/:passwordResetKey', element: <ResetPasswordController /> },
{ path: '/data-privacy-policy', element: <DataPrivacyPolicy /> },
projectConfig.applicationFeatureEnabled ? { path: '/beantragen', element: <ApplyController /> } : null,
...(projectConfig.applicationFeatureEnabled
? [
{ path: '/beantragen', element: <ApplyController /> },
{ path: '/antrag-einsehen/:accessKey', element: <ApplicationUserOverviewController /> },
]
: []),
{
path: '*',
element: !isLoggedIn ? (
Expand Down
47 changes: 47 additions & 0 deletions administration/src/components/applications/ApplicationAction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Alert, Button } from '@blueprintjs/core'
import React, { ReactElement, useState } from 'react'

type ApplicationActionProps = {
confirmAction: () => void
loading: boolean
cancelButtonText: string
confirmButtonText: string
buttonLabel: string
dialogText: string
}

const ApplicationAction = ({
confirmAction,
loading,
buttonLabel,
confirmButtonText,
cancelButtonText,
dialogText,
}: ApplicationActionProps): ReactElement => {
const [dialogOpen, setDialogOpen] = useState<boolean>(false)

const onConfirm = () => {
confirmAction()
setDialogOpen(false)
}
return (
<>
<Button onClick={() => setDialogOpen(true)} intent='danger' icon='trash'>
{buttonLabel}
</Button>
<Alert
cancelButtonText={cancelButtonText}
confirmButtonText={confirmButtonText}
icon='trash'
intent='danger'
isOpen={dialogOpen}
loading={loading}
onCancel={() => setDialogOpen(false)}
onConfirm={onConfirm}>
<p>{dialogText}</p>
</Alert>
</>
)
}

export default ApplicationAction
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React, { useState } from 'react'
import { NonIdealState, Spinner } from '@blueprintjs/core'
f1sh1918 marked this conversation as resolved.
Show resolved Hide resolved

import { useGetApplicationByUserAccessKeyQuery } from '../../generated/graphql'
import ErrorHandler from '../../ErrorHandler'
import { useParams } from 'react-router-dom'
import styled from 'styled-components'
import { ApplicationViewComponent } from './ApplicationsOverview'
import { useAppToaster } from '../AppToaster'

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

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

const ApplicationUserOverviewController = (props: { providedKey: string }) => {
const [withdrawed, setWithdrawed] = useState<boolean>(false)
const appToaster = useAppToaster()
const { loading, error, data, refetch } = useGetApplicationByUserAccessKeyQuery({
variables: { accessKey: props.providedKey },
onError: error => {
console.error(error)
appToaster?.show({ intent: 'danger', message: 'Etwas ist schief gelaufen.' })
},
})

if (loading) return <Spinner />
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 (
<ApplicationViewComponent
application={data.application}
gotConfirmed={() => setWithdrawed(true)}
actionType={'withdraw'}
/>
f1sh1918 marked this conversation as resolved.
Show resolved Hide resolved
)
}
}

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

if (!accessKey) {
return <NotFound title='Nicht gefunden' description='Diese Seite konnte nicht gefunden werden.' />
} else {
return <ApplicationUserOverviewController providedKey={accessKey} />
}
}

export default ApplicationUserController
105 changes: 78 additions & 27 deletions administration/src/components/applications/ApplicationsOverview.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,35 @@
import { Alert, Button, Card, Divider, H4, IResizeEntry, NonIdealState, ResizeSensor } from '@blueprintjs/core'
import { Button, 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'
import JsonFieldView, { GeneralJsonField } from './JsonFieldView'
import { useAppToaster } from '../AppToaster'
import FlipMove from 'react-flip-move'
import { GetApplicationsQuery, useDeleteApplicationMutation } from '../../generated/graphql'
import {
GetApplicationsQuery,
useDeleteApplicationMutation,
useWithdrawApplicationMutation,
} from '../../generated/graphql'
import { ProjectConfigContext } from '../../project-configs/ProjectConfigContext'
import VerificationsView, { VerificationsQuickIndicator } from './VerificationsView'
import ApplicationAction from './ApplicationAction'
import { useParams } from 'react-router-dom'

type Application = GetApplicationsQuery['applications'][number]
export type Application = GetApplicationsQuery['applications'][number]
export type ActionType = 'delete' | 'withdraw'

const CARD_PADDING = 20
const COLLAPSED_HEIGHT = 250

const ApplicationViewCard = styled(Card)<{ $collapsed: boolean; $contentHeight: number }>`
const ApplicationViewCard = styled(Card)<{ $collapsed: boolean; $contentHeight: number; $centered: boolean }>`
transition: height 0.2s;
height: ${props => (props.$collapsed ? COLLAPSED_HEIGHT : props.$contentHeight + 2 * CARD_PADDING)}px;
width: 600px;
overflow: hidden;
margin: 10px;
padding: 0;
position: relative;
${props => props.$centered && 'align-self: center;'}
`

const ExpandContainer = styled.div<{ $collapsed: boolean }>`
Expand All @@ -40,40 +48,69 @@ const ExpandContainer = styled.div<{ $collapsed: boolean }>`
pointer-events: ${props => (props.$collapsed ? 'all' : 'none')};
`

const ApplicationView: FunctionComponent<{ application: Application; gotDeleted: () => void }> = ({
application,
gotDeleted,
}) => {
const ApplicationView: FunctionComponent<{
application: Application
gotConfirmed: () => void
actionType: ActionType
}> = ({ application, gotConfirmed, actionType }) => {
const { createdDate: createdDateString, jsonValue, id } = application
const { accessKey } = useParams()
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 [collapsed, setCollapsed] = useState(false)
const [height, setHeight] = useState(0)
const appToaster = useAppToaster()
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
const [deleteApplication, { loading }] = useDeleteApplicationMutation({
onError: error => {
console.error(error)
appToaster?.show({ intent: 'danger', message: 'Etwas ist schief gelaufen.' })
},
onCompleted: ({ deleted }: { deleted: boolean }) => {
if (deleted) gotDeleted()
if (deleted) gotConfirmed()
f1sh1918 marked this conversation as resolved.
Show resolved Hide resolved
else {
console.error('Delete operation returned false.')
appToaster?.show({ intent: 'danger', message: 'Etwas ist schief gelaufen.' })
}
},
})

const [withdrawApplication, { loading: withdrawalLoading }] = useWithdrawApplicationMutation({
onError: error => {
console.error(error)
appToaster?.show({ intent: 'danger', message: 'Etwas ist schief gelaufen.' })
},
onCompleted: ({ withdrawed }: { withdrawed: boolean }) => {
if (withdrawed) gotConfirmed()
else {
console.error('Witdraw operation returned false.')
appToaster?.show({ intent: 'danger', message: 'Etwas ist schief gelaufen.' })
}
},
})

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

f1sh1918 marked this conversation as resolved.
Show resolved Hide resolved
const handleResize = (entries: IResizeEntry[]) => {
setHeight(entries[0].contentRect.height)
if (height === 0 && entries[0].contentRect.height > COLLAPSED_HEIGHT) setCollapsed(true)
}

return (
<ApplicationViewCard elevation={2} $collapsed={collapsed} $contentHeight={height}>
<ApplicationViewCard
elevation={2}
$collapsed={collapsed}
$contentHeight={height}
$centered={actionType === 'withdraw'}>
<ExpandContainer onClick={() => setCollapsed(false)} $collapsed={collapsed}>
<Button icon='caret-down'>Mehr anzeigen</Button>
</ExpandContainer>
Expand All @@ -89,6 +126,9 @@ const ApplicationView: FunctionComponent<{ application: Application; gotDeleted:
<H4>Antrag vom {format(createdDate, 'dd.MM.yyyy, HH:mm')}</H4>
<VerificationsQuickIndicator verifications={application.verifications} />
</div>
{actionType === 'delete' && application.withdrawalDate && (
<div style={{ marginBottom: '16px', color: 'red' }}>Antrag wurde zurückgezogen.</div>
)}
<JsonFieldView jsonField={jsonField} baseUrl={baseUrl} key={0} hierarchyIndex={0} />
<Divider style={{ margin: '24px 0px' }} />
<VerificationsView verifications={application.verifications} />
Expand All @@ -104,20 +144,26 @@ const ApplicationView: FunctionComponent<{ application: Application; gotDeleted:
Weniger anzeigen
</Button>
) : null}
<Button onClick={() => setDeleteDialogOpen(true)} intent='danger' icon='trash'>
Antrag löschen
</Button>
<Alert
cancelButtonText='Abbrechen'
confirmButtonText='Antrag löschen'
icon='trash'
intent='danger'
isOpen={deleteDialogOpen}
loading={loading}
onCancel={() => setDeleteDialogOpen(false)}
onConfirm={() => deleteApplication({ variables: { applicationId: application.id } })}>
<p>Möchten Sie den Antrag unwiderruflich löschen?</p>
</Alert>
{actionType === 'delete' && (
<ApplicationAction
confirmAction={() => deleteApplication({ variables: { applicationId: application.id } })}
loading={loading}
cancelButtonText='Abbrechen'
confirmButtonText='Antrag löschen'
buttonLabel='Antrag löschen'
dialogText='Möchten Sie den Antrag unwiderruflich löschen?'
/>
)}
{actionType === 'withdraw' && !application.withdrawalDate && (
<ApplicationAction
confirmAction={submitWithdrawal}
loading={withdrawalLoading}
cancelButtonText='Abbrechen'
confirmButtonText='Antrag zurückziehen'
buttonLabel='Antrag zurückziehen'
dialogText='Möchten Sie den Antrag zurückziehen?'
/>
)}
f1sh1918 marked this conversation as resolved.
Show resolved Hide resolved
</div>
</div>
</ResizeSensor>
Expand All @@ -126,7 +172,11 @@ 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
gotConfirmed: () => void
actionType: ActionType
}> {
render() {
return <ApplicationView {...this.props} />
}
Expand All @@ -140,9 +190,10 @@ const ApplicationsOverview = (props: { applications: Application[] }) => {
<FlipMove style={{ display: 'flex', justifyContent: 'center', flexWrap: 'wrap' }}>
{updatedApplications.map(application => (
<ApplicationViewComponent
actionType={'delete'}
key={application.id}
application={application}
gotDeleted={() => setUpdatedApplications(updatedApplications.filter(a => a !== application))}
gotConfirmed={() => setUpdatedApplications(updatedApplications.filter(a => a !== application))}
/>
))}
{updatedApplications.length === 0 ? (
Expand Down
14 changes: 14 additions & 0 deletions administration/src/graphql/applications/getApplication.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
query getApplicationByUserAccessKey($accessKey: String!) {
application: getApplicationByUserAccessKey(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
Loading