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

Feature/725 admin recurring donation person select #728

21 changes: 21 additions & 0 deletions public/locales/bg/person.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"selectDialog": {
"notSelected": "Не сте избрали човек",
"select": "Избиране",
"personSelect": "Изберете човек",
"confirm": "Потвърждавам"
},
"autocomplete": {
"personSearch": "Намерете човек"
},
"info": {
"tel": "Тел",
"contact": "Контакти",
"address": "Адрес",
"email": "Имейл",
"general": "Генерална информация",
"createdAt": "Създаден в",
"company": "Компания",
"confirmedEmail": "Потвърден имейл"
}
}
21 changes: 21 additions & 0 deletions public/locales/en/person.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"selectDialog": {
"notSelected": "You haven't chosen a person",
"select": "Select",
"personSelect": "Person Select",
"confirm": "Confirm"
},
"autocomplete": {
"personSearch": "Find a person"
},
"info": {
"tel": "Тел",
"contact": "Contact info",
"address": "Address",
"email": "Email",
"general": "General information",
"createdAt": "Created at",
"company": "Company",
"confirmedEmail": "Confirmed email"
}
}
5 changes: 3 additions & 2 deletions src/common/hooks/person.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { useKeycloak } from '@react-keycloak/ssr'
import { PersonResponse } from 'gql/person'
import { KeycloakInstance } from 'keycloak-js'
import { useQuery } from 'react-query'
import { useQuery, UseQueryOptions } from 'react-query'
import { endpoints } from 'service/apiEndpoints'
import { authQueryFnFactory } from 'service/restRequests'

export const usePersonList = () => {
export const usePersonList = (options?: UseQueryOptions<PersonResponse[]>) => {
const { keycloak } = useKeycloak<KeycloakInstance>()
return useQuery<PersonResponse[]>(
endpoints.person.list.url,
authQueryFnFactory<PersonResponse[]>(keycloak?.token),
options,
)
}
44 changes: 44 additions & 0 deletions src/common/hooks/useConfirm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useState } from 'react'

export type ConfirmProps = {
onConfirm?: () => void | Promise<void>
onClose?: () => void | Promise<void>
}
export type ConfirmHookProps = {
open: boolean
loading: boolean
openHandler: () => void
confirmHandler: () => void
closeHandler: () => void
}

const useConfirm = ({ onConfirm, onClose }: ConfirmProps): ConfirmHookProps => {
const [open, setOpen] = useState<boolean>(false)
const [loading, setLoading] = useState<boolean>(false)

return {
open,
loading,
openHandler: () => {
setOpen(true)
},
confirmHandler: async () => {
setOpen(false)
if (typeof onConfirm === 'function') {
setLoading(true)
await onConfirm()
setLoading(false)
}
},
closeHandler: async () => {
setOpen(false)
if (typeof onClose === 'function') {
setLoading(true)
await onClose()
setLoading(false)
}
},
}
}

export default useConfirm
62 changes: 62 additions & 0 deletions src/components/common/FormFieldButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from 'react'
import { Button, Typography, Box } from '@mui/material'
import makeStyles from '@mui/styles/makeStyles'

type Props = {
error?: string
onClick?: () => void
placeholder?: string
value?: string
button?: { label: string }
}

const useStyles = makeStyles({
imitateInputBox: {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
border: `1px solid rgba(0, 0, 0, 0.23)`,
borderRadius: '3px',
padding: '8.5px 14px',
cursor: 'pointer',
},
errorInputBox: {
borderColor: '#d32f2f',
color: '#d32f2f',
},
errorText: {
color: '#d32f2f',
fontWeight: 400,
fontSize: '0.75rem',
lineHeight: 1.66,
letterSpacing: '0.03333em',
textAlign: 'left',
marginTop: '4px',
marginRight: '14px',
marginBottom: 0,
marginLeft: '14px',
},
})

function FormFieldButton({ error, onClick, value, placeholder, button }: Props) {
const classes = useStyles()
return (
<>
<Box
onClick={onClick}
className={
error ? classes.imitateInputBox + ' ' + classes.errorInputBox : classes.imitateInputBox
}>
<Typography>{value || placeholder}</Typography>
{button ? (
<Button sx={{ padding: 0 }} onClick={onClick}>
{button.label}
</Button>
) : null}
</Box>
{error ? <p className={classes.errorText}>{error}</p> : null}
</>
)
}

export default FormFieldButton
56 changes: 56 additions & 0 deletions src/components/person/PersonAutocomplete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useTranslation } from 'react-i18next'
import { Autocomplete, AutocompleteProps, TextField } from '@mui/material'
import { PersonResponse } from 'gql/person'
import { usePersonList } from 'common/hooks/person'

export type PersonAutocompleteProps = {
onSelect: (person: PersonResponse | null) => void
autocompleteProps?: Omit<
AutocompleteProps<PersonResponse, undefined, undefined, undefined>,
'renderInput' | 'options' | 'getOptionLabel' | 'onChange' | 'loading'
>
showId?: boolean
}
export default function PersonAutocomplete({
onSelect,
showId,
autocompleteProps,
}: PersonAutocompleteProps) {
const { t } = useTranslation('person')
const {
data: personList,
isLoading,
refetch,
} = usePersonList({
enabled: false,
refetchOnWindowFocus: false,
})
return (
<Autocomplete
isOptionEqualToValue={(option, value) => option.firstName === value.firstName}
options={personList || []}
getOptionLabel={(person) =>
showId
? `${person.firstName} ${person.lastName} (${person.id})`
: person.firstName + ' ' + person.lastName
}
onChange={(e, person) => {
onSelect(person)
}}
onOpen={() => {
refetch()
}}
loading={isLoading}
renderInput={(params) => (
<TextField
{...params}
type="text"
fullWidth
defaultValue=""
label={t('person:autocomplete.personSearch')}
/>
)}
{...autocompleteProps}
/>
)
}
69 changes: 69 additions & 0 deletions src/components/person/PersonInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Box, Grid, Theme, Typography } from '@mui/material'
import createStyles from '@mui/styles/createStyles'
import makeStyles from '@mui/styles/makeStyles'
import theme from 'common/theme'
import { formatDateString } from 'common/util/date'
import { PersonResponse } from 'gql/person'
import { useTranslation } from 'next-i18next'
import React from 'react'

type Props = {
person: PersonResponse
}

const useStyles = makeStyles((theme: Theme) =>
createStyles({
infoHeading: {
marginBottom: theme.spacing(2),
marginTop: theme.spacing(2),
},
infoWrapper: {
'&>*': {
marginBottom: theme.spacing(1),
},
},
}),
)

function PersonInfo({ person }: Props) {
const classes = useStyles()
const { t } = useTranslation()
return (
<Grid container>
<Grid item xs={12} md={6}>
<Typography className={classes.infoHeading} variant="h6" color={theme.palette.primary.dark}>
{t('person:info.contact')}
</Typography>
<Box className={classes.infoWrapper}>
<Typography>
{t('person:info.email')}: {person.email}
</Typography>
<Typography>
{t('person:info.tel')}: {person.phone}
</Typography>
<Typography>
{t('person:info.address')}: {person.address}
</Typography>
</Box>
</Grid>
<Grid item xs={12} md={6}>
<Typography className={classes.infoHeading} variant="h6" color={theme.palette.primary.dark}>
{t('person:info.general')}
</Typography>
<Box className={classes.infoWrapper}>
<Typography>
{t('person:info.createdAt')}: {formatDateString(person.createdAt)}
</Typography>
<Typography>
{t('person:info.company')}: {person.company}
</Typography>
<Typography>
{t('person:info.confirmedEmail')}: {person.emailConfirmed ? 'Yes' : 'No'}
</Typography>
</Box>
</Grid>
</Grid>
)
}

export default PersonInfo
69 changes: 69 additions & 0 deletions src/components/person/PersonSelectDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { LoadingButton } from '@mui/lab'
import { Box, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material'
import { translateError } from 'common/form/validation'
import useConfirm from 'common/hooks/useConfirm'
import theme from 'common/theme'
import CloseModalButton from 'components/common/CloseModalButton'
import FormFieldButton from 'components/common/FormFieldButton'
import { PersonResponse } from 'gql/person'
import { useTranslation } from 'next-i18next'
import React, { useState } from 'react'
import PersonAutocomplete from './PersonAutocomplete'
import PersonInfo from './PersonInfo'

type Props = {
onConfirm?: (person: PersonResponse | null) => void
onClose?: (person: PersonResponse | null) => void
error?: string
}

function PersonSelectDialog({ onConfirm: confirmCallback, onClose: closeCallback, error }: Props) {
const [person, setPerson] = useState<PersonResponse | null>(null)
const { t } = useTranslation()
const { open, confirmHandler, closeHandler, openHandler, loading } = useConfirm({
onConfirm: async () => {
confirmCallback ? confirmCallback(person) : null
},
onClose: async () => {
closeCallback ? closeCallback(person) : null
},
})
console.log(person)
return (
<>
<FormFieldButton
onClick={openHandler}
placeholder={t('person:selectDialog.notSelected')}
value={person ? `${person.firstName} ${person.lastName} (${person.id})` : undefined}
button={{ label: t('person:selectDialog.select') }}
error={error ? translateError(error, t) : undefined}
/>
<Dialog fullWidth open={open} onClose={closeHandler}>
<DialogTitle>{t('person:selectDialog.personSelect')}</DialogTitle>
<DialogContent>
<Box sx={{ marginTop: theme.spacing(2) }}>
<PersonAutocomplete
onSelect={(person) => {
setPerson(person)
}}
showId
autocompleteProps={{ defaultValue: person }}
/>
</Box>

<Box sx={{ marginTop: theme.spacing(2) }}>
{person ? <PersonInfo person={person} /> : t('person:selectDialog.notSelected')}
</Box>
</DialogContent>
<DialogActions>
<CloseModalButton onClose={closeHandler} />
<LoadingButton onClick={confirmHandler} loading={loading}>
{t('person:selectDialog.confirm')}
</LoadingButton>
</DialogActions>
</Dialog>
</>
)
}

export default PersonSelectDialog
Loading