Skip to content
Open
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"@react-pdf/renderer": "^3.1.3",
"@sentry/nextjs": "^7.80.0",
"@stripe/react-stripe-js": "^1.16.1",
"@stripe/stripe-js": "^1.46.0",
"@stripe/stripe-js": "^3.1.0",
"@tanstack/react-query": "^4.16.1",
"@tryghost/content-api": "^1.11.4",
"axios": "^1.6.8",
Expand All @@ -62,6 +62,7 @@
"next-auth": "^4.24.5",
"next-i18next": "^14.0.3",
"nookies": "^2.5.2",
"papaparse": "^5.4.1",
"quill-blot-formatter": "^1.0.5",
"quill-html-edit-button": "^2.2.12",
"react": "18.2.0",
Expand Down Expand Up @@ -98,6 +99,7 @@
"@types/lodash.truncate": "^4.4.7",
"@types/lru-cache": "^5.1.1",
"@types/node": "14.14.37",
"@types/papaparse": "^5",
"@types/react": "18.2.14",
"@types/react-dom": "^18.0.0",
"@types/react-gtm-module": "2.0.0",
Expand Down
10 changes: 9 additions & 1 deletion src/common/hooks/bank-transactions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BankTransactionsHistoryResponse } from 'gql/bank-transactions'
import { BankTransactionsHistoryResponse, BankTransactionsInput } from 'gql/bank-transactions'
import { useSession } from 'next-auth/react'

import { useQuery } from '@tanstack/react-query'
Expand All @@ -21,3 +21,11 @@ export function useBankTransactionsList(
},
)
}

export function useFindBankTransaction(id: string) {
const { data: session } = useSession()
return useQuery<BankTransactionsInput>(
[endpoints.bankTransactions.getTransactionById(id).url],
authQueryFnFactory<BankTransactionsInput>(session?.accessToken),
)
}
9 changes: 9 additions & 0 deletions src/common/hooks/donation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
DonationResponse,
DonorsCountResult,
PaymentAdminResponse,
StripeChargeResponse,
TPaymentResponse,
TotalDonatedMoneyResponse,
UserDonationResult,
Expand Down Expand Up @@ -114,3 +115,11 @@ export function getTotalDonatedMoney() {
export function useDonatedUsersCount() {
return useQuery<DonorsCountResult>([endpoints.donation.getDonorsCount.url])
}

export function useGetStripeChargeFromPID(stripeId: string) {
const { data: session } = useSession()
return useQuery<StripeChargeResponse>(
[endpoints.payments.referenceStripeWithInternal(stripeId).url],
{ queryFn: authQueryFnFactory(session?.accessToken) },
)
}
21 changes: 21 additions & 0 deletions src/common/util/lazyWithPreload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React, { ComponentProps, ComponentType, LazyExoticComponent } from 'react'

export type PreloadableComponent<T extends ComponentType<ComponentProps<T>>> =
LazyExoticComponent<T> & {
preload(): Promise<T>
}

function lazyWithPreload<T extends ComponentType<ComponentProps<T>>>(
factory: () => Promise<{ default: T }>,
): PreloadableComponent<T> {
const Component: Partial<PreloadableComponent<T>> = React.lazy(factory)

Component.preload = async () => {
const LoadableComponent = await factory()
return LoadableComponent.default
}

return Component as PreloadableComponent<T>
}

export default lazyWithPreload
2 changes: 1 addition & 1 deletion src/components/admin/payments/PaymentsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const ModalStore = new ModalStoreImpl()
export const RefundStore = new RefundStoreImpl()
export const InvalidateStore = new ModalStoreImpl()

export default function DocumentsPage() {
export default function PaymentsPage() {
const { t } = useTranslation()

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React, { createContext } from 'react'
import {
Actions,
CreatePayment,
createPaymentStepReducer,
} from './helpers/createPaymentStepReducer'
import CreatePaymentStepper from './CreatePaymentStepper'
import { observer } from 'mobx-react'

export type PaymentContext = {
payment: CreatePayment
dispatch: React.Dispatch<Actions>
}
export const PaymentContext = createContext<PaymentContext>({} as PaymentContext)

function CreatePaymentDialog() {
const [payment, dispatch] = createPaymentStepReducer()
return (
<PaymentContext.Provider value={{ payment, dispatch }}>
<CreatePaymentStepper />
</PaymentContext.Provider>
)
}

export default observer(CreatePaymentDialog)
113 changes: 113 additions & 0 deletions src/components/admin/payments/create-payment/CreatePaymentStepper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { Button, Card, CardContent, Dialog, Grid, IconButton } from '@mui/material'

import { Form, Formik } from 'formik'
import React, { useContext } from 'react'
import * as yup from 'yup'
import { FileImportDialog } from './benevity/FileImportDialog'
import { benevityInputValidation, benevityValidation } from './benevity/helpers/validation'
import BenevityImportTypeSelector from './benevity/BenevityImportTypeSelector'
import CreatePaymentFromBenevityRecord from './benevity/CreatePaymentFromBenevityRecord'
import { PaymentContext } from './CreatePaymentDialog'
import PaymentTypeSelector from './PaymentTypeSelector'
import { BenevityManualImport } from './benevity/BenevityManualImport'
import { stripeInputValidation } from './stripe/helpers/validations'
import { StripeChargeLookupForm } from './stripe/StripeChargeLookupForm'
import { SelectedPaymentSource } from './helpers/createPaymentStepReducer'
import { CreatePaymentFromStripeCharge } from './stripe/CreatePaymentFromStripeCharge'
import { ModalStore } from '../PaymentsPage'
import ArrowBackIcon from '@mui/icons-material/ArrowBack'
import { Close } from '@mui/icons-material'
type Validation = yup.InferType<
typeof stripeInputValidation | typeof benevityValidation | typeof benevityInputValidation
>

type Steps<T> = {
[key in SelectedPaymentSource]: {
component: React.JSX.Element
validation?: yup.SchemaOf<T> | null
}[]
}

function CreatePaymentStepper() {
const paymentContext = useContext(PaymentContext)
const { isPaymentImportOpen, hideImport } = ModalStore
const { payment, dispatch } = paymentContext

const steps: Steps<Validation> = {
none: [{ component: <PaymentTypeSelector /> }],
stripe: [
{
component: <StripeChargeLookupForm />,
validation: stripeInputValidation,
},
{
component: <CreatePaymentFromStripeCharge />,
},
],
benevity: [
{
component: <BenevityImportTypeSelector />,
validation: null,
},
{
component:
payment.benevityImportType === 'file' ? <FileImportDialog /> : <BenevityManualImport />,
validation: payment.benevityImportType === 'file' ? null : benevityInputValidation,
},
{
component: <CreatePaymentFromBenevityRecord />,
validation: benevityValidation,
},
],
}
const onClose = () => {
dispatch({ type: 'RESET_MODAL' })
hideImport()
}

const handleOnBackClick = () => {
dispatch({ type: 'DECREMENT_STEP' })
}
return (
<Dialog open={isPaymentImportOpen} onClose={onClose} maxWidth={false}>
<Card sx={{ display: 'flex' }}>
<CardContent>
<Grid container direction="column" gap={3}>
<Grid
container
item
direction={'row'}
justifyContent={'space-between'}
px={1}
sx={{ display: payment.paymentSource !== 'none' ? 'flex' : 'none' }}>
<Button
startIcon={<ArrowBackIcon />}
onClick={handleOnBackClick}
sx={{ display: payment.paymentSource !== 'none' ? 'flex' : 'none', padding: 0 }}>
Назад
</Button>
<IconButton onClick={onClose} sx={{ alignSelf: 'flex-end', padding: 0 }}>
<Close color="error" />
</IconButton>
</Grid>
<Grid container item>
<Formik
validateOnBlur
onSubmit={async () => {
if (payment.step < steps[payment.paymentSource].length - 1) {
dispatch({ type: 'INCREMENT_STEP' })
}
}}
initialValues={{}}
validationSchema={steps[payment.paymentSource][payment.step].validation}>
<Form>{steps[payment.paymentSource][payment.step].component}</Form>
</Formik>
</Grid>
</Grid>
</CardContent>
</Card>
</Dialog>
)
}

export default CreatePaymentStepper
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Box, Button, Typography } from '@mui/material'
import { useTranslation } from 'next-i18next'
import React, { useContext } from 'react'
import { SelectedPaymentSource } from './helpers/createPaymentStepReducer'
import { PaymentContext } from './CreatePaymentDialog'

export default function PaymentTypeSelector() {
const paymentContext = useContext(PaymentContext)
const { t } = useTranslation('')
const handleSubmit = (source: SelectedPaymentSource) => {
paymentContext.dispatch({ type: 'UPDATE_PAYMENT_SOURCE', payload: source })
}
return (
<>
<Typography variant="h6" component={'h2'} sx={{ marginBottom: '16px', textAlign: 'center' }}>
Ръчно добавяне на плащане
</Typography>
<Typography variant="body1" sx={{ marginBottom: '16px', textAlign: 'center' }}>
{t('')} <b />
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', gap: 2 }}>
<Button variant="outlined" onClick={() => handleSubmit('stripe')}>
През Stripe
</Button>
<Button variant="outlined" onClick={() => handleSubmit('benevity')}>
През Benevity
</Button>
</Box>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { IconButton, TextField } from '@mui/material'
import { TranslatableField, translateError } from 'common/form/validation'
import { useField } from 'formik'
import { useTranslation } from 'next-i18next'
import { useEffect, useRef, useState } from 'react'
import EditIcon from '@mui/icons-material/Edit'

export const BenevityInput = ({
name,
suffix,
canEdit = false,
}: {
name: string
suffix?: string
canEdit?: boolean
}) => {
const { t } = useTranslation()
const [editable, setEditable] = useState(false)
const [field, meta] = useField(name)
const helperText = meta.touched ? translateError(meta.error as TranslatableField, t) : ''

const ref = useRef<HTMLDivElement | null>(null)

useEffect(() => {
if (!editable) return
const child = ref.current
child?.focus()
}, [editable])

const toggleEdit = () => {
setEditable((prev) => !prev)
}
const onBlur = (
formikBlur: typeof field.onBlur,
e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement, Element>,
) => {
formikBlur(e)
toggleEdit()
}

return (
<TextField
helperText={helperText}
error={Boolean(meta.error) && Boolean(meta.touched)}
variant="standard"
id={field.name}
type="text"
{...field}
onBlur={(e) => onBlur(field.onBlur, e)}
InputProps={{
disableUnderline: !editable,
inputRef: ref,
disabled: !editable,
sx: {
'& .MuiInputBase-input.Mui-disabled': {
WebkitTextFillColor: '#000000',
},
'& .MuiInputBase-input': {
width: `${String(field.value).length + 1}ch`,
maxWidth: !editable ? `12ch` : 'auto',
},
},
endAdornment: (
<>
{suffix && <span>{suffix}</span>}
{canEdit && (
<IconButton size="small" onClick={toggleEdit} sx={{ color: 'primary.light' }}>
<EditIcon />
</IconButton>
)}
</>
),
}}
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Box, Button, Typography } from '@mui/material'
import { useTranslation } from 'next-i18next'
import { observer } from 'mobx-react'
import { useContext } from 'react'
import { PaymentContext } from '../CreatePaymentDialog'
import { BenevityImportType } from '../helpers/createPaymentStepReducer'

function BenevityImportFirstStep() {
const { t } = useTranslation()
const paymentContext = useContext(PaymentContext)

const handleImportTypeChange = (importType: BenevityImportType) => {
paymentContext.dispatch({ type: 'SET_BENEVITY_IMPORT_TYPE', payload: importType })
}

return (
<>
<Typography variant="h6" sx={{ marginBottom: '16px', textAlign: 'center' }}>
Прикачване на CSV файл
</Typography>
<Typography variant="body1" sx={{ marginBottom: '16px', textAlign: 'center' }}>
{t('Създаване на дарения от Benevity, чрез CSV файл')} <b />
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', gap: 2 }}>
<Button onClick={() => handleImportTypeChange('file')} variant="outlined">
{t('Чрез файл')}
</Button>
<Button onClick={() => handleImportTypeChange('manual')} variant="outlined">
{t('Ръчно въвеждане')}
</Button>
</Box>
</>
)
}

export default observer(BenevityImportFirstStep)
Loading